*/ class LSldap extends LSlog_staticLoggerClass { private static $config; private static $cnx = NULL; /** * Set configuration * * This method permit to define LDAP server access configuration * * @author Benjamin Renard * * @param[in] $config array Configuration array as accepted by Net_LDAP2 * * @retval void */ public static function setConfig ($config) { self :: $config = $config; } /** * Connect to LDAP server * * This method establish connection to LDAP server * * @author Benjamin Renard * * @param[in] $config array LDAP configuration array in format of Net_LDAP2 * * @retval boolean true if connected, false instead */ public static function connect($config = null) { if ($config) { self :: setConfig($config); } self :: $cnx = Net_LDAP2::connect(self :: $config); if (Net_LDAP2::isError(self :: $cnx)) { LSerror :: addErrorCode('LSldap_01',self :: $cnx -> getMessage()); self :: $cnx = NULL; return; } return true; } /** * Reconnect (or connect) with other credentials * * @author Benjamin Renard * * @param[in] $dn string Bind DN * @param[in] $pwd array Bind password * @param[in] $config array LDAP configuration array in format of Net_LDAP2 * (optional, default: keep current) * * @retval boolean true if connected, false instead */ public static function reconnectAs($dn, $pwd, $config=null) { if ($config) { self :: setConfig($config); } if (self :: $cnx) { self :: $cnx -> done(); } $config = self :: $config; $config['binddn'] = $dn; $config['bindpw'] = $pwd; self :: $cnx = Net_LDAP2::connect($config); if (Net_LDAP2::isError(self :: $cnx)) { LSerror :: addErrorCode('LSldap_01', self :: $cnx -> getMessage()); self :: $cnx = NULL; return; } return true; } /** * Set authz proxy control * * @author Benjamin Renard * * @param[in] $dn string Bind DN * * @retval boolean true if authz proxy controle is set, false otherwise */ public static function setAuthzProxyControl($dn) { if (!self :: $cnx) { self :: connect(); } $result = self :: $cnx -> setOption( 'LDAP_OPT_SERVER_CONTROLS', array ( array( 'oid' => '2.16.840.1.113730.3.4.18', 'value' => "dn:$dn", 'iscritical' => true ) ) ); // Also check user exists to validate the connection with // authz proxy control. if ($result !== True || !self :: exists($dn)) { LSerror :: addErrorCode('LSldap_09'); return False; } return True; } /** * Disconnect * * This method permit to close the connection to the LDAP server * * @author Benjamin Renard * * @retval void */ public static function close() { self :: $cnx -> done(); } /** * Search in LDAP directory * * This method make a search in LDAP directory and return the result as an array. * * @author Benjamin Renard * * @param[in] $filter [required] string The search LDAP filter * @param[in] $basedn string The base DN of the search * @param[in] $params array Array to search parameters as accepted by Net_LDAP2::search() * * @see Net_LDAP2::search() * * @retval array Return an array of entries returned by the LDAP directory. Each element * of this array corresponded to one returned entry and is an array with * the following keys: * - dn: The DN of the entry * - attrs: Associative array of the entry's attributes values */ public static function search($filter, $basedn=NULL, $params=array()) { $filterstr = (is_a($filter, 'Net_LDAP2_Filter')?$filter->as_string():$filter); if (is_empty($basedn)) { $basedn = self :: getConfig('basedn'); if (is_empty($basedn)) { LSerror :: addErrorCode('LSldap_08'); return; } self :: log_debug("LSldap::search($filterstr): empty basedn provided, use basedn from configuration: ".varDump($basedn)); } self :: log_trace("LSldap::search($filterstr, $basedn): run search with parameters: ".varDump($params)); $ret = self :: $cnx -> search($basedn, $filter, $params); if (Net_LDAP2::isError($ret)) { LSerror :: addErrorCode('LSldap_02', $ret -> getMessage()); return; } self :: log_debug("LSldap::search($filterstr, $basedn) : return ".$ret->count()." objet(s)"); $retInfos = array(); foreach($ret as $dn => $entry) { if (!$entry instanceof Net_LDAP2_Entry) { LSerror :: addErrorCode('LSldap_02', "LDAP search return an ".get_class($entry).". object"); continue; } $retInfos[] = array( 'dn' => $dn, 'attrs' => $entry -> getValues() ); } return $retInfos; } /** * Count the number of mathching objects found in LDAP directory * * This method make a search in LDAP directory and return the number of * macthing entries. * * @author Benjamin Renard * * @param[in] $filter [required] string The search LDAP filter * @param[in] $basedn string The base DN of the search * @param[in] $params array Array to search parameters as accepted by Net_LDAP2::search() * * @see Net_LDAP2::search() * * @retval integer|null The number of matching entries on success, null otherwise */ public static function getNumberResult($filter, $basedn=NULL, $params=array()) { if (empty($filter)) $filter = NULL; $filterstr = (is_a($filter, 'Net_LDAP2_Filter')?$filter->as_string():$filter); if (is_empty($basedn)) { $basedn = self :: getConfig('basedn'); if (is_empty($basedn)) { LSerror :: addErrorCode('LSldap_08'); return; } self :: log_debug("LSldap::getNumberResult($filterstr): empty basedn provided, use basedn from configuration: ".varDump($basedn)); } self :: log_trace("LSldap::getNumberResult($filterstr, $basedn): run search with parameters: ".varDump($params)); $ret = self :: $cnx -> search($basedn, $filter, $params); if (Net_LDAP2::isError($ret)) { LSerror :: addErrorCode('LSldap_02',$ret -> getMessage()); return; } $count = $ret -> count(); self :: log_trace("LSldap::getNumberResult($filterstr, $basedn): result=$count"); return $count; } /** * Load values of an LDAP entry attributes * * This method retrieve attributes values of an LDAP entry and return it * as associative array. * * @author Benjamin Renard * * @param[in] $dn string DN de l'entré Ldap * @param[in] $filter string LDAP filter string (optional, default: null == '(objectClass=*)') * @param[in] $attrs array|null Array of requested attribute (optional, default: null == all attributes, excepted internal) * @param[in] $include_internal boolean If true, internal attributes will be included (default: false) * * @retval array|false Associative array of attributes values (with attribute name as key), or false on error */ public static function getAttrs($dn, $filter=null, $attrs=null, $include_internal=false) { $infos = ldap_explode_dn($dn,0); if((!$infos)||($infos['count']==0)) return; if (!$filter) $filter = '(objectClass=*)'; $params = array( 'scope' => 'base', 'attributes' => (is_array($attrs)?$attrs:array('*')), ); if ($include_internal && !in_array('+', $params['attributes'])) $params['attributes'][] = '+'; $return = self :: search($filter, $dn, $params); if (is_array($return) && count($return) == 1) return $return[0]['attrs']; return false; } /** * Return a date string * * @param[in] string LDAP date * * @retval string Date YYYY/MM/DD HH:mm:ss */ public static function parseDate($value) { $datetime = date_create_from_format('YmdHis.uO', $value); return ($datetime instanceof DateTime) ? $datetime -> setTimezone(timezone_open(date_default_timezone_get())) : $datetime; } /** * Return a attribute value * * It performs a case-insensitive search. * * @author Emmanuel Saracco * * @param[in] $attrs array Array of LDAP attributes * @param[in] $name array Name of a attribute * * @retval boolean true if found */ public static function attrExists($attrs, $name) { return array_key_exists(strtolower($name), array_change_key_case($attrs)); } /** * Return a attribute value * * It performs a case-insensitive search. * * @author Emmanuel Saracco * * @param[in] $attrs array Array of LDAP attributes * @param[in] $name array Name of a attribute * @param[in] $multiple boolean true if we must return array * * @retval mixed Found value (or array of values) or null */ public static function getAttr($attrs, $name, $multiple = false) { $name = strtolower($name); foreach ($attrs as $k => $v) { if (strtolower($k) === $name) { return $multiple ? $v : $v[0]; } } return $multiple ? array() : null; } /** * Return an existing or new LDAP entry * * @author Benjamin Renard * * @param[in] $object_type string The object type * @param[in] $dn string The DN of the LDAP entry * * @retval ldapentry|array A Net_LDAP2_Entry object or an array if * it's a new entry: * Array ( * 'entry' => Net_LDAP2_Entry, * 'new' => true * ) */ public static function getEntry($object_type, $dn) { $obj_classes = LSconfig :: get("LSobjects.$object_type.objectclass"); if(!is_array($obj_classes)){ LSerror :: addErrorCode('LSldap_03'); return; } $attrs = array_keys(LSconfig :: get("LSobjects.$object_type.attrs", array(), 'array')); $entry = self :: getLdapEntry($dn, $attrs); if ($entry === false) { $newentry = self :: getNewEntry($dn, $obj_classes, array()); if (!$newentry) { return; } // Mark entry as new $newentry -> markAsNew(); return $newentry; } // Mark entry as NOT new $entry -> markAsNew(false); return $entry; } /** * Return a Net_LDAP2_Entry object of an existing entry * * @author Benjamin Renard * * @param[in] $dn string DN of the requested LDAP entry * @param[in] $attrs array|null Array of requested attribute (optional, default: null == all attributes, excepted internal) * * @retval ldapentry|boolean A Net_LDAP2_Entry object or false if error occured */ public static function getLdapEntry($dn, $attrs=null) { $entry = self :: $cnx -> getEntry($dn, (is_array($attrs)?$attrs:array())); if (Net_LDAP2::isError($entry)) { return false; } else { return $entry; } } /** * Check if an LDAP object exists * * @author Benjamin Renard * * @param[in] $dn string DN of the LDAP entry to check * * @retval boolean True if entry exists, false otherwise */ public static function exists($dn) { return is_a(self :: getLdapEntry($dn), 'Net_LDAP2_Entry'); } /** * Return a new Net_LDAP2_Entry object * * @param[in] $dn string The DN of the object * @param[in] $objectClass array Array of the object's object classes * @param[in] $attrs array Array of object attributes values. Format: array('attr_name' => attr_value, ...) * * @retval Net_LDAP2_Entry|False A Net_LDAP2_Entry object on success, False otherwise */ public static function getNewEntry($dn, $objectClass, $attrs, $add=false) { $newentry = Net_LDAP2_Entry::createFresh( $dn, array_merge( array('objectclass' =>$objectClass), ensureIsArray($attrs) ) ); if(Net_LDAP2::isError($newentry)) { return false; } if($add) { if(!self :: $cnx -> add($newentry)) { return; } } return $newentry; } /** * Update an entry in LDAP * * Note: this method drop empty attribute values and attributes without value. * * @author Benjamin Renard * * @param[in] $object_type string The object type * @param[in] $dn string DN of the LDAP object * @param[in] $change array Array of object attributes changes * * @retval boolean True if object was updated, False otherwise. */ public static function update($object_type, $dn, $change) { self :: log_trace("update($object_type, $dn): change=".varDump($change)); // Retrieve current LDAP entry $entry = self :: getEntry($object_type, $dn); if(!is_a($entry, 'Net_LDAP2_Entry')) { LSerror :: addErrorCode('LSldap_04'); return; } // Distinguish drop attributes from change attributes $changed_attrs = array(); $dropped_attrs = array(); foreach($change as $attrName => $attrVal) { $drop = true; if (is_array($attrVal)) { foreach($attrVal as $val) { if (!is_empty($val)) { $drop = false; $changed_attrs[$attrName][]=$val; } } } else { if (!is_empty($val)) { $drop = false; $changed_attrs[$attrName][]=$attrVal; } } if($drop) { $dropped_attrs[] = $attrName; } } self :: log_trace("update($object_type, $dn): changed attrs=".varDump($changed_attrs)); self :: log_trace("update($object_type, $dn): dropped attrs=".varDump($dropped_attrs)); // Set an error flag to false $error = false; // Handle special case: user password change if ($changed_attrs && self :: attrExists($changed_attrs, 'userPassword')) { $changed_attrs = self :: updateUserPassword($object_type, $changed_attrs, $dn); if ($changed_attrs === false) { return false; } } // Handle attributes changes (if need) if ($changed_attrs) { $entry -> replace($changed_attrs); if ($entry -> isNew()) { self :: log_debug("update($object_type, $dn): add new entry"); $ret = self :: $cnx -> add($entry); } else { self :: log_debug("update($object_type, $dn): update entry (for changed attributes)"); $ret = $entry -> update(); } if (Net_LDAP2::isError($ret)) { LSerror :: addErrorCode('LSldap_05',$dn); LSerror :: addErrorCode(0,'NetLdap-Error : '.$ret->getMessage()); return false; } } elseif ($entry -> isNew()) { self :: log_error("update($object_type, $dn): no changed attribute but it's a new entry..."); return false; } else { self :: log_debug("update($object_type, $dn): no changed attribute"); } // Handle droped attributes (is need and not a new entry) if ($dropped_attrs && !$entry -> isNew()) { // $entry -> delete() method is buggy (for some attribute like jpegPhoto) // Prefer replace attribute by an empty array $replace_attrs = array(); foreach($dropped_attrs as $attr) { // Check if attribute is present if(!$entry -> exists($attr)) { // Attribute not present on LDAP entry self :: log_debug("update($object_type, $dn): dropped attr $attr is not present in LDAP entry => ignore it"); continue; } $replace_attrs[$attr] = array(); } if (!$replace_attrs) { self :: log_debug("update($object_type, $dn): no attribute to drop"); return true; } // Replace values in LDAP $entry -> replace($replace_attrs); self :: log_debug("update($object_type, $dn): update entry (for dropped attributes: ".implode(', ', array_keys($replace_attrs)).")"); $ret = $entry -> update(); // Check result if (Net_LDAP2::isError($ret)) { LSerror :: addErrorCode('LSldap_06'); LSerror :: addErrorCode(0,'NetLdap-Error : '.$ret->getMessage()); return false; } } return true; } /** * Test to bind to LDAP directory * * This method establish a connection to the LDAP server and test * to bind with provided DN and password. * * @author Benjamin Renard * * @retval boolean True on bind success, False otherwise. */ public static function checkBind($dn,$pwd) { $config = self :: $config; $config['binddn'] = $dn; $config['bindpw'] = $pwd; $cnx = Net_LDAP2::connect($config); if (Net_LDAP2::isError($cnx)) { return; } return true; } /** * Return the status of the LDAP connection * * @retval boolean True if connected on LDAP server, False otherwise */ public static function isConnected() { return (self :: $cnx == NULL)?false:true; } /** * Drop an object in LDAP directory * * @param[in] string The DN of the object to remove * * @retval boolean True if object was removed, False otherwise. */ public static function remove($dn) { $ret = self :: $cnx -> delete($dn,array('recursive' => true)); if (Net_LDAP2::isError($ret)) { LSerror :: addErrorCode(0,'NetLdap-Error : '.$ret->getMessage()); return; } return true; } /** * Move an entry in LDAP directory * * @param[in] $old string The current object DN * @param[in] $new string The new object DN * * @retval boolean True if object was moved, False otherwise. */ public static function move($old, $new) { $ret = self :: $cnx -> move($old, $new); if (Net_LDAP2::isError($ret)) { LSerror :: addErrorCode('LSldap_07'); LSerror :: addErrorCode(0,'NetLdap-Error : '.$ret->getMessage()); return; } return true; } /** * Combine LDAP Filters * * @param[in] $op string The combine logical operator. May be "and", * "or", "not" or the subsequent logical * equivalents "&", "|", "!". * @param[in] $filters array Array of LDAP filters (as string or * Net_LDAP2_Filter object) * @param[in] $asStr boolean Set to true if you want to retreive * combined filter as string instead of * as a Net_LDAP2_Filter object (optional, * default: false) * * @retval Net_LDAP2_Filter | False The combined filter or False **/ public static function combineFilters($op, $filters, $asStr=false) { if (is_array($filters) && !empty($filters)) { if (count($filters)==1) { if ($asStr && $filters[0] instanceof Net_LDAP2_Filter) { return $filters[0]->asString(); } else { return $filters[0]; } } $filter=Net_LDAP2_Filter::combine($op,$filters); if (!Net_LDAP2::isError($filter)) { if ($asStr) { return $filter->asString(); } else { return $filter; } } else { LSerror :: addErrorCode(0,$filter -> getMessage()); } } return; } /** * Check LDAP Filters String * * @params string A LDAP filter as string * * @retval boolean True only if the filter could be parsed **/ public static function isValidFilter($filter) { if (is_string($filter) && !empty($filter)) { $filter=Net_LDAP2_Filter::parse($filter); if (!Net_LDAP2::isError($filter)) { return true; } else { LSerror :: addErrorCode(0,$filter -> getMessage()); } } return; } /** * Update userPassword attribute * * This method uses LDAP controls when possible (Net_LDAP2 does not). * * @param[in] $object_type string The object type * @param[in] $changed_attrs array Array of changed attributes * @param[in] $dn string DN of the LDAP object * * @author Emmanuel Saracco * * @retval mixed New array of changed attributes or false **/ private static function updateUserPassword($object_type, $changed_attrs, $dn) { if (self :: getConfig('version') < 3 || !function_exists('ldap_mod_replace_ext')) { return $changed_attrs; } $ppolicyErrorMsg = array( _('The password expired'), _('The account is locked'), _('The password was reset and must be changed'), _('It is not possible to modify the password'), _('The old password must be supplied'), _('The password does not meet the quality requirements'), _('The password is too short'), _('It is too soon to change the password'), _('This password was recently used and cannot be used again'), ); self :: log_debug("update($object_type, $dn): update entry for userPassword"); $ldap = self :: $cnx->getLink(); $attr = array('userPassword' => self :: getAttr($changed_attrs, 'userPassword')); $ctrlRequest = array(array('oid' => LDAP_CONTROL_PASSWORDPOLICYREQUEST)); $r = ldap_mod_replace_ext($ldap, $dn, $attr, $ctrlRequest); if ($r && ldap_parse_result($ldap, $r, $errcode, $matcheddn, $errmsg, $ref, $ctrlResponse)) { if ($errcode !== 0 && isset($ctrlResponse[LDAP_CONTROL_PASSWORDPOLICYRESPONSE])) { LSerror :: addErrorCode('LSldap_10', $ppolicyErrorMsg[$ctrlResponse[LDAP_CONTROL_PASSWORDPOLICYRESPONSE]['value']['error']]); return false; } // If everything OK, remove userPassword to prevent it from being processed by Net_LDAP2 unset($changed_attrs['userPassword']); } else { if (ldap_errno($ldap) !== 0) { LSerror :: addErrorCode('LSldap_10', ldap_error($ldap)); } else { LSerror :: addErrorCode('LSldap_11'); } return false; } return $changed_attrs; } /** * Return a configuration parameter (or default value) * * @param[] $param The configuration parameter * @param[] $default The default value (default : null) * @param[] $cast Cast resulting value in specific type (default : disabled) * * @retval mixed The configuration parameter value or default value if not set **/ private static function getConfig($param, $default=null, $cast=null) { return LSconfig :: get($param, $default, $cast, self :: $config); } } /* * Error Codes */ LSerror :: defineError('LSldap_01', ___("LSldap: Error during the LDAP server connection (%{msg}).") ); LSerror :: defineError('LSldap_02', ___("LSldap: Error during the LDAP search (%{msg}).") ); LSerror :: defineError('LSldap_03', ___("LSldap: Object type unknown.") ); LSerror :: defineError('LSldap_04', ___("LSldap: Error while fetching the LDAP entry.") ); LSerror :: defineError('LSldap_05', ___("LSldap: Error while changing the LDAP entry (DN : %{dn}).") ); LSerror :: defineError('LSldap_06', ___("LSldap: Error while deleting empty attributes.") ); LSerror :: defineError('LSldap_07', ___("LSldap: Error while changing the DN of the object.") ); LSerror :: defineError('LSldap_08', ___("LSldap: LDAP server base DN not configured.") ); LSerror :: defineError('LSldap_09', ___("LSldap: Fail to set authz proxy option on LDAP server connection.") ); LSerror :: defineError('LSldap_10', ___("LSldap: Error while changing the user password: %{msg}.") ); LSerror :: defineError('LSldap_11', ___("LSldap: Unknown LDAP error while updating user password") );