accounts addressbook incl. working updates for active directory

This commit is contained in:
Ralf Becker 2013-06-01 17:55:33 +00:00
parent 5c63214e82
commit d328af7cff
5 changed files with 273 additions and 116 deletions

View File

@ -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);
}
}

View File

@ -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)
{

View File

@ -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 != '*')
{

View File

@ -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();
}

View File

@ -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;
}
}