* Setup: add dry-run option to account import from AD or LDAP

This commit is contained in:
ralf 2022-11-21 10:10:08 +01:00
parent 972acb7aa9
commit a823563281
3 changed files with 80 additions and 27 deletions

View File

@ -145,11 +145,12 @@ class Import
/** /**
* @param bool $initial_import true: initial sync, false: incremental sync * @param bool $initial_import true: initial sync, false: incremental sync
* @param bool $dry_run true: only log what would be done, but do NOT make any changes
* @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) public function run(bool $initial_import=true, bool $dry_run=false)
{ {
try { try {
// determine from where we migrate to what // determine from where we migrate to what
@ -182,7 +183,7 @@ class Import
if (in_array('groups', explode('+', $type))) if (in_array('groups', explode('+', $type)))
{ {
foreach($this->groups($initial_import ? null : $GLOBALS['egw_info']['server']['account_import_lastrun'], foreach($this->groups($initial_import ? null : $GLOBALS['egw_info']['server']['account_import_lastrun'],
$delete, $groups, $set_members) as $name => $val) $delete, $groups, $set_members, $dry_run) as $name => $val)
{ {
$$name += $val; $$name += $val;
} }
@ -234,7 +235,11 @@ class Import
{ {
unset($sql_account['account_id']); unset($sql_account['account_id']);
} }
if (($account_id = $sql_account['account_id'] = $this->accounts_sql->save($sql_account, true)) > 0) if ($dry_run)
{
$this->logger("Dry-run: would created user '$account[account_lid]' (#$account[account_id])", 'detail');
}
elseif (($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(
@ -274,7 +279,12 @@ class Import
} }
if (($diff = array_diff_assoc($to_update, $sql_account))) if (($diff = array_diff_assoc($to_update, $sql_account)))
{ {
if ($this->accounts_sql->save($to_update) > 0) if ($dry_run)
{
$this->logger("Dry-run: would updated user '$account[account_lid]' (#$account_id): " .
json_encode($diff, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'detail');
}
elseif ($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(
@ -297,7 +307,7 @@ class Import
$this->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 = $this->contacts_sql->read(['account_id' => $account_id]))) if (!$dry_run && !($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!
@ -314,7 +324,7 @@ class Import
continue; continue;
} }
} }
else elseif (!$dry_run)
{ {
// photo and public keys are not stored in SQL but in filesystem, fetch it to compare // photo and public keys are not stored in SQL but in filesystem, fetch it to compare
$contact['files'] = 0; $contact['files'] = 0;
@ -379,7 +389,7 @@ class Import
} }
} }
// if requested, also set memberships // if requested, also set memberships
if ($type === 'users+groups') if ($type === 'users+groups' && !$dry_run)
{ {
// LDAP backend does not query it automatic // LDAP backend does not query it automatic
if (!isset($account['memberships'])) if (!isset($account['memberships']))
@ -412,7 +422,7 @@ class Import
if ($set_members) if ($set_members)
{ {
foreach($this->setMembers($set_members) as $name => $num) foreach($this->setMembers($set_members, $dry_run) as $name => $num)
{ {
$$name += $num; $$name += $num;
} }
@ -421,15 +431,23 @@ class Import
// do we need to delete (or deactivate) no longer existing users // do we need to delete (or deactivate) no longer existing users
if ($delete !== 'no' && $sql_users) if ($delete !== 'no' && $sql_users)
{ {
if ($delete === 'deactivate') $num = count($sql_users);
if ($dry_run)
{
$this->logger("Dry-run: would ".($delete === 'deactivate' ? 'deactivate' : 'delete')." $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 += $num;
}
elseif ($delete === 'deactivate')
{ {
$GLOBALS['egw']->db->update(Sql::TABLE, ['account_status' => null], ['account_id' => array_keys($sql_users)], __LINE__, __FILE__); $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) $this->logger("Deactivated $num no longer existing user(s): ".implode(', ', array_map(static function ($account_id, $account_lid)
{ {
return $account_lid.' (#'.$account_id.')'; return $account_lid.' (#'.$account_id.')';
}, array_keys($sql_users), $sql_users)), 'detail'); }, array_keys($sql_users), $sql_users)), 'detail');
$deleted += count($sql_users); $deleted += $num;
} }
else else
{ {
@ -450,21 +468,28 @@ class Import
$last_run = max($start_import-1, $last_modified); $last_run = max($start_import-1, $last_modified);
Api\Config::save_value('account_import_lastrun', $last_run, 'phpgwapi'); Api\Config::save_value('account_import_lastrun', $last_run, 'phpgwapi');
$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 && !$dry_run)
{ {
$this->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)
{
if ($dry_run)
{
$result = "Dry-run: would created $created, updated $updated and deleted $deleted account(s).";
}
else
{ {
$result = "Created $created, updated $updated and deleted $deleted account(s), with $errors error(s)."; $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.";
} }
$this->logger($result, 'info'); $this->logger($result, 'info');
if ($initial_import && self::installAsyncJob()) if (!$dry_run && $initial_import && self::installAsyncJob())
{ {
$this->logger('Async job for periodic import installed', 'info'); $this->logger('Async job for periodic import installed', 'info');
} }
@ -500,9 +525,10 @@ class Import
* @param string $delete what to do with no longer existing groups: "yes": delete incl. data, "deactivate": delete group, "no": do nothing * @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
* @param bool $dry_run true: only log what would be done, but do NOT make any changes
* @return int[] values for keys "created", "updated", "uptodate", "errors", "deleted" * @return int[] values for keys "created", "updated", "uptodate", "errors", "deleted"
*/ */
protected function groups(?int $modified, string $delete, array &$groups=null, array &$set_members=null) protected function groups(?int $modified, string $delete, array &$groups=null, array &$set_members=null, bool $dry_run=false)
{ {
// 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 ($modified) $delete = 'no'; if ($modified) $delete = 'no';
@ -537,6 +563,12 @@ class Import
{ {
unset($group['account_id']); unset($group['account_id']);
} }
if ($dry_run)
{
$this->logger("Dry-run: would create group '$group[account_lid]' (#$account_id".($sql_id != $account_id ? " as #$sql_id" : '').')', 'detail');
$created++;
continue;
}
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 eg. home-directory or mail account
@ -569,7 +601,12 @@ 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 ($this->accounts_sql->save($to_update) < 0) if ($dry_run)
{
$this->logger("Dry-run: would update group '$group[account_lid]' (#$sql_id): ".json_encode($diff), 'detail');
$updated++;
}
elseif ($this->accounts_sql->save($to_update) < 0)
{ {
Api\Hooks::process($to_update+array( Api\Hooks::process($to_update+array(
'location' => 'editgroup', 'location' => 'editgroup',
@ -607,10 +644,9 @@ class Import
// delete the groups not returned from LDAP, groups can NOT be deactivated, we just delete them in the DB // delete the groups not returned from LDAP, groups can NOT be deactivated, we just delete them in the DB
foreach($delete !== 'no' ? $sql_groups : [] as $account_id => $account_lid) foreach($delete !== 'no' ? $sql_groups : [] as $account_id => $account_lid)
{ {
static $acl=null;
if ($delete === 'yes') if ($delete === 'yes')
{ {
if ($this->deleteAccount($account_id, $account_lid, $this->logger)) if ($this->deleteAccount($account_id, $account_lid, $dry_run))
{ {
$deleted++; $deleted++;
} }
@ -636,9 +672,10 @@ class Import
* *
* @param int $account_id * @param int $account_id
* @param string $account_lid * @param string $account_lid
* @param bool $dry_run true: only log what would be done, but do NOT make any changes
* @return bool * @return bool
*/ */
protected function deleteAccount(int $account_id, string $account_lid) protected function deleteAccount(int $account_id, string $account_lid, bool $dry_run=false)
{ {
// make sure admin_cmd_delete_account uses the SQL accounts object, to not delete in the source, but in EGroupware DB! // 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; $backup_accounts = $GLOBALS['egw']->accounts;
@ -647,9 +684,16 @@ class Import
$type = $account_id < 0 ? 'group' : 'user'; $type = $account_id < 0 ? 'group' : 'user';
try { try {
if ($dry_run)
{
$this->logger("Dry-run: would deleted no longer existing $type '$account_lid' (#$account_id)", 'detail');
}
else
{
$cmd = new \admin_cmd_delete_account($account_id, null, $account_id > 0); $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'); $this->logger("Successful deleted no longer existing $type '$account_lid' (#$account_id): ".$cmd->run(), 'detail');
} }
}
catch (\Exception $e) { catch (\Exception $e) {
$this->logger("Error deleting no longer existing $type '$account_lid' (#$account_id): ".$e->getMessage(), 'error'); $this->logger("Error deleting no longer existing $type '$account_lid' (#$account_id): ".$e->getMessage(), 'error');
} }
@ -799,14 +843,21 @@ class Import
* Set members of a group specified by its (sql)account_id after an incremental update of the groups * Set members of a group specified by its (sql)account_id after an incremental update of the groups
* *
* We need to take into account: * We need to take into account:
* - members/users might not yet be added, if visible members are by membership to that group (eg. custom account-filter by membership in Default group) * - members/users might not yet be added, if visible members are by membership to that group (e.g. custom account-filter by membership in Default group)
* - members might not be readable from LDAP, because the are not in account-filter * - members might not be readable from LDAP, because they 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 bool $dry_run true: only log what would be done, but do NOT make any changes
* @return int[] values for keys "created", "updated" and "errors" * @return int[] values for keys "created", "updated" and "errors"
* @todo add dry_run support
*/ */
protected function setMembers(array $set_members) protected function setMembers(array $set_members, bool $dry_run=false)
{ {
if ($dry_run)
{
$this->logger("Dry-run: setting (or adding) members of groups not (yet) supported --> ignored", 'detail');
return [];
}
// setting (new) members // setting (new) members
$created = $updated = $errors = 0; $created = $updated = $errors = 0;
foreach($set_members as $sql_group_id => $members) foreach($set_members as $sql_group_id => $members)

View File

@ -50,7 +50,8 @@ try {
$import->showLog(); $import->showLog();
return; return;
} }
$import->run(!empty($_GET['initial']) && $_GET['initial'] !== 'false'); $import->run(!empty($_GET['initial']) && $_GET['initial'] !== 'false',
!empty($_GET['dry_run'] ?? $_GET['dry-run']) && ($_GET['dry_run'] ?? $_GET['dry-run']) !== 'false');
} }
catch (\Exception $e) { catch (\Exception $e) {
http_response_code(500); http_response_code(500);

View File

@ -500,8 +500,9 @@
<tr class="row_on"> <tr class="row_on">
<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'+(document.getElementById('import_dry_run')?.checked?'&dry_run=true':''), '_blank')">{lang_Initial_import}</button>
<button onclick="window.open('account_import.php', '_blank')">{lang_Incremental_import}</button><br/> <button onclick="window.open('account_import.php'+(document.getElementById('import_dry_run')?.checked?'?dry_run=true':''), '_blank')">{lang_Incremental_import}</button>
<label><input type="checkbox" id="import_dry_run"/> {lang_Dry-run_(only_show_what_would_happen)}</label><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>