* LDAP/ActiveDirectory Sync: permanently store DN+entryUUID and use the latter to detect renamed user or accounts

This commit is contained in:
ralf 2024-08-12 14:32:14 +02:00
parent 10a7a4bd7e
commit 9888a681e7
6 changed files with 73 additions and 31 deletions

View File

@ -11,7 +11,7 @@
/* Basic information about this app */ /* Basic information about this app */
$setup_info['api']['name'] = 'api'; $setup_info['api']['name'] = 'api';
$setup_info['api']['title'] = 'EGroupware API'; $setup_info['api']['title'] = 'EGroupware API';
$setup_info['api']['version'] = '23.1.007'; $setup_info['api']['version'] = '23.1.008';
$setup_info['api']['versions']['current_header'] = '1.29'; $setup_info['api']['versions']['current_header'] = '1.29';
// maintenance release in sync with changelog in doc/rpm-build/debian.changes // maintenance release in sync with changelog in doc/rpm-build/debian.changes
$setup_info['api']['versions']['maintenance_release'] = '23.1.20240624'; $setup_info['api']['versions']['maintenance_release'] = '23.1.20240624';

View File

@ -64,7 +64,9 @@ $phpgw_baseline = array(
'account_expires' => array('type' => 'int','precision' => '4'), 'account_expires' => array('type' => 'int','precision' => '4'),
'account_type' => array('type' => 'char','precision' => '1'), 'account_type' => array('type' => 'char','precision' => '1'),
'account_primary_group' => array('type' => 'int','meta' => 'group','precision' => '4','nullable' => False,'default' => '0'), 'account_primary_group' => array('type' => 'int','meta' => 'group','precision' => '4','nullable' => False,'default' => '0'),
'account_description' => array('type' => 'varchar','precision' => '255','comment' => 'group description') 'account_description' => array('type' => 'varchar','precision' => '255','comment' => 'group description'),
'account_uuid' => array('type' => 'ascii','precision' => '64','comment' => 'UUID of synced (LDAP) entries'),
'account_dn' => array('type' => 'varchar','precision' => '255','comment' => 'DN or container')
), ),
'pk' => array('account_id'), 'pk' => array('account_id'),
'fk' => array(), 'fk' => array(),
@ -553,4 +555,4 @@ $phpgw_baseline = array(
'ix' => array('account_id'), 'ix' => array('account_id'),
'uc' => array() 'uc' => array()
) )
); );

View File

@ -976,4 +976,20 @@ function api_upgrade23_1_006()
)); ));
return $GLOBALS['setup_info']['api']['currentver'] = '23.1.007'; return $GLOBALS['setup_info']['api']['currentver'] = '23.1.007';
}
function api_upgrade23_1_007()
{
$GLOBALS['egw_setup']->oProc->AddColumn('egw_accounts','account_uuid',array(
'type' => 'ascii',
'precision' => '64',
'comment' => 'UUID of synced (LDAP) entries'
));
$GLOBALS['egw_setup']->oProc->AddColumn('egw_accounts','account_dn',array(
'type' => 'varchar',
'precision' => '255',
'comment' => 'DN or container'
));
return $GLOBALS['setup_info']['api']['currentver'] = '23.1.008';
} }

View File

@ -113,7 +113,7 @@ class Ads
* @var array * @var array
*/ */
protected static $default_attributes = array( protected static $default_attributes = array(
'objectsid', 'samaccounttype', 'samaccountname', 'objectsid', 'samaccounttype', 'samaccountname', 'entryuuid',
); );
/** /**
@ -122,7 +122,7 @@ class Ads
* @var array * @var array
*/ */
protected static $user_attributes = array( protected static $user_attributes = array(
'objectsid', 'samaccounttype', 'samaccountname', 'objectsid', 'samaccounttype', 'samaccountname', 'entryuuid',
'primarygroupid', 'givenname', 'sn', 'mail', 'displayname', 'telephonenumber', 'primarygroupid', 'givenname', 'sn', 'mail', 'displayname', 'telephonenumber',
'objectguid', 'useraccountcontrol', 'accountexpires', 'pwdlastset', 'whencreated', 'whenchanged', 'lastlogon', 'objectguid', 'useraccountcontrol', 'accountexpires', 'pwdlastset', 'whencreated', 'whenchanged', 'lastlogon',
'jpegphoto', 'jpegphoto',
@ -134,7 +134,7 @@ class Ads
* @var array * @var array
*/ */
protected static $group_attributes = array( protected static $group_attributes = array(
'objectsid', 'samaccounttype', 'samaccountname', 'objectsid', 'samaccounttype', 'samaccountname', 'entryuuid',
'objectguid', 'mail', 'whencreated', 'whenchanged', 'description', 'objectguid', 'mail', 'whencreated', 'whenchanged', 'description',
); );
@ -565,6 +565,7 @@ class Ads
$group = array( $group = array(
'account_dn' => $data['dn'], 'account_dn' => $data['dn'],
'account_uuid' => $data['entryuuid'][0],
'account_id' => $account_id, 'account_id' => $account_id,
'account_sid' => $sid, 'account_sid' => $sid,
'account_guid' => $this->adldap->utilities()->decodeGuid($data['objectguid'][0]), 'account_guid' => $this->adldap->utilities()->decodeGuid($data['objectguid'][0]),
@ -644,6 +645,7 @@ class Ads
$user = array( $user = array(
'account_dn' => $data['dn'], 'account_dn' => $data['dn'],
'account_uuid' => $data['entryuuid'][0],
'account_id' => $account_id, 'account_id' => $account_id,
'account_sid' => $sid, 'account_sid' => $sid,
'account_guid' => $this->adldap->utilities()->decodeGuid($data['objectguid'][0]), 'account_guid' => $this->adldap->utilities()->decodeGuid($data['objectguid'][0]),
@ -1327,6 +1329,8 @@ class Ads
'account_fullname' => 'cn', 'account_fullname' => 'cn',
'account_sid' => 'objectsid', 'account_sid' => 'objectsid',
'account_guid' => 'objectguid', 'account_guid' => 'objectguid',
'account_uuid' => 'entryuuid',
'account_dn' => 'dn',
); );
$ret = false; $ret = false;
if (isset($to_ldap[$which])) if (isset($to_ldap[$which]))

View File

@ -325,7 +325,8 @@ class Import
$hide_binary = ['jpegphoto' => $contact['jpegphoto'] ? bytes($contact['jpegphoto']).' bytes binary data' : null]; $hide_binary = ['jpegphoto' => $contact['jpegphoto'] ? bytes($contact['jpegphoto']).' bytes binary data' : null];
$this->logger(++$num.'. User: '.json_encode($hide_binary + $contact + $account, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE), 'debug'); $this->logger(++$num.'. User: '.json_encode($hide_binary + $contact + $account, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE), 'debug');
// check if account exists in sql // check if account exists in sql
if (!($account_id = $this->accounts_sql->name2id($account['account_lid']))) if (!($account_id = $this->accounts_sql->name2id($account['account_uuid'], 'account_uuid')) &&
!($account_id = $this->accounts_sql->name2id($account['account_lid'])))
{ {
$sql_account = $account; $sql_account = $account;
// if only users are imported set primary group as configured // if only users are imported set primary group as configured
@ -702,10 +703,14 @@ class Import
$local_groups = in_array('local', explode('+', $GLOBALS['egw_info']['server']['account_import_type'])); $local_groups = in_array('local', explode('+', $GLOBALS['egw_info']['server']['account_import_type']));
// query all groups in SQL // query all groups in SQL
$sql_groups = $groups = $set_members = []; $sql_groups = $uuid2account_id = $groups = $set_members = [];
foreach($GLOBALS['egw']->db->select(Sql::TABLE, 'account_id,account_lid', ['account_type' => 'g'], __LINE__, __FILE__) as $row) foreach($GLOBALS['egw']->db->select(Sql::TABLE, 'account_id,account_lid,account_uuid', ['account_type' => 'g'], __LINE__, __FILE__) as $row)
{ {
$sql_groups[-$row['account_id']] = self::strtolower($row['account_lid']); $sql_groups[-$row['account_id']] = self::strtolower($row['account_lid']);
if (!empty($row['account_uuid']))
{
$uuid2account_id[$row['account_uuid']] = -$row['account_id'];
}
} }
// fill groups with existing ones, for incremental sync, but only if we have NO local groups, otherwise we need to query them all // fill groups with existing ones, for incremental sync, but only if we have NO local groups, otherwise we need to query them all
$filter = ['type' => 'groups']; $filter = ['type' => 'groups'];
@ -733,7 +738,13 @@ class Import
} }
$this->logger(++$num.'. Group: '.json_encode($group, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), 'debug'); $this->logger(++$num.'. Group: '.json_encode($group, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), 'debug');
if (!($sql_id = array_search(self::strtolower($group['account_lid']), $sql_groups))) $sql_id = null;
// found group by uuid
if ($uuid2account_id && ($sql_id = $uuid2account_id[$group['account_uuid']] ?? false))
{
}
if (!$sql_id && !($sql_id = array_search(self::strtolower($group['account_lid']), $sql_groups)))
{ {
if ($this->accounts_sql->name2id($group['account_lid']) > 0) if ($this->accounts_sql->name2id($group['account_lid']) > 0)
{ {
@ -754,7 +765,7 @@ class Import
} }
if (($sql_id = $group['account_id'] = $this->accounts_sql->save($group, true)) < 0) if (($sql_id = $group['account_id'] = $this->accounts_sql->save($group, true)) < 0)
{ {
// run addgroup hook to create eg. home-directory or mail account // run addgroup hook to create e.g. home-directory or mail account
Api\Hooks::process($group+array( Api\Hooks::process($group+array(
'location' => 'addgroup' 'location' => 'addgroup'
),False,True); // called for every app now, not only enabled ones) ),False,True); // called for every app now, not only enabled ones)

View File

@ -535,7 +535,7 @@ class Ldap
} }
} }
if (!($data = $this->_ldap_search($this->group_context,'(&(objectClass=posixGroup)(gidnumber=' . abs($account_id).'))', if (!($data = $this->_ldap_search($this->group_context,'(&(objectClass=posixGroup)(gidnumber=' . abs($account_id).'))',
array('dn', 'gidnumber', 'cn', 'objectclass', static::MAIL_ATTR, 'memberuid', 'description')))) array('dn', 'gidnumber', 'cn', 'objectclass', static::MAIL_ATTR, 'memberuid', 'description', 'entryuuid'))))
{ {
return false; // group not found return false; // group not found
} }
@ -543,6 +543,7 @@ class Ldap
$group += array( $group += array(
'account_dn' => $data['dn'], 'account_dn' => $data['dn'],
'account_uuid' => $data['entryuuid'][0],
'account_id' => -$data['gidnumber'][0], 'account_id' => -$data['gidnumber'][0],
'account_lid' => $data['cn'][0], 'account_lid' => $data['cn'][0],
'account_type' => 'g', 'account_type' => 'g',
@ -585,7 +586,7 @@ class Ldap
if (!($data = $this->_ldap_search($this->user_context, '(&(objectclass=posixAccount)(uidnumber=' . (int)$account_id.")$account_filter)", if (!($data = $this->_ldap_search($this->user_context, '(&(objectclass=posixAccount)(uidnumber=' . (int)$account_id.")$account_filter)",
array('dn','uidnumber','uid','gidnumber','givenname','sn','cn',static::MAIL_ATTR,'userpassword','telephonenumber', array('dn','uidnumber','uid','gidnumber','givenname','sn','cn',static::MAIL_ATTR,'userpassword','telephonenumber',
'shadowexpire','shadowlastchange','homedirectory','loginshell','createtimestamp','modifytimestamp')))) 'shadowexpire','shadowlastchange','homedirectory','loginshell','createtimestamp','modifytimestamp','entryuuid'))))
{ {
return false; // user not found return false; // user not found
} }
@ -593,6 +594,7 @@ class Ldap
$utc_diff = date('Z'); $utc_diff = date('Z');
$user = array( $user = array(
'account_dn' => $data['dn'], 'account_dn' => $data['dn'],
'account_uuid' => $data['entryuuid'][0],
'account_id' => (int)$data['uidnumber'][0], 'account_id' => (int)$data['uidnumber'][0],
'account_lid' => $data['uid'][0], 'account_lid' => $data['uid'][0],
'account_type' => 'u', 'account_type' => 'u',
@ -893,7 +895,7 @@ class Ldap
{ {
$allValues = $this->vlvSortQuery($this->user_context, $filter, $allValues = $this->vlvSortQuery($this->user_context, $filter,
['uid', 'uidNumber', 'givenname', 'sn', static::MAIL_ATTR, 'shadowExpire', 'createtimestamp', ['uid', 'uidNumber', 'givenname', 'sn', static::MAIL_ATTR, 'shadowExpire', 'createtimestamp',
'modifytimestamp', 'objectclass', 'gidNumber', 'jpegphoto'], 'modifytimestamp', 'objectclass', 'gidNumber', 'jpegphoto', 'entryuuid'],
$order_by, $start, $offset, $totalcount); $order_by, $start, $offset, $totalcount);
$utc_diff = date('Z'); $utc_diff = date('Z');
@ -904,6 +906,7 @@ class Ldap
{ {
$account = array( $account = array(
'account_dn' => $allVals['dn'], 'account_dn' => $allVals['dn'],
'account_uuid' => $allVals['entryuuid'][0],
'account_id' => $allVals['uidnumber'][0], 'account_id' => $allVals['uidnumber'][0],
'account_lid' => Api\Translation::convert($allVals['uid'][0], 'utf-8'), 'account_lid' => Api\Translation::convert($allVals['uid'][0], 'utf-8'),
'account_type' => 'u', 'account_type' => 'u',
@ -963,7 +966,7 @@ class Ldap
$filter .= "(modifytimestamp>=".gmdate('YmdHis', $param['modified']).".0Z)"; $filter .= "(modifytimestamp>=".gmdate('YmdHis', $param['modified']).".0Z)";
} }
$filter .= ')'; $filter .= ')';
$allValues = $this->vlvSortQuery($this->group_context, $filter, ['cn', 'gidNumber'], $order_by, $start, $offset, $group_total); $allValues = $this->vlvSortQuery($this->group_context, $filter, ['cn', 'gidNumber', 'entryuuid'], $order_by, $start, $offset, $group_total);
$totalcount += $group_total; $totalcount += $group_total;
foreach($allValues ?: [] as $allVals) foreach($allValues ?: [] as $allVals)
{ {
@ -972,6 +975,7 @@ class Ldap
{ {
$accounts[(string)-$allVals['gidnumber'][0]] = Array( $accounts[(string)-$allVals['gidnumber'][0]] = Array(
'account_dn' => $allVals['dn'], 'account_dn' => $allVals['dn'],
'account_uuid' => $allVals['entryuuid'][0],
'account_id' => -$allVals['gidnumber'][0], 'account_id' => -$allVals['gidnumber'][0],
'account_lid' => Api\Translation::convert($allVals['cn'][0],'utf-8'), 'account_lid' => Api\Translation::convert($allVals['cn'][0],'utf-8'),
'account_type' => 'g', 'account_type' => 'g',
@ -1085,32 +1089,38 @@ class Ldap
{ {
$name = Api\Ldap::quote(Api\Translation::convert($_name,Api\Translation::charset(),'utf-8')); $name = Api\Ldap::quote(Api\Translation::convert($_name,Api\Translation::charset(),'utf-8'));
if (in_array($which, array('account_lid','account_email')) && $account_type !== 'u') // groups only support account_(lid|email) $to_ldap = [
'account_lid' => 'cn',
'account_email' => static::MAIL_ATTR,
'account_uuid' => 'entryuuid',
'account_dn' => 'dn',
];
if (isset($to_ldap[$which]) && $account_type !== 'u') // groups only support account_(lid|email)
{ {
$attr = $which == 'account_lid' ? 'cn' : static::MAIL_ATTR; $attr = $which == 'account_lid' ? 'cn' : static::MAIL_ATTR;
if (($sri = ldap_search($this->ds, $this->group_context, '(&('.$attr.'=' . $name . ")(objectclass=posixgroup)$this->group_filter)", array('gidNumber'))) && if (($data = ldap_search($this->group_context, '(&('.$attr.'=' . $name . ")(objectclass=posixgroup)$this->group_filter)", ['gidNumber'])) &&
($allValues = ldap_get_entries($this->ds, $sri)) && !empty($data['gidnumber'][0]))
!empty($allValues[0]['gidnumber'][0]))
{ {
return -$allValues[0]['gidnumber'][0]; return -$data['gidnumber'][0];
} }
} }
$to_ldap = array( $to_ldap = [
'account_lid' => 'uid', 'account_lid' => 'uid',
'account_email' => static::MAIL_ATTR, 'account_email' => static::MAIL_ATTR,
'account_fullname' => 'cn', 'account_fullname' => 'cn',
); 'account_uuid' => 'entryuuid',
'account_dn' => 'dn',
];
if (!isset($to_ldap[$which]) || $account_type === 'g') if (!isset($to_ldap[$which]) || $account_type === 'g')
{ {
return False; return False;
} }
if (($sri = ldap_search($this->ds, $this->user_context, '(&('.$to_ldap[$which].'=' . $name . ')(objectclass=posixaccount))', array('uidNumber'))) && if (($data = $this->_ldap_search($this->user_context, '(&('.$to_ldap[$which].'=' . $name . ')(objectclass=posixaccount))', array('uidNumber'))) &&
($allValues = ldap_get_entries($this->ds, $sri)) && !empty($data['uidNumber'][0]))
!empty($allValues[0]['uidnumber'][0]))
{ {
return (int)$allValues[0]['uidnumber'][0]; return (int)$data['uidnumber'][0];
} }
return False; return False;
} }
@ -1181,15 +1191,14 @@ class Ldap
$gid = abs($_gid); // our gid is negative! $gid = abs($_gid); // our gid is negative!
$sri = ldap_search($this->ds,$this->group_context,"(&(objectClass=posixGroup)(gidnumber=$gid))",array('memberuid')); $group = $this->_ldap_search($this->group_context,"(&(objectClass=posixGroup)(gidnumber=$gid))", ['memberuid']);
$group = ldap_get_entries($this->ds, $sri);
$members = array(); $members = array();
if (isset($group[0]['memberuid'])) if ($group && isset($group['memberuid']))
{ {
unset($group[0]['memberuid']['count']); unset($group['memberuid']['count']);
foreach($group[0]['memberuid'] as $lid) foreach($group['memberuid'] as $lid)
{ {
if (($id = $this->name2id($lid, 'account_lid'))) // also return groups! if (($id = $this->name2id($lid, 'account_lid'))) // also return groups!
{ {