* ActiveDirectory/LDAP: implemented deleting of accounts via full/initial import, periodic import does NOT delete

This commit is contained in:
ralf 2022-07-06 17:08:44 +02:00
parent 489eb615ac
commit 6fb464d90b
7 changed files with 327 additions and 140 deletions

View File

@ -16,6 +16,9 @@
namespace EGroupware\Api; namespace EGroupware\Api;
use EGroupware\Api\Accounts\Sql;
use EGroupware\Api\Exception\AssertionFailed;
/** /**
* API - accounts * API - accounts
* *
@ -132,8 +135,9 @@ class Accounts
* Constructor * Constructor
* *
* @param string|array $backend =null string with backend 'sql'|'ldap', or whole config array, default read from global egw_info * @param string|array $backend =null string with backend 'sql'|'ldap', or whole config array, default read from global egw_info
* @param Sql|Ldap|Ads|Univention|null $backend_object
*/ */
public function __construct($backend=null) public function __construct($backend=null, $backend_object=null)
{ {
if (is_array($backend)) if (is_array($backend))
{ {
@ -165,7 +169,11 @@ class Accounts
} }
$backend_class = 'EGroupware\\Api\\Accounts\\'.ucfirst($backend); $backend_class = 'EGroupware\\Api\\Accounts\\'.ucfirst($backend);
$this->backend = new $backend_class($this); if ($backend_object && !is_a($backend_object, $backend_class))
{
throw new AssertionFailed("Invalid backend object, not a $backend_class object!");
}
$this->backend = $backend_object ?: new $backend_class($this);
} }
/** /**

View File

@ -15,11 +15,32 @@ use EGroupware\Api;
/** /**
* Account import from LDAP (incl. ADS) to SQL * Account import from LDAP (incl. ADS) to SQL
* *
* @todo check that ADS and LDAP update modification time of account, if memberships change * @todo test with LDAP eg. update modification time of account, if memberships change
* @todo support changed account_lid of users e.g. be checking UID (not possible for groups)
*/ */
class Import class Import
{ {
public function __construct() /** @var Api\Accounts */
protected $frontend_sql;
/** @var Api\Contacts\Ldap|Api\Contacts\Ldap */
protected $contacts;
/** @var Api\Contacts\Sql */
protected $contacts_sql;
/** @var Api\Contacts */
protected $contacts_sql_frontend;
/** @var Ldap|Ads */
protected $accounts;
/** @var Sql */
protected $accounts_sql;
/** @var callable */
protected $_logger;
/**
* Constructor
*
* @param callable|null $logger function($str, $level) level: "debug", "detail", "info", "error" or "fatal"
*/
public function __construct(callable $logger=null)
{ {
// if we run from setup, we need to take care of loading db and egw_info/server // if we run from setup, we need to take care of loading db and egw_info/server
if (isset($GLOBALS['egw_setup'])) if (isset($GLOBALS['egw_setup']))
@ -30,23 +51,55 @@ class Import
} }
$GLOBALS['egw_info']['server'] += Api\Config::read('phpgwapi'); $GLOBALS['egw_info']['server'] += Api\Config::read('phpgwapi');
} }
if (!in_array($source = $GLOBALS['egw_info']['server']['account_import_source'], ['ldap', 'ads']))
{
throw new \InvalidArgumentException("Invalid account_import_source='{$GLOBALS['egw_info']['server']['account_import_source']}'!");
}
$class = 'EGroupware\\Api\\Contacts\\'.ucfirst($source);
/** @var Api\Contacts\Ldap $contacts */
$this->contacts = new $class($GLOBALS['egw_info']['server']);
$this->contacts_sql = new Api\Contacts\Sql();
// instantiate contacts object with SQL backend
$this->contacts_sql_frontend = new Api\Contacts();
$this->contacts_sql_frontend->somain = $this->contacts_sql_frontend->so_accounts = $this->contacts_sql;
$class = 'EGroupware\\Api\\Accounts\\'.ucfirst($source);
/** @var Api\Accounts\Ldap $accounts */
$this->accounts = new $class($frontend = new Api\Accounts(['account_repository' => $source]+$GLOBALS['egw_info']['server']));
// instantiate accounts backend and frontend for SQL
$this->accounts_sql = new Api\Accounts\Sql();
$this->frontend_sql = new Api\Accounts(['account_repository' => 'sql']+$GLOBALS['egw_info']['server'], $this->accounts_sql);
$this->accounts_sql->setFrontend($this->frontend_sql);
$this->accounts_sql->setContacts($this->contacts_sql_frontend);
$this->_logger = $logger;
}
/**
* @param string $message
* @param string $level log-level: "debug", "detail", "info", "error" or "fatal"
* @return void
*/
protected function logger(string $message, string $level)
{
if ($this->_logger)
{
call_user_func($this->_logger, $message, $level);
}
} }
/** /**
* @param bool $initial_import true: initial sync, false: incremental sync * @param bool $initial_import true: initial sync, false: incremental sync
* @param callable|null $logger function($str, $level) level: "debug", "detail", "info", "error" or "fatal"
* @return array with int values for keys 'created', 'updated', 'uptodate', 'errors' and string 'result' * @return array with int values for keys 'created', 'updated', 'uptodate', 'errors' and string 'result'
* @throws \Exception also gets logged as level "fatal" * @throws \Exception also gets logged as level "fatal"
* @throws \InvalidArgumentException if not correctly configured * @throws \InvalidArgumentException if not correctly configured
*/ */
public function run(bool $initial_import=true, callable $logger=null) public function run(bool $initial_import=true)
{ {
try { try {
if (!isset($logger))
{
$logger = static function($str, $level){};
}
// determine from where we migrate to what // determine from where we migrate to what
if (!in_array($source = $GLOBALS['egw_info']['server']['account_import_source'], ['ldap', 'ads'])) if (!in_array($source = $GLOBALS['egw_info']['server']['account_import_source'], ['ldap', 'ads']))
{ {
@ -65,23 +118,37 @@ class Import
throw new \InvalidArgumentException(lang("You need to run the inital import first!")); throw new \InvalidArgumentException(lang("You need to run the inital import first!"));
} }
$class = 'EGroupware\\Api\\Contacts\\'.ucfirst($source);
/** @var Api\Contacts\Ldap $contacts */
$contacts = new $class($GLOBALS['egw_info']['server']);
$contacts_sql = new Api\Contacts\Sql();
$class = 'EGroupware\\Api\\Accounts\\'.ucfirst($source);
/** @var Api\Accounts\Ldap $accounts */
$accounts = new $class(new Api\Accounts(['account_repository' => $source]+$GLOBALS['egw_info']['server']));
$accounts_sql = new Api\Accounts\Sql(new Api\Accounts(['account_repository' => 'sql']+$GLOBALS['egw_info']['server']));
Api\Accounts::cache_invalidate(); // to not get any cached data eg. from the wrong backend Api\Accounts::cache_invalidate(); // to not get any cached data eg. from the wrong backend
// deleting accounts currently only works with (manual) initial import
if (!$initial_import && $delete !== 'no')
{
$delete = 'no';
}
$created = $updated = $uptodate = $errors = $deleted = 0; $created = $updated = $uptodate = $errors = $deleted = 0;
if (in_array('groups', explode('+', $type))) if (in_array('groups', explode('+', $type)))
{ {
[$created, $updated, $uptodate, $errors, $deleted] = $this->groups( foreach($this->groups($initial_import ? null : $GLOBALS['egw_info']['server']['account_import_lastrun'],
$initial_import ? null : $GLOBALS['egw_info']['server']['account_import_lastrun'], $delete, $groups, $set_members) as $name => $val)
$accounts, $accounts_sql, $logger, $delete, $groups, $set_members); {
$$name += $val;
}
}
// query all groups in SQL
$sql_users = [];
if ($delete !== 'no')
{
$where = ['account_type' => 'u'];
if ($delete === 'deactivate')
{
$where['account_status'] = 'A'; // no need to deactivate already deactivated users
}
foreach($GLOBALS['egw']->db->select(Sql::TABLE, 'account_id,account_lid', $where, __LINE__, __FILE__) as $row)
{
$sql_users[$row['account_id']] = $row['account_lid'];
}
} }
$filter = [ $filter = [
@ -97,37 +164,37 @@ class Import
$start = ['', 5, &$cookie]; // cookie must be a reference! $start = ['', 5, &$cookie]; // cookie must be a reference!
do do
{ {
foreach ($contacts->search('', false, '', 'account_lid', '', '', 'AND', $start, $filter) as $contact) foreach ($this->contacts->search('', false, '', 'account_lid', '', '', 'AND', $start, $filter) as $contact)
{ {
$new = null; $new = null;
if (!isset($last_modified) || (int)$last_modified < (int)$contact['modified']) if (!isset($last_modified) || (int)$last_modified < (int)$contact['modified'])
{ {
$last_modified = $contact['modified']; $last_modified = $contact['modified'];
} }
$account = $accounts->read($contact['account_id']); $account = $this->accounts->read($contact['account_id']);
$logger(json_encode($contact + $account), 'debug'); $this->logger(json_encode($contact + $account), 'debug');
// check if account exists in sql // check if account exists in sql
if (!($account_id = $accounts_sql->name2id($account['account_lid']))) if (!($account_id = $this->accounts_sql->name2id($account['account_lid'])))
{ {
$sql_account = $account; $sql_account = $account;
// check if account_id is not yet taken by another user or group --> unset it to let DB assign a new one // check if account_id is not yet taken by another user or group --> unset it to let DB assign a new one
if ($accounts_sql->read($account['account_id'])) if ($this->accounts_sql->read($account['account_id']))
{ {
unset($sql_account['account_id']); unset($sql_account['account_id']);
} }
if (($account_id = $sql_account['account_id'] = $accounts_sql->save($sql_account, true)) > 0) if (($account_id = $sql_account['account_id'] = $this->accounts_sql->save($sql_account, true)) > 0)
{ {
// run addaccount hook to create eg. home-directory or mail account // run addaccount hook to create eg. home-directory or mail account
Api\Hooks::process($sql_account+array( Api\Hooks::process($sql_account+array(
'location' => 'addaccount' 'location' => 'addaccount'
),False,True); // called for every app now, not only enabled ones) ),False,True); // called for every app now, not only enabled ones)
$logger("Successful created user '$account[account_lid]' (#$account[account_id]". $this->logger("Successful created user '$account[account_lid]' (#$account[account_id]".
($account['account_id'] != $account_id ? " as #$account_id" : '').')', 'detail'); ($account['account_id'] != $account_id ? " as #$account_id" : '').')', 'detail');
} }
else else
{ {
$logger("Error creaing user '$account[account_lid]' (#$account[account_id])", 'error'); $this->logger("Error creaing user '$account[account_lid]' (#$account[account_id])", 'error');
$errors++; $errors++;
continue; continue;
} }
@ -136,7 +203,7 @@ class Import
{ {
throw new \Exception("User '$account[account_lid]' already exists as group!"); throw new \Exception("User '$account[account_lid]' already exists as group!");
} }
elseif (!($sql_account = $accounts_sql->read($account_id))) elseif (!($sql_account = $this->accounts_sql->read($account_id)))
{ {
throw new \Exception("User '$account[account_lid]' (#$account_id) should exist, but not found!"); throw new \Exception("User '$account[account_lid]' (#$account_id) should exist, but not found!");
} }
@ -155,42 +222,42 @@ class Import
} }
if (($diff = array_diff_assoc($to_update, $sql_account))) if (($diff = array_diff_assoc($to_update, $sql_account)))
{ {
if ($accounts_sql->save($to_update) > 0) if ($this->accounts_sql->save($to_update) > 0)
{ {
// run editaccount hook to create eg. home-directory or mail account // run editaccount hook to create eg. home-directory or mail account
Api\Hooks::process($to_update+array( Api\Hooks::process($to_update+array(
'location' => 'editaccount' 'location' => 'editaccount'
),False,True); // called for every app now, not only enabled ones) ),False,True); // called for every app now, not only enabled ones)
$logger("Successful updated user '$account[account_lid]' (#$account_id): " . $this->logger("Successful updated user '$account[account_lid]' (#$account_id): " .
json_encode($diff, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'detail'); json_encode($diff, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'detail');
if (!$new) $new = false; if (!$new) $new = false;
} }
else else
{ {
$logger("Error updating user '$account[account_lid]' (#$account_id)", 'error'); $this->logger("Error updating user '$account[account_lid]' (#$account_id)", 'error');
$errors++; $errors++;
continue; continue;
} }
} }
else else
{ {
$logger("User '$account[account_lid]' (#$account_id) already up to date", 'debug'); $this->logger("User '$account[account_lid]' (#$account_id) already up to date", 'debug');
} }
} }
if (!($sql_contact = $contacts_sql->read(['account_id' => $account_id]))) if (!($sql_contact = $this->contacts_sql->read(['account_id' => $account_id])))
{ {
$sql_contact = $contact; $sql_contact = $contact;
unset($sql_contact['id']); // LDAP contact-id is the UID! unset($sql_contact['id']); // LDAP contact-id is the UID!
if (!$contacts_sql->save($sql_contact)) if (!$this->contacts_sql->save($sql_contact))
{ {
$sql_contact['id'] = $contacts_sql->data['id']; $sql_contact['id'] = $this->contacts_sql->data['id'];
$logger("Successful created contact for user '$account[account_lid]' (#$account_id)", 'detail'); $this->logger("Successful created contact for user '$account[account_lid]' (#$account_id)", 'detail');
$new = true; $new = true;
} }
else else
{ {
$logger("Error creating contact for user '$account[account_lid]' (#$account_id)", 'error'); $this->logger("Error creating contact for user '$account[account_lid]' (#$account_id)", 'error');
$errors++; $errors++;
continue; continue;
} }
@ -203,29 +270,29 @@ class Import
$to_update['id'] = $sql_contact['id']; $to_update['id'] = $sql_contact['id'];
if (($diff = array_diff_assoc($to_update, $sql_contact))) if (($diff = array_diff_assoc($to_update, $sql_contact)))
{ {
if ($contacts_sql->save($to_update) === 0) if ($this->contacts_sql->save($to_update) === 0)
{ {
$logger("Successful updated contact data of '$account[account_lid]' (#$account_id): ". $this->logger("Successful updated contact data of '$account[account_lid]' (#$account_id): ".
json_encode($diff, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE), 'detail'); json_encode($diff, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE), 'detail');
if (!$new) $new = false; if (!$new) $new = false;
} }
else else
{ {
$logger("Error updating contact data of '$account[account_lid]' (#$account_id)", 'error'); $this->logger("Error updating contact data of '$account[account_lid]' (#$account_id)", 'error');
++$errors; ++$errors;
continue; continue;
} }
} }
else else
{ {
$logger("Contact data of '$account[account_lid]' (#$account_id) already up to date", 'debug'); $this->logger("Contact data of '$account[account_lid]' (#$account_id) already up to date", 'debug');
} }
} }
// if requested, also set memberships // if requested, also set memberships
if ($type === 'users+groups') if ($type === 'users+groups')
{ {
// we need to convert the account_id's of memberships, in case we use different ones in SQL // we need to convert the account_id's of memberships, in case we use different ones in SQL
$accounts_sql->set_memberships(array_filter(array_map(static function($account_lid) use ($groups) $this->accounts_sql->set_memberships(array_filter(array_map(static function($account_lid) use ($groups)
{ {
return array_search($account_lid, $groups); return array_search($account_lid, $groups);
}, $account['memberships'])), $account_id); }, $account['memberships'])), $account_id);
@ -242,13 +309,47 @@ class Import
{ {
++$uptodate; ++$uptodate;
} }
// remember the users we imported, to be able to delete the ones we dont
unset($sql_users[$account_id]);
} }
} }
while ($start[2] !== ''); while ($start[2] !== '');
if ($set_members) if ($set_members)
{ {
$this->setMembers($set_members, $accounts, $accounts_sql, $logger); foreach($this->setMembers($set_members) as $name => $num)
{
$$name += $num;
}
}
// do we need to delete (or deactivate) no longer existing users
if ($delete !== 'no' && $sql_users)
{
if ($delete === 'deactivate')
{
$GLOBALS['egw']->db->update(Sql::TABLE, ['account_status' => null], ['account_id' => array_keys($sql_users)], __LINE__, __FILE__);
$num = count($sql_users);
$this->logger("Deactivated $num no longer existing user(s): ".implode(', ', array_map(static function ($account_id, $account_lid)
{
return $account_lid.' (#'.$account_id.')';
}, array_keys($sql_users), $sql_users)), 'detail');
$deleted += count($sql_users);
}
else
{
foreach($sql_users as $account_id => $account_lid)
{
if ($this->deleteAccount($account_id, $account_lid, $this->logger))
{
$deleted++;
}
else
{
$errors++;
}
}
}
} }
$last_run = max($start_import-1, $last_modified); $last_run = max($start_import-1, $last_modified);
@ -256,27 +357,31 @@ class Import
$str = gmdate('Y-m-d H:i:s', $last_run). ' UTC'; $str = gmdate('Y-m-d H:i:s', $last_run). ' UTC';
if (!$errors) if (!$errors)
{ {
$logger("Setting new incremental import time to: $str ($last_run)", 'detail'); $this->logger("Setting new incremental import time to: $str ($last_run)", 'detail');
} }
if ($created || $updated || $errors || $deleted) if ($created || $updated || $errors || $deleted)
{ {
$result = "Created $created, updated $updated and deleted $deleted accounts, with $errors errors."; $result = "Created $created, updated $updated and deleted $deleted account(s), with $errors error(s).";
} }
else else
{ {
$result = "All accounts are up-to-date."; $result = "All accounts are up-to-date.";
} }
$logger($result, 'info'); $this->logger($result, 'info');
if ($initial_import && self::installAsyncJob()) if ($initial_import && self::installAsyncJob())
{ {
$logger('Async job for periodic import installed', 'info'); $this->logger('Async job for periodic import installed', 'info');
} }
} }
catch(\Exception $e) { catch(\Exception $e) {
$logger($e->getMessage(), 'fatal'); $this->logger($e->getMessage(), 'fatal');
$GLOBALS['egw']->accounts = $frontend;
throw $e; throw $e;
} }
// restore regular frontend
$GLOBALS['egw']->accounts = $frontend;
return [ return [
'created' => $created, 'created' => $created,
'updated' => $updated, 'updated' => $updated,
@ -296,23 +401,16 @@ class Import
* We can only delete no longer existing groups, if we query all groups! * We can only delete no longer existing groups, if we query all groups!
* So $delete !== 'no', requires $modified === null. * So $delete !== 'no', requires $modified === null.
* *
* @param int|null $modified to only query groups modified after given timestamp
* @param Ldap|Ads $accounts
* @param Sql $accounts_sql
* @param callable $logger function($str, $level) level: "debug", "detail", "info", "error" or "fatal"
* @param string $delete what to do with no longer existing groups: "yes": delete incl. data, "deactivate": delete group, "no": do nothing
* @param int|null $modified null: initial import, int: timestamp of last import * @param int|null $modified null: initial import, int: timestamp of last import
* @param string $delete what to do with no longer existing groups: "yes": delete incl. data, "deactivate": delete group, "no": do nothing
* @param array|null &$groups on return all current groups as account_id => account_lid pairs * @param array|null &$groups on return all current groups as account_id => account_lid pairs
* @param array|null &$set_members on return, if modified: (sql)account_id => [(ldap)account_id => account_lid] pairs * @param array|null &$set_members on return, if modified: (sql)account_id => [(ldap)account_id => account_lid] pairs
* @return array with int values [$created, $updated, $uptodate, $errors, $deleted] * @return int[] values for keys "created", "updated", "uptodate", "errors", "deleted"
*/ */
protected function groups(?int $modified, $accounts, Sql $accounts_sql, callable $logger, string $delete, array &$groups=null, array &$set_members=null) protected function groups(?int $modified, string $delete, array &$groups=null, array &$set_members=null)
{ {
// to delete no longer existing groups, we have to query all groups! // to delete no longer existing groups, we have to query all groups!
if ($delete !== 'no') if ($modified) $delete = 'no';
{
$modified = null;
}
// query all groups in SQL // query all groups in SQL
$sql_groups = $groups = $set_members = []; $sql_groups = $groups = $set_members = [];
@ -327,41 +425,41 @@ class Import
} }
$created = $updated = $uptodate = $errors = $deleted = 0; $created = $updated = $uptodate = $errors = $deleted = 0;
foreach($accounts->search(['type' => 'groups', 'modified' => $modified]) as $account_id => $group) foreach($this->accounts->search(['type' => 'groups', 'modified' => $modified]) as $account_id => $group)
{ {
$logger(json_encode($group, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), 'debug'); $this->logger(json_encode($group, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), 'debug');
if (!($sql_id = array_search($group['account_lid'], $sql_groups))) if (!($sql_id = array_search($group['account_lid'], $sql_groups)))
{ {
if ($accounts_sql->name2id($group['account_lid']) > 0) if ($this->accounts_sql->name2id($group['account_lid']) > 0)
{ {
$logger("Group '$group[account_lid]' already exists as user --> skipped!", 'error'); $this->logger("Group '$group[account_lid]' already exists as user --> skipped!", 'error');
$errors++; $errors++;
continue; continue;
} }
// check if the numeric account_id is not yet taken --> unset account_id and let DB create a new one // check if the numeric account_id is not yet taken --> unset account_id and let DB create a new one
if ($accounts_sql->read($account_id)) if ($this->accounts_sql->read($account_id))
{ {
unset($group['account_id']); unset($group['account_id']);
} }
if (($sql_id = $group['account_id'] = $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 eg. 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)
$logger("Successful created group '$group[account_lid]' (#$account_id".($sql_id != $account_id ? " as #$sql_id" : '').')', 'detail'); $this->logger("Successful created group '$group[account_lid]' (#$account_id".($sql_id != $account_id ? " as #$sql_id" : '').')', 'detail');
$created++; $created++;
} }
else else
{ {
$logger("Error creating group '$group[account_lid]' (#$account_id)", 'error'); $this->logger("Error creating group '$group[account_lid]' (#$account_id)", 'error');
$errors++; $errors++;
continue; continue;
} }
} }
elseif (!($sql_group = $accounts_sql->read($sql_id))) elseif (!($sql_group = $this->accounts_sql->read($sql_id)))
{ {
throw new \Exception("Group '$group[account_lid]' (#$sql_id) should exist, but not found!"); throw new \Exception("Group '$group[account_lid]' (#$sql_id) should exist, but not found!");
} }
@ -376,26 +474,27 @@ class Import
$to_update = $relevant + $sql_group; $to_update = $relevant + $sql_group;
if (($diff = array_diff_assoc($to_update, $sql_group))) if (($diff = array_diff_assoc($to_update, $sql_group)))
{ {
if ($accounts_sql->save($group, true) > 0) if ($this->accounts_sql->save($to_update) < 0)
{ {
Api\Hooks::process($group+array( Api\Hooks::process($to_update+array(
'location' => 'editgroup' 'location' => 'editgroup',
'old_name' => $sql_group['account_lid'],
),False,True); // called for every app now, not only enabled ones) ),False,True); // called for every app now, not only enabled ones)
$logger("Successful updated group '$group[account_lid]' (#$sql_id): " . $this->logger("Successful updated group '$group[account_lid]' (#$sql_id): " .
json_encode($diff, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), 'detail'); json_encode($diff, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), 'detail');
$updated++; $updated++;
} }
else else
{ {
$logger("Error updating group '$group[account_lid]' (#$sql_id)", 'error'); $this->logger("Error updating group '$group[account_lid]' (#$sql_id)", 'error');
$errors++; $errors++;
continue; continue;
} }
} }
else else
{ {
$logger("Group '$group[account_lid]' (#$sql_id) already up to date", 'debug'); $this->logger("Group '$group[account_lid]' (#$sql_id) already up to date", 'debug');
$uptodate++; $uptodate++;
} }
// unset the updated groups, so we can delete the ones not returned from LDAP // unset the updated groups, so we can delete the ones not returned from LDAP
@ -406,7 +505,7 @@ class Import
// if we only get modified groups, we need to record and return the id's to update members, AFTER users are created/updated // if we only get modified groups, we need to record and return the id's to update members, AFTER users are created/updated
if ($modified) if ($modified)
{ {
$set_members[$sql_id] = $accounts->read($group['account_id'])['members']; $set_members[$sql_id] = $this->accounts->read($group['account_id'])['members'];
} }
} }
@ -416,30 +515,52 @@ class Import
static $acl=null; static $acl=null;
if ($delete === 'yes') if ($delete === 'yes')
{ {
try { if ($this->deleteAccount($account_id, $account_lid, $this->logger))
$cmd = new \admin_cmd_delete_account($account_id); {
$cmd->run(); $deleted++;
} }
catch (\Exception $e) { else
$logger("Error deleting no longer existing group '$account_lid' (#$account_id).", 'error'); {
$errors++; $errors++;
} }
} }
// still run the SQL commands, as an LDAP/ADS system will not run them
if ($accounts_sql->delete($account_id))
{
if (!isset($acl)) $acl = new Api\Acl();
$acl->delete_account($account_id);
$logger("Successful deleted no longer existing group '$account_lid' (#$account_id).", 'detail');
$deleted++;
}
elseif (!isset($e))
{
$logger("Error deleting no longer existing group '$account_lid' (#$account_id).", 'error');
$errors++;
}
} }
return [$created, $updated, $uptodate, $errors, $deleted]; return [
'created' => $created,
'updated' => $updated,
'uptodate' => $uptodate,
'errors' => $errors,
'deleted' => $deleted,
];
}
/**
* Delete an account from SQL including its data
*
* We use admin_cmd_delete_account to also log that the account was deleted.
*
* @param int $account_id
* @param string $account_lid
* @return bool
*/
protected function deleteAccount(int $account_id, string $account_lid)
{
// make sure admin_cmd_delete_account uses the SQL accounts object, to not delete in the source, but in EGroupware DB!
$backup_accounts = $GLOBALS['egw']->accounts;
$GLOBALS['egw']->accounts = $this->frontend_sql;
$type = $account_id < 0 ? 'group' : 'user';
try {
$cmd = new \admin_cmd_delete_account($account_id, null, $account_id > 0);
$this->logger("Successful deleted no longer existing $type '$account_lid' (#$account_id): ".$cmd->run(), 'detail');
}
catch (\Exception $e) {
$this->logger("Error deleting no longer existing $type '$account_lid' (#$account_id): ".$e->getMessage(), 'error');
}
$GLOBALS['egw']->accounts = $backup_accounts;
return !isset($e);
} }
/** /**
@ -526,7 +647,6 @@ class Import
public static function async() public static function async()
{ {
try { try {
$import = new self();
$log = $GLOBALS['egw_info']['server']['files_dir'].'/'.self::LOG_FILE; $log = $GLOBALS['egw_info']['server']['files_dir'].'/'.self::LOG_FILE;
if (!file_exists($dir=dirname($log)) && !mkdir($dir) || !is_dir($dir) || if (!file_exists($dir=dirname($log)) && !mkdir($dir) || !is_dir($dir) ||
!($fp = fopen($log, 'a+'))) !($fp = fopen($log, 'a+')))
@ -550,7 +670,8 @@ class Import
}; };
} }
$logger(date('Y-m-d H:i:s O').' LDAP account import started', 'info'); $logger(date('Y-m-d H:i:s O').' LDAP account import started', 'info');
$import->run(false, $logger); $import = new self($logger);
$import->run(false);
$logger(date('Y-m-d H:i:s O').' LDAP account import finished'.(!empty($fp)?"\n":''), 'info'); $logger(date('Y-m-d H:i:s O').' LDAP account import finished'.(!empty($fp)?"\n":''), 'info');
} }
catch (\InvalidArgumentException $e) { catch (\InvalidArgumentException $e) {
@ -587,72 +708,102 @@ class Import
* - members might not be readable from LDAP, because the are not in account-filter * - members might not be readable from LDAP, because the are not in account-filter
* *
* @param array $set_members (sql)account_id => [(ldap)account_id => account_lid] pairs * @param array $set_members (sql)account_id => [(ldap)account_id => account_lid] pairs
* @param Ldap|ADS $accounts * @return int[] values for keys "created", "updated" and "errors"
* @param Sql $accounts_sql
* @param callable $logger
* @return void
*/ */
protected function setMembers(array $set_members, $accounts, Sql $accounts_sql, callable $logger) protected function setMembers(array $set_members)
{ {
// setting (new) members // setting (new) members
$created = $updated = $errors = 0;
foreach($set_members as $sql_group_id => $members) foreach($set_members as $sql_group_id => $members)
{ {
$group = $accounts_sql->id2name($sql_group_id) ?: '#'.$sql_group_id; $group = $this->accounts_sql->id2name($sql_group_id) ?: '#'.$sql_group_id;
foreach($members as $ldap_id => $account_lid) foreach($members as $ldap_id => $account_lid)
{ {
if (!($account_id = $accounts_sql->name2id($account_lid))) if (!($account_id = $this->accounts_sql->name2id($account_lid)))
{ {
if (!($account = $accounts->read($ldap_id))) if (!($account = $this->accounts->read($ldap_id)))
{ {
$logger("Failed reading user '$account_lid' (#$ldap_id) from LDAP, maybe he is not contained in filter --> ignored", 'detail'); $this->logger("Failed reading user '$account_lid' (#$ldap_id) from LDAP, maybe he is not contained in filter --> ignored", 'detail');
continue; continue;
} }
if (!($contact = $this->contacts->read($account['person_id'])))
{
$this->logger("Error reading contact-data of user '$account_lid' (#$ldap_id)", 'error');
$errors++;
continue;
}
$this->logger(json_encode($account + $contact, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), 'debug');
$sql_account = $account; $sql_account = $account;
// check if ldap-id is already taken --> create with own id // check if ldap-id is already taken --> create with own id
if ($accounts_sql->id2name($ldap_id)) if ($this->accounts_sql->id2name($ldap_id))
{ {
unset($sql_account['account_id']); unset($sql_account['account_id']);
} }
if (!($sql_account['account_id'] = $accounts_sql->save($sql_account, true))) if (($sql_account['account_id'] = $this->accounts_sql->save($sql_account, true)))
{ {
$logger("Error creating user '$account_lid' (#$ldap_id)", 'error'); $this->logger("Successful created user '$account_lid' (#$ldap_id)", 'detail');
$created++;
// save contact-data of user
$sql_contact = $contact;
$sql_contact['account_id'] = $sql_account['account_id'];
unset($sql_contact['id']);
if ($this->contacts_sql->save($sql_contact))
{
$this->logger("Error saving contact-data of user '$account_lid' (#$ldap_id)", 'error');
$errors++;
}
}
else
{
$this->logger("Error creating user '$account_lid' (#$ldap_id)", 'error');
$errors++;
continue; continue;
} }
// set memberships using id's in sql (!) // set memberships using id's in sql (!)
$accounts_sql->set_memberships(array_filter(array_map(static function($account_lid) use ($accounts_sql) $this->accounts_sql->set_memberships(array_filter(array_map(function($account_lid)
{ {
return $accounts_sql->name2id($account_lid); return $this->accounts_sql->name2id($account_lid);
}, $account['memberships'])), $sql_account['account_id']); }, $account['memberships'])), $sql_account['account_id']);
} }
else else
{ {
if (!($memberships = $accounts_sql->memberships($account_id))) if (!($memberships = $this->accounts_sql->memberships($account_id)))
{ {
$logger("Error reading memberships of (existing) user '$account_lid' (#$account_id)!", 'error'); $this->logger("Error reading memberships of (existing) user '$account_lid' (#$account_id)!", 'error');
$errors++;
continue; continue;
} }
if (!isset($memberships[$sql_group_id])) if (!isset($memberships[$sql_group_id]))
{ {
$accounts_sql->set_memberships(array_merge(array_keys($memberships), [$sql_group_id]), $account_id); $this->accounts_sql->set_memberships(array_merge(array_keys($memberships), [$sql_group_id]), $account_id);
$logger("Adding membership of user '$account_lid' (#$account_id) to group '$group' (#$sql_group_id)", 'detail'); $this->logger("Adding membership of user '$account_lid' (#$account_id) to group '$group' (#$sql_group_id)", 'detail');
$updated++;
} }
} }
} }
// removing no longer set members // removing no longer set members
if (($sql_members = $accounts_sql->members($sql_group_id)) && if (($sql_members = $this->accounts_sql->members($sql_group_id)) &&
($removed = array_diff($sql_members, $members))) ($removed = array_diff($sql_members, $members)))
{ {
foreach($removed as $sql_account_id => $sql_account_lid) foreach($removed as $sql_account_id => $sql_account_lid)
{ {
if (($memberships = $accounts_sql->memberships($sql_account_id)) && isset($memberships[$sql_group_id])) if (($memberships = $this->accounts_sql->memberships($sql_account_id)) && isset($memberships[$sql_group_id]))
{ {
unset($memberships[$sql_group_id]); unset($memberships[$sql_group_id]);
$accounts_sql->set_memberships(array_keys($memberships), $sql_account_id); $this->accounts_sql->set_memberships(array_keys($memberships), $sql_account_id);
$logger("Removing membership of user '$sql_account_lid' (#$sql_account_id) from group '$group' (#$sql_group_id)", 'detail'); $this->logger("Removing membership of user '$sql_account_lid' (#$sql_account_id) from group '$group' (#$sql_group_id)", 'detail');
$updated++;
} }
} }
} }
} }
return [
'created' => $created,
'updated' => $updated,
'errors' => $errors,
];
} }
} }

View File

@ -97,7 +97,7 @@ class Sql
* *
* @param Api\Accounts $frontend reference to the frontend class, to be able to call it's methods if needed * @param Api\Accounts $frontend reference to the frontend class, to be able to call it's methods if needed
*/ */
function __construct(Api\Accounts $frontend) function __construct(Api\Accounts $frontend=null)
{ {
$this->frontend = $frontend; $this->frontend = $frontend;
@ -111,6 +111,26 @@ class Sql
} }
} }
/**
* Set used frontend object
*
* @param Api\Accounts $frontend
*/
public function setFrontend(Api\Accounts $frontend)
{
$this->frontend = $frontend;
}
/**
* Set used contacts object
*
* @param Api\Contacts $contacts
*/
public function setContacts(Api\Contacts $contacts)
{
$this->contacts = $contacts;
}
/** /**
* Reads the data of one account * Reads the data of one account
* *
@ -281,14 +301,16 @@ class Sql
{ {
if (!(int)$account_id) return false; if (!(int)$account_id) return false;
$contact_id = $this->id2name($account_id,'person_id');
if (!$this->db->delete($this->table,array('account_id' => abs($account_id)),__LINE__,__FILE__)) if (!$this->db->delete($this->table,array('account_id' => abs($account_id)),__LINE__,__FILE__))
{ {
return false; return false;
} }
if ($account_id > 0 && ($contact_id = $this->id2name($account_id,'person_id'))) if ($account_id > 0 && $contact_id)
{ {
if (!isset($this->contacts)) $this->contacts = new Api\Contacts(); if (!isset($this->contacts)) $this->contacts = new Api\Contacts();
$this->contacts->delete($contact_id,false); // false = allow to delete accounts (!) $this->contacts->delete($contact_id,false, false, true); // false = allow to delete accounts (!)
} }
return true; return true;
} }

View File

@ -836,9 +836,10 @@ class Contacts extends Contacts\Storage
* @param mixed &$contact contact array with key id or (array of) id(s) * @param mixed &$contact contact array with key id or (array of) id(s)
* @param boolean $deny_account_delete =true if true never allow to delete accounts * @param boolean $deny_account_delete =true if true never allow to delete accounts
* @param int $check_etag =null * @param int $check_etag =null
* @return boolean|int true on success or false on failiure, 0 if etag does not match * @param bool $no_permission_check =false true: do not call check_perms and error out on missing rights
* @return boolean|int true on success or false on failure, 0 if etag does not match
*/ */
function delete($contact,$deny_account_delete=true,$check_etag=null) function delete($contact,$deny_account_delete=true,$check_etag=null,$no_permission_check=false)
{ {
if (is_array($contact) && isset($contact['id'])) if (is_array($contact) && isset($contact['id']))
{ {
@ -853,11 +854,11 @@ class Contacts extends Contacts\Storage
$id = is_array($c) ? $c['id'] : $c; $id = is_array($c) ? $c['id'] : $c;
$ok = false; $ok = false;
if ($this->check_perms(Acl::DELETE,$c,$deny_account_delete)) if ($no_permission_check || $this->check_perms(Acl::DELETE,$c,$deny_account_delete))
{ {
if (!($old = $this->read($id))) return false; if (!($old = $this->read($id))) return false;
// already marked as deleted item and accounts are always really deleted // already marked as deleted item and accounts are always really deleted
// we cant mark accounts as deleted, as no such thing exists for accounts! // we can't mark accounts as deleted, as no such thing exists for accounts!
if ($old['owner'] && $old['tid'] !== self::DELETED_TYPE) if ($old['owner'] && $old['tid'] !== self::DELETED_TYPE)
{ {
$delete = $old; $delete = $old;

View File

@ -98,6 +98,10 @@ class Ads extends Ldap
} }
$this->accounts_ads = $GLOBALS['egw']->accounts->backend; $this->accounts_ads = $GLOBALS['egw']->accounts->backend;
if (!is_a($this->accounts_ads, Api\Accounts\Ads::class))
{
throw new Api\Exception\AssertionFailed('$GLOBALS[egw]->accounts->backend is no Api\Accounts\Ads object!');
}
//$this->personalContactsDN = 'ou=personal,ou=contacts,'. $this->ldap_config['ldap_contact_context']; //$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->sharedContactsDN = 'ou=shared,ou=contacts,'. $this->ldap_config['ldap_contact_context'];
$this->allContactsDN = $this->accountContactsDN = $this->accounts_ads->ads_context(); $this->allContactsDN = $this->accountContactsDN = $this->accounts_ads->ads_context();
@ -243,4 +247,4 @@ class Ads extends Ldap
} }
return $filter; return $filter;
} }
} }

View File

@ -27,13 +27,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST' && $_POST)
} }
try { try {
$import = new Api\Accounts\Import(); $import = new Api\Accounts\Import(static function($str, $level)
if (!empty($_GET['log']))
{
$import->showLog();
return;
}
$import->run(!empty($_GET['initial']) && $_GET['initial'] !== 'false', static function($str, $level)
{ {
switch($level) switch($level)
{ {
@ -51,6 +45,12 @@ try {
break; break;
} }
}); });
if (!empty($_GET['log']))
{
$import->showLog();
return;
}
$import->run(!empty($_GET['initial']) && $_GET['initial'] !== 'false');
} }
catch (\Exception $e) { catch (\Exception $e) {
http_response_code(500); http_response_code(500);

View File

@ -480,10 +480,11 @@
<td>{lang_What_to_do_in_EGroupware_if_an_user_get_deleted?}:</td> <td>{lang_What_to_do_in_EGroupware_if_an_user_get_deleted?}:</td>
<td> <td>
<select name="newsettings[account_import_delete]"> <select name="newsettings[account_import_delete]">
<option value="yes" disabled {selected_account_import_delete_yes}>{lang_Delete_user_AND_his_data}</option> <option value="yes" {selected_account_import_delete_yes}>{lang_Delete_user_AND_his_data}</option>
<option value="deactivate" disabled {selected_account_import_delete_deactivate}>{lang_Deactivate_user}</option> <option value="deactivate" {selected_account_import_delete_deactivate}>{lang_Deactivate_user}</option>
<option value="no" {selected_account_import_delete_no}>{lang_Do_NOT_check_for_deleted_user}</option> <option value="no" {selected_account_import_delete_no}>{lang_Do_NOT_check_for_deleted_user}</option>
</select> </select>
<br/><i>Deleting or disabling no longer existing accounts works currently only by using manual inital import!</i>
</td> </td>
</tr> </tr>
<tr class="row_off"> <tr class="row_off">
@ -500,7 +501,7 @@
<td>{lang_You_must_save_AND_run_an_inital_import,_before_the_periodic_import_will_start}:</td> <td>{lang_You_must_save_AND_run_an_inital_import,_before_the_periodic_import_will_start}:</td>
<td> <td>
<button onclick="window.open('account_import.php?initial=true', '_blank')">{lang_Initial_import}</button> <button onclick="window.open('account_import.php?initial=true', '_blank')">{lang_Initial_import}</button>
<button onclick="window.open('account_import.php', '_blank')">{lang_Incremental_import}</button> <button onclick="window.open('account_import.php', '_blank')">{lang_Incremental_import}</button><br/>
{lang_We_strongly_recomment_to_run_a_DB_backup_BEFORE_running_the_import!} {lang_We_strongly_recomment_to_run_a_DB_backup_BEFORE_running_the_import!}
</td> </td>
</tr> </tr>