From d328af7cff7c8acce7f84f01af9719c7b7a16145 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Sat, 1 Jun 2013 17:55:33 +0000 Subject: [PATCH] accounts addressbook incl. working updates for active directory --- addressbook/inc/class.addressbook_ads.inc.php | 67 ++++++++- .../inc/class.addressbook_ldap.inc.php | 141 ++++++++++++++---- phpgwapi/inc/class.accounts_ads.inc.php | 14 ++ phpgwapi/inc/class.ldap.inc.php | 78 +--------- phpgwapi/inc/class.ldapserverinfo.inc.php | 89 +++++++++++ 5 files changed, 273 insertions(+), 116 deletions(-) diff --git a/addressbook/inc/class.addressbook_ads.inc.php b/addressbook/inc/class.addressbook_ads.inc.php index 08c4e6999a..db06f14b03 100644 --- a/addressbook/inc/class.addressbook_ads.inc.php +++ b/addressbook/inc/class.addressbook_ads.inc.php @@ -12,12 +12,15 @@ /** * Active directory backend for accounts (not yet AD contacts) * - * We use ADS objectGUID as contact ID and UID. + * We use ADS string representation of objectGUID as contact ID and UID. + * + * Unfortunatly Samba4 and active directory of win2008r2 differn on how to search for an objectGUID: + * - Samba4 can only search for string representation eg. (objectGUID=2336A3FC-EDBD-42A2-9EEB-BD7A5DD2804E) + * - win2008r2 can only search for hex representation eg. (objectGUID=\FC\A3\36\23\BD\ED\A2\42\9E\EB\BD\7A\5D\D2\80\4E) + * We could use both filters or-ed together, for now we detect Samba4 and use string GUID for it. * * All values used to construct filters need to run through ldap::quote(), * to be save against LDAP query injection!!! - * - * @todo get saving of contacts working: fails while checking of container exists ... */ class addressbook_ads extends addressbook_ldap { @@ -37,6 +40,13 @@ class addressbook_ads extends addressbook_ldap */ var $accountsFilter = '(objectclass=user)'; + /** + * Attribute used for DN + * + * @var string + */ + var $dn_attribute='cn'; + /** * Accounts ADS object * @@ -44,6 +54,14 @@ class addressbook_ads extends addressbook_ldap */ protected $accounts_ads; + /** + * ADS is Samba4 (true), otherwise false + * + * @var boolean + */ + public $is_samba4 = false; + + /** * constructor of the class * @@ -78,6 +96,7 @@ class addressbook_ads extends addressbook_ldap $this->connect(); } $this->ldapServerInfo = ldapserverinfo::get($this->ds, $this->ldap_config['ads_host']); + $this->is_samba4 = $this->ldapServerInfo->serverType == SAMBA4_LDAPSERVER; // AD seems to use user, instead of inetOrgPerson $this->schema2egw['user'] = $this->schema2egw['inetorgperson']; @@ -106,6 +125,25 @@ class addressbook_ads extends addressbook_ldap $this->ds = $this->accounts_ads->ldap_connection(); } + /** + * Return LDAP filter for contact id + * + * @param string $contact_id + * @return string + */ + protected function id_filter($contact_id) + { + // check that GUID eg. from URL contains only valid hex characters and dash + // we cant use ldap::quote() for win2008r2 hex GUID, as it contains backslashes + if (!preg_match('/^[0-9A-Fa-f-]+/', $contact_id)) + { + throw new egw_exception_assertion_failed("'$contact_id' is NOT a valid GUID!"); + } + + // samba4 can only search by string representation of objectGUID, while win2008r2 requires hex representation + return '(objectguid='.($this->is_samba4 ? $contact_id : $this->accounts_ads->objectguid2hex($contact_id)).')'; + } + /** * reads contact data * @@ -120,11 +158,11 @@ class addressbook_ads extends addressbook_ldap $account_id = (int)(is_array($contact_id) ? $contact_id['account_id'] : substr($contact_id,8)); $contact_id = $GLOBALS['egw']->accounts->id2name($account_id, 'person_id'); } - $contact_id = ldap::quote(!is_array($contact_id) ? $contact_id : - (isset ($contact_id['id']) ? $contact_id['id'] : $contact_id['uid'])); - - $rows = $this->_searchLDAP($this->allContactsDN, "(objectguid=$contact_id)", $this->all_attributes, ADDRESSBOOK_ALL); + $contact_id = !is_array($contact_id) ? $contact_id : + (isset ($contact_id['id']) ? $contact_id['id'] : $contact_id['uid']); + $rows = $this->_searchLDAP($this->allContactsDN, $filter=$this->id_filter($contact_id), $this->all_attributes, ADDRESSBOOK_ALL); + //error_log(__METHOD__."('$contact_id') _searchLDAP($this->allContactsDN, '$filter',...)=".array2string($rows)); return $rows ? $rows[0] : false; } @@ -147,4 +185,19 @@ class addressbook_ads extends addressbook_ldap $this->_inetorgperson2egw($contact, $data); } + + /** + * Remove attributes we are not allowed to update + * + * @param array $attributes + */ + function sanitize_update(array &$ldapContact) + { + // not allowed and not need to update these in AD + unset($ldapContact['objectguid']); + unset($ldapContact['objectsid']); + + parent::sanitize_update($ldapContact); + } + } diff --git a/addressbook/inc/class.addressbook_ldap.inc.php b/addressbook/inc/class.addressbook_ldap.inc.php index 61c7939d8d..be9832d69d 100644 --- a/addressbook/inc/class.addressbook_ldap.inc.php +++ b/addressbook/inc/class.addressbook_ldap.inc.php @@ -75,6 +75,13 @@ class addressbook_ldap */ var $allContactsDN; + /** + * Attribute used for DN + * + * @var string + */ + var $dn_attribute='uid'; + /** * @var int $total holds the total count of found rows */ @@ -237,6 +244,13 @@ class addressbook_ldap */ private $ldap_config; + /** + * LDAP connection + * + * @var resource + */ + var $ds; + /** * constructor of the class * @@ -256,10 +270,10 @@ class addressbook_ldap { $this->ldap_config =& $GLOBALS['egw_info']['server']; } - $this->personalContactsDN = 'ou=personal,ou=contacts,'. $this->ldap_config['ldap_contact_context']; - $this->sharedContactsDN = 'ou=shared,ou=contacts,'. $this->ldap_config['ldap_contact_context']; $this->accountContactsDN = $this->ldap_config['ldap_context']; $this->allContactsDN = $this->ldap_config['ldap_contact_context']; + $this->personalContactsDN = 'ou=personal,ou=contacts,'. $this->allContactsDN; + $this->sharedContactsDN = 'ou=shared,ou=contacts,'. $this->allContactsDN; if ($ds) { @@ -303,7 +317,7 @@ class addressbook_ldap elseif (substr($GLOBALS['egw_info']['server']['contact_repository'],-4) != 'ldap') // not (ldap or sql-ldap) { $this->ldap_config['ldap_contact_host'] = $this->ldap_config['ldap_host']; - $this->ldap_config['ldap_contact_context'] = $this->ldap_config['ldap_context']; + $this->allContactsDN = $this->ldap_config['ldap_context']; $this->ds = $GLOBALS['egw']->ldap->ldapConnect(); } else @@ -340,6 +354,37 @@ class addressbook_ldap return array_values(array_unique($fields)); } + /** + * Return LDAP filter for contact id + * + * @param string $id + * @return string + */ + protected function id_filter($id) + { + return '(|(entryUUID='.ldap::quote($id).')(uid='.ldap::quote($id).'))'; + } + + /** + * Return LDAP filter for (multiple) contact ids + * + * @param array|string $ids + * @return string + */ + protected function ids_filter($ids) + { + if (!is_array($ids) || count($ids) == 1) + { + return $this->id_filter(is_array($ids) ? array_shift($ids) : $ids); + } + $filter = array(); + foreach($ids as $id) + { + $filter[] = $this->id_filter($id); + } + return '(|'.implode('', $filter).')'; + } + /** * reads contact data * @@ -355,16 +400,30 @@ class addressbook_ldap } else { - $contact_id = ldap::quote(!is_array($contact_id) ? $contact_id : - (isset ($contact_id['id']) ? $contact_id['id'] : $contact_id['uid'])); - $filter = "(|(entryUUID=$contact_id)(uid=$contact_id))"; + if (is_array($contact_id)) $contact_id = isset ($contact_id['id']) ? $contact_id['id'] : $contact_id['uid']; + $filter = $this->id_filter($contact_id); } - $rows = $this->_searchLDAP($this->ldap_config['ldap_contact_context'], + $rows = $this->_searchLDAP($this->allContactsDN, $filter, $this->all_attributes, ADDRESSBOOK_ALL); return $rows ? $rows[0] : false; } + /** + * Remove attributes we are not allowed to update + * + * @param array $attributes + */ + function sanitize_update(array &$ldapContact) + { + // never allow to change the uidNumber (account_id) on update, as it could be misused by eg. xmlrpc or syncml + unset($ldapContact['uidnumber']); + + unset($ldapContact['entryuuid']); // not allowed to modify that, no need either + + unset($ldapContact['objectClass']); + } + /** * saves the content of data to the db * @@ -403,7 +462,7 @@ class addressbook_ldap $baseDN = $this->accountContactsDN; $cn = false; // we need an admin connection - $this->ds = $this->connect(true); + $this->connect(true); // for sql-ldap we need to account_lid/uid as id, NOT the contact_id in id! if ($GLOBALS['egw_info']['server']['contact_repository'] == 'sql-ldap') @@ -416,7 +475,6 @@ class addressbook_ldap error_log("Permission denied, to write: data[owner]=$data[owner], data[account_id]=$data[account_id], account_id=".$GLOBALS['egw_info']['user']['account_id']); return lang('Permission denied !!!'); // only admin or the user itself is allowd to write accounts! } - // check if $baseDN exists. If not create it if (($err = $this->_check_create_dn($baseDN))) { @@ -424,11 +482,11 @@ class addressbook_ldap } // check the existing objectclasses of an entry, none = array() for new ones $oldObjectclasses = array(); - $attributes = array('dn','cn','objectClass','uid','mail'); + $attributes = array('dn','cn','objectClass',$this->dn_attribute,'mail'); + $contactUID = $this->data[$this->contacts_id]; - if(!empty($contactUID) && - ($result = ldap_search($this->ds, $this->ldap_config['ldap_contact_context'], - '(|(entryUUID='.ldap::quote($contactUID).')(uid='.ldap::quote($contactUID).'))', $attributes)) && + if (!empty($contactUID) && + ($result = ldap_search($this->ds, $base=$this->allContactsDN, $filter=$this->id_filter($contactUID), $attributes)) && ($oldContactInfo = ldap_get_entries($this->ds, $result)) && $oldContactInfo['count']) { unset($oldContactInfo[0]['objectclass']['count']); @@ -438,13 +496,12 @@ class addressbook_ldap } $isUpdate = true; } - if(!$contactUID) + + if(empty($contactUID)) { - $this->data[$this->contacts_id] = $contactUID = md5($GLOBALS['egw']->common->randomstring(15)); + $ldapContact[$this->contacts_id] = $this->data[$this->contacts_id] = $contactUID = md5($GLOBALS['egw']->common->randomstring(15)); } - - $ldapContact['uid'] = $contactUID; - + //error_log(__METHOD__."() contactUID='$contactUID', isUpdate=".array2string($isUpdate).", oldContactInfo=".array2string($oldContactInfo)); // add for all supported objectclasses the objectclass and it's attributes foreach($this->schema2egw as $objectclass => $mapping) { @@ -484,7 +541,7 @@ class addressbook_ldap $this->$egw2objectclass($ldapContact,$data,$isUpdate); } } - if($isUpdate) + if ($isUpdate) { // make sure multiple email-addresses in the mail attribute "survive" if (isset($ldapContact['mail']) && $oldContactInfo[0]['mail']['count'] > 1) @@ -497,9 +554,6 @@ class addressbook_ldap // update entry $dn = $oldContactInfo[0]['dn']; $needRecreation = false; - // never allow to change the uidNumber (account_id) on update, as it could be misused by eg. xmlrpc or syncml - unset($ldapContact['uidnumber']); - unset($ldapContact['entryuuid']); // not allowed to modify that, no need either // add missing objectclasses if($ldapContact['objectClass'] && array_diff($ldapContact['objectClass'],$oldObjectclasses)) @@ -522,9 +576,9 @@ class addressbook_ldap } // check if we need to rename the DN or need to recreate the contact - $newRDN = 'uid='. ldap::quote($contactUID); + $newRDN = $this->dn_attribute.'='. ldap::quote($ldapContact[$this->dn_attribute]); $newDN = $newRDN .','. $baseDN; - if(strtolower($dn) != strtolower($newDN) || $needRecreation) + if ($needRecreation) { $result = ldap_read($this->ds, $dn, 'objectclass=*'); $oldContact = ldap_get_entries($this->ds, $result); @@ -536,7 +590,7 @@ class addressbook_ldap $newContact[$key] = $value; } } - $newContact['uid'] = $contactUID; + $newContact[$this->dn_attribute] = $ldapContact[$this->dn_attribute]; if(is_array($ldapContact['objectClass']) && count($ldapContact['objectClass']) > 0) { @@ -559,7 +613,21 @@ class addressbook_ldap } $dn = $newDN; } - unset($ldapContact['objectClass']); + // try renaming entry if content of dn-attribute changed + if (strtolower($dn) != strtolower($newDN) || $ldapContact[$this->dn_attribute] != $oldContactInfo[$this->dn_attribute]) + { + if (@ldap_rename($this->ds, $dn, $newRDN, null, true)) + { + $dn = $newDN; + } + else + { + error_log(__METHOD__."() ldap_rename or $dn to $newRDN failed! ".ldap_error($this->ds)); + } + } + unset($ldapContact[$this->dn_attribute]); + + $this->sanitize_update($ldapContact); if (!@ldap_modify($this->ds, $dn, $ldapContact)) { @@ -571,7 +639,7 @@ class addressbook_ldap } else { - $dn = 'uid='. ldap::quote($ldapContact['uid']) .','. $baseDN; + $dn = $this->dn_attribute.'='. ldap::quote($ldapContact[$this->dn_attribute]) .','. $baseDN; unset($ldapContact['entryuuid']); // trying to write it, gives an error if (!@ldap_add($this->ds, $dn, $ldapContact)) @@ -608,7 +676,7 @@ class addressbook_ldap foreach($keys as $entry) { $entry = ldap::quote(is_array($entry) ? $entry['id'] : $entry); - if($result = ldap_search($this->ds, $this->ldap_config['ldap_contact_context'], + if($result = ldap_search($this->ds, $this->allContactsDN, "(|(entryUUID=$entry)(uid=$entry))", $attributes)) { $contactInfo = ldap_get_entries($this->ds, $result); @@ -715,6 +783,11 @@ class addressbook_ldap $searchFilter = ''; foreach($criteria as $egwSearchKey => $searchValue) { + if (in_array($egwSearchKey, array('id','contact_id'))) + { + $searchFilter .= $this->ids_filter($searchValue); + continue; + } foreach($this->schema2egw as $mapping) { if(($ldapSearchKey = $mapping[$egwSearchKey])) @@ -736,7 +809,6 @@ class addressbook_ldap } $colFilter = $this->_colFilter($filter); $ldapFilter = "(&$objectFilter$searchFilter$colFilter)"; - if (!($rows = $this->_searchLDAP($searchDN, $ldapFilter, $this->all_attributes, $addressbookType))) { return $rows; @@ -849,6 +921,11 @@ class addressbook_ldap } break; + case 'id': + case 'contact_id': + $filter .= $this->ids_filter($value); + break; + default: if (!is_int($key)) { @@ -1020,7 +1097,7 @@ class addressbook_ldap /** * check if $baseDN exists. If not create it * - * @param string $baseDN cn=xxx,ou=yyy,ou=contacts,$this->ldap_config['ldap_contact_context'] + * @param string $baseDN cn=xxx,ou=yyy,ou=contacts,$this->allContactsDN * @return boolean/string false on success or string with error-message */ function _check_create_dn($baseDN) @@ -1042,8 +1119,8 @@ class addressbook_ldap list(,$ou) = explode(',',$baseDN); foreach(array( - 'ou=contacts,'.$this->ldap_config['ldap_contact_context'], - $ou.',ou=contacts,'.$this->ldap_config['ldap_contact_context'], + 'ou=contacts,'.$this->allContactsDN, + $ou.',ou=contacts,'.$this->allContactsDN, $baseDN, ) as $dn) { diff --git a/phpgwapi/inc/class.accounts_ads.inc.php b/phpgwapi/inc/class.accounts_ads.inc.php index 511eb2e856..f7b7b4f672 100644 --- a/phpgwapi/inc/class.accounts_ads.inc.php +++ b/phpgwapi/inc/class.accounts_ads.inc.php @@ -29,6 +29,7 @@ require_once EGW_API_INC.'/adldap/adLDAP.php'; * @access internal only use the interface provided by the accounts class * @link http://www.selfadsi.org/user-attributes-w2k8.htm * @link http://www.selfadsi.org/attributes-e2k7.htm + * @link http://msdn.microsoft.com/en-us/library/ms675090(v=vs.85).aspx */ class accounts_ads { @@ -298,6 +299,17 @@ class accounts_ads return $this->adldap->utilities()->decodeGuid(is_array($objectguid) ? $objectguid[0] : $objectguid); } + /** + * Convert a string GUID to hex string used in filter + * + * @param string $strGUID + * @return int + */ + public function objectguid2hex($strGUID) + { + return $this->adldap->utilities()->strGuidToHex($strGUID); + } + /** * Reads the data of one account * @@ -841,6 +853,8 @@ class accounts_ads } if ($param['type'] == 'groups' || $param['type'] == 'both') { + $query = ldap::quote(strtolower($param['query'])); + $filter = null; if(!empty($query) && $query != '*') { diff --git a/phpgwapi/inc/class.ldap.inc.php b/phpgwapi/inc/class.ldap.inc.php index afaff35bac..6c766bb4fd 100644 --- a/phpgwapi/inc/class.ldap.inc.php +++ b/phpgwapi/inc/class.ldap.inc.php @@ -193,83 +193,7 @@ class ldap //error_log("no ldap server info found"); $ldapbind = @ldap_bind($this->ds, $GLOBALS['egw_info']['server']['ldap_root_dn'], $GLOBALS['egw_info']['server']['ldap_root_pw']); - $filter='(objectclass=*)'; - $justthese = array('structuralObjectClass','namingContexts','supportedLDAPVersion','subschemaSubentry'); - - if(($sr = @ldap_read($this->ds, '', $filter, $justthese))) - { - if($info = ldap_get_entries($this->ds, $sr)) - { - $this->ldapServerInfo = new ldapserverinfo($host); - - $this->ldapServerInfo->setVersion($supportedLDAPVersion); - - // check for naming contexts - if($info[0]['namingcontexts']) - { - for($i=0; $i<$info[0]['namingcontexts']['count']; $i++) - { - $namingcontexts[] = $info[0]['namingcontexts'][$i]; - } - $this->ldapServerInfo->setNamingContexts($namingcontexts); - } - - // check for ldap server type - if($info[0]['structuralobjectclass']) - { - switch($info[0]['structuralobjectclass'][0]) - { - case 'OpenLDAProotDSE': - $ldapServerType = OPENLDAP_LDAPSERVER; - break; - default: - $ldapServerType = UNKNOWN_LDAPSERVER; - break; - } - $this->ldapServerInfo->setServerType($ldapServerType); - } - - // check for subschema entry dn - if($info[0]['subschemasubentry']) - { - $subschemasubentry = $info[0]['subschemasubentry'][0]; - $this->ldapServerInfo->setSubSchemaEntry($subschemasubentry); - } - - // create list of supported objetclasses - if(!empty($subschemasubentry)) - { - $filter='(objectclass=*)'; - $justthese = array('objectClasses'); - - if($sr=ldap_read($this->ds, $subschemasubentry, $filter, $justthese)) - { - if($info = ldap_get_entries($this->ds, $sr)) - { - if($info[0]['objectclasses']) { - for($i=0; $i<$info[0]['objectclasses']['count']; $i++) - { - $pattern = '/^\( (.*) NAME \'(\w*)\' /'; - if(preg_match($pattern, $info[0]['objectclasses'][$i], $matches)) - { - #_debug_array($matches); - if(count($matches) == 3) - { - $supportedObjectClasses[$matches[1]] = strtolower($matches[2]); - } - } - } - $this->ldapServerInfo->setSupportedObjectClasses($supportedObjectClasses); - } - } - } - } - } - } - else - { - unset($this->ldapServerInfo); - } + $this->ldapServerInfo = ldapserverinfo::get($this->ds, $host, $supportedLDAPVersion); $this->saveSessionData(); } diff --git a/phpgwapi/inc/class.ldapserverinfo.inc.php b/phpgwapi/inc/class.ldapserverinfo.inc.php index 52be9e7801..336b207b9c 100644 --- a/phpgwapi/inc/class.ldapserverinfo.inc.php +++ b/phpgwapi/inc/class.ldapserverinfo.inc.php @@ -13,6 +13,7 @@ define('UNKNOWN_LDAPSERVER',0); define('OPENLDAP_LDAPSERVER',1); +define('SAMBA4_LDAPSERVER',2); /** * Class to store and retrieve information (eg. supported object classes) of a connected ldap server @@ -140,4 +141,92 @@ class ldapserverinfo } return false; } + + /** + * Query given ldap connection for available information + * + * @param resource $ds + * @param string $host + * @param int $version 2 or 3 + * @return ldapserverinfo + */ + public static function get($ds, $host, $version=3) + { + $filter='(objectclass=*)'; + $justthese = array('structuralObjectClass','namingContexts','supportedLDAPVersion','subschemaSubentry','vendorname'); + if(($sr = @ldap_read($ds, '', $filter, $justthese))) + { + if($info = ldap_get_entries($ds, $sr)) + { + $ldapServerInfo = new ldapserverinfo($host); + $ldapServerInfo->setVersion($version); + + // check for naming contexts + if($info[0]['namingcontexts']) + { + for($i=0; $i<$info[0]['namingcontexts']['count']; $i++) + { + $namingcontexts[] = $info[0]['namingcontexts'][$i]; + } + $ldapServerInfo->setNamingContexts($namingcontexts); + } + + // check for ldap server type + if($info[0]['structuralobjectclass']) + { + switch($info[0]['structuralobjectclass'][0]) + { + case 'OpenLDAProotDSE': + $ldapServerType = OPENLDAP_LDAPSERVER; + break; + default: + $ldapServerType = UNKNOWN_LDAPSERVER; + break; + } + $ldapServerInfo->setServerType($ldapServerType); + } + if ($info[0]['vendorname'] && stripos($info[0]['vendorname'][0], 'samba') !== false) + { + $ldapServerInfo->setServerType(SAMBA4_LDAPSERVER); + } + + // check for subschema entry dn + if($info[0]['subschemasubentry']) + { + $subschemasubentry = $info[0]['subschemasubentry'][0]; + $ldapServerInfo->setSubSchemaEntry($subschemasubentry); + } + + // create list of supported objetclasses + if(!empty($subschemasubentry)) + { + $filter='(objectclass=*)'; + $justthese = array('objectClasses'); + + if($sr=ldap_read($ds, $subschemasubentry, $filter, $justthese)) + { + if($info = ldap_get_entries($ds, $sr)) + { + if($info[0]['objectclasses']) { + for($i=0; $i<$info[0]['objectclasses']['count']; $i++) + { + $pattern = '/^\( (.*) NAME \'(\w*)\' /'; + if(preg_match($pattern, $info[0]['objectclasses'][$i], $matches)) + { + #_debug_array($matches); + if(count($matches) == 3) + { + $supportedObjectClasses[$matches[1]] = strtolower($matches[2]); + } + } + } + $ldapServerInfo->setSupportedObjectClasses($supportedObjectClasses); + } + } + } + } + } + } + return $ldapServerInfo; + } }