diff --git a/auth/ldap/auth.php b/auth/ldap/auth.php index eff6fe2d913..e0971147673 100644 --- a/auth/ldap/auth.php +++ b/auth/ldap/auth.php @@ -668,16 +668,29 @@ class auth_plugin_ldap extends auth_plugin_base { } /** - * Syncronizes user fron external LDAP server to moodle user table + * Synchronise users from the external LDAP server to Moodle's user table. + * + * Calls sync_users_update_callback() with default callback if appropriate. + * + * @param bool $doupdates will do pull in data updates from LDAP if relevant + * @return bool success + */ + public function sync_users($doupdates = true) { + return $this->sync_users_update_callback($doupdates ? [$this, 'update_users'] : null); + } + + /** + * Synchronise users from the external LDAP server to Moodle's user table (callback). * * Sync is now using username attribute. * * Syncing users removes or suspends users that dont exists anymore in external LDAP. * Creates new users and updates coursecreator status of users. * - * @param bool $do_updates will do pull in data updates from LDAP if relevant + * @param callable|null $updatecallback will do pull in data updates from LDAP if relevant + * @return bool success */ - function sync_users($do_updates=true) { + public function sync_users_update_callback(?callable $updatecallback = null): bool { global $CFG, $DB; require_once($CFG->dirroot . '/user/profile/lib.php'); @@ -861,40 +874,24 @@ class auth_plugin_ldap extends auth_plugin_base { unset($revive_users); } - /// User Updates - time-consuming (optional) - if ($do_updates) { - // Narrow down what fields we need to update - $updatekeys = $this->get_profile_keys(); - - } else { - print_string('noupdatestobedone', 'auth_ldap'); - } - if ($do_updates and !empty($updatekeys)) { // run updates only if relevant + if ($updatecallback && $updatekeys = $this->get_profile_keys()) { // Run updates only if relevant. $users = $DB->get_records_sql('SELECT u.username, u.id FROM {user} u WHERE u.deleted = 0 AND u.auth = ? AND u.mnethostid = ?', array($this->authtype, $CFG->mnet_localhost_id)); if (!empty($users)) { - print_string('userentriestoupdate', 'auth_ldap', count($users)); - - foreach ($users as $user) { - $transaction = $DB->start_delegated_transaction(); - echo "\t"; print_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); - $userinfo = $this->get_userinfo($user->username); - if (!$this->update_user_record($user->username, $updatekeys, true, - $this->is_user_suspended((object) $userinfo))) { - echo ' - '.get_string('skipped'); + // Update users in chunks as specified in sync_updateuserchunk. + if (!empty($this->config->sync_updateuserchunk)) { + foreach (array_chunk($users, $this->config->sync_updateuserchunk) as $chunk) { + call_user_func($updatecallback, $chunk, $updatekeys); } - echo "\n"; - - // Update system roles, if needed. - $this->sync_roles($user); - $transaction->allow_commit(); + } else { + call_user_func($updatecallback, $users, $updatekeys); } - unset($users); // free mem + unset($users); // Free mem. } - } else { // end do updates + } else { print_string('noupdatestobedone', 'auth_ldap'); } @@ -974,6 +971,36 @@ class auth_plugin_ldap extends auth_plugin_base { return true; } + /** + * Update users from the external LDAP server into Moodle's user table. + * + * Sync helper + * + * @param array $users chunk of users to update + * @param array $updatekeys fields to update + */ + public function update_users(array $users, array $updatekeys): void { + global $DB; + + print_string('userentriestoupdate', 'auth_ldap', count($users)); + + foreach ($users as $user) { + $transaction = $DB->start_delegated_transaction(); + echo "\t"; + print_string('auth_dbupdatinguser', 'auth_db', ['name' => $user->username, 'id' => $user->id]); + $userinfo = $this->get_userinfo($user->username); + if (!$this->update_user_record($user->username, $updatekeys, true, + $this->is_user_suspended((object) $userinfo))) { + echo ' - '.get_string('skipped'); + } + echo "\n"; + + // Update system roles, if needed. + $this->sync_roles($user); + $transaction->allow_commit(); + } + } + /** * Bulk insert in SQL's temp table */ diff --git a/auth/ldap/classes/task/asynchronous_sync_task.php b/auth/ldap/classes/task/asynchronous_sync_task.php new file mode 100644 index 00000000000..b36fb78ca6d --- /dev/null +++ b/auth/ldap/classes/task/asynchronous_sync_task.php @@ -0,0 +1,61 @@ +. + +/** + * Adhoc task for LDAP user sync. + * + * @package auth_ldap + * @copyright Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace auth_ldap\task; + +use core\task\adhoc_task; + +/** + * Adhoc task class for LDAP user sync. + * + * @package auth_ldap + * @copyright Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class asynchronous_sync_task extends adhoc_task { + + /** @var string Message prefix for mtrace */ + protected const MTRACE_MSG = 'Synced ldap users'; + + /** + * Constructor + */ + public function __construct() { + $this->set_blocking(false); + $this->set_component('auth_ldap'); + } + + /** + * Run users sync. + */ + public function execute() { + $data = $this->get_custom_data(); + + /** @var auth_plugin_ldap $auth */ + $auth = get_auth_plugin('ldap'); + $auth->update_users($data->users, $data->updatekeys); + + mtrace(sprintf(" %s (%d)", self::MTRACE_MSG, count($data->users))); + } +} diff --git a/auth/ldap/classes/task/sync_task.php b/auth/ldap/classes/task/sync_task.php index ed747fb9142..5d2f7166e11 100644 --- a/auth/ldap/classes/task/sync_task.php +++ b/auth/ldap/classes/task/sync_task.php @@ -21,6 +21,7 @@ * @copyright 2015 Vadim Dvorovenko * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + namespace auth_ldap\task; /** @@ -31,6 +32,9 @@ namespace auth_ldap\task; */ class sync_task extends \core\task\scheduled_task { + /** @var string Message prefix for mtrace */ + protected const MTRACE_MSG = 'Synced ldap users'; + /** * Get a descriptive name for this task (shown to admins). * @@ -44,11 +48,22 @@ class sync_task extends \core\task\scheduled_task { * Run users sync. */ public function execute() { - global $CFG; if (is_enabled_auth('ldap')) { + /** @var auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); - $auth->sync_users(true); + $count = 0; + $auth->sync_users_update_callback(function ($users, $updatekeys) use (&$count) { + $asynctask = new asynchronous_sync_task(); + $asynctask->set_custom_data([ + 'users' => $users, + 'updatekeys' => $updatekeys, + ]); + \core\task\manager::queue_adhoc_task($asynctask); + + $count++; + mtrace(sprintf(" %s (%d)", self::MTRACE_MSG, $count)); + sleep(1); + }); } } - } diff --git a/auth/ldap/lang/en/auth_ldap.php b/auth/ldap/lang/en/auth_ldap.php index 3a233b87a17..67fa20571ef 100644 --- a/auth/ldap/lang/en/auth_ldap.php +++ b/auth/ldap/lang/en/auth_ldap.php @@ -142,6 +142,8 @@ $string['renamingnotallowed'] = 'User renaming not allowed in LDAP'; $string['rootdseerror'] = 'Error querying rootDSE for Active Directory'; $string['syncroles'] = 'Synchronise system roles from LDAP'; $string['synctask'] = 'LDAP users sync job'; +$string['sync_updateuserchunk'] = 'Set this value to the number of users you want updated per transaction. Setting this to 0 will update all users in one transaction.'; +$string['sync_updateuserchunk_key'] = 'Sync update users chunk size'; $string['systemrolemapping'] = 'System role mapping'; $string['start_tls'] = 'Use regular LDAP service (port 389) with TLS encryption'; $string['start_tls_key'] = 'Use TLS'; diff --git a/auth/ldap/settings.php b/auth/ldap/settings.php index e9921f6f7ef..cd3b2a2a9d5 100644 --- a/auth/ldap/settings.php +++ b/auth/ldap/settings.php @@ -289,6 +289,11 @@ if ($ADMIN->fulltree) { new lang_string('auth_sync_suspended_key', 'auth'), new lang_string('auth_sync_suspended', 'auth'), 0 , $yesno)); + // Sync update users chunk size. + $settings->add(new admin_setting_configtext('auth_ldap/sync_updateuserchunk', + new lang_string('sync_updateuserchunk_key', 'auth_ldap'), + new lang_string('sync_updateuserchunk', 'auth_ldap'), 1000, PARAM_INT)); + // NTLM SSO Header. $settings->add(new admin_setting_heading('auth_ldap/ntlm', new lang_string('auth_ntlmsso', 'auth_ldap'), '')); diff --git a/auth/ldap/tests/plugin_test.php b/auth/ldap/tests/auth_ldap_test.php similarity index 93% rename from auth/ldap/tests/plugin_test.php rename to auth/ldap/tests/auth_ldap_test.php index d94722c76bb..2fe6a1890f0 100644 --- a/auth/ldap/tests/plugin_test.php +++ b/auth/ldap/tests/auth_ldap_test.php @@ -29,11 +29,32 @@ namespace auth_ldap; * define('TEST_AUTH_LDAP_DOMAIN', 'dc=example,dc=local'); * * @package auth_ldap - * @category phpunit * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class plugin_test extends \advanced_testcase { + +use auth_plugin_ldap; +use auth_ldap\task\{ + sync_task, + asynchronous_sync_task, +}; + +/** + * LDAP authentication plugin tests. + * + * @package auth_ldap + * @copyright 2013 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class auth_ldap_test extends \advanced_testcase { + + public static function setUpBeforeClass(): void { + global $CFG; + parent::setUpBeforeClass(); + + require_once($CFG->dirroot . '/auth/ldap/auth.php'); + require_once($CFG->libdir . '/ldaplib.php'); + } /** * Data provider for auth_ldap tests @@ -65,7 +86,7 @@ class plugin_test extends \advanced_testcase { * @param int $subcontext Value to be configured in settings controlling searching in subcontexts. */ public function test_auth_ldap(int $pagesize, int $subcontext) { - global $CFG, $DB; + global $DB; if (!extension_loaded('ldap')) { $this->markTestSkipped('LDAP extension is not loaded.'); @@ -73,9 +94,6 @@ class plugin_test extends \advanced_testcase { $this->resetAfterTest(); - require_once($CFG->dirroot.'/auth/ldap/auth.php'); - require_once($CFG->libdir.'/ldaplib.php'); - if (!defined('TEST_AUTH_LDAP_HOST_URL') or !defined('TEST_AUTH_LDAP_BIND_DN') or !defined('TEST_AUTH_LDAP_BIND_PW') or !defined('TEST_AUTH_LDAP_DOMAIN')) { $this->markTestSkipped('External LDAP test server not configured.'); } @@ -336,6 +354,41 @@ class plugin_test extends \advanced_testcase { $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); + // Let's test syncing users in chunks of '1'. + set_config('field_updatelocal_email', 'onlogin', 'auth_ldap'); + set_config('sync_updateuserchunk', 1, 'auth_ldap'); + + /** @var auth_plugin_ldap $auth */ + $auth = get_auth_plugin('ldap'); + + $count = 0; + ob_start(); + $auth->sync_users_update_callback(function ($users, $updatekeys) use (&$count) { + $count++; + }); + ob_end_clean(); + // After updating in chunks of '1', we should have counted more than one update. + $this->assertGreaterThan(1, $count); + + ob_start(); + \core\cron::setup_user(); + $cron = new sync_task(); + $cron->execute(); + + $this->runAdhocTasks('\auth_ldap\task\asynchronous_sync_task'); + $output = ob_get_contents(); + ob_end_clean(); + + // Use Reflection to make protected constants available. + $rp = new \ReflectionClassConstant(sync_task::class, 'MTRACE_MSG'); + $synctaskmsg = $rp->getValue(); + $rp = new \ReflectionClassConstant(asynchronous_sync_task::class, 'MTRACE_MSG'); + $asynctaskmsg = $rp->getValue(); + + $this->assertMatchesRegularExpression( + sprintf('/%s.*%s/s', $synctaskmsg, $asynctaskmsg), + $output + ); $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest'); ldap_close($connection); @@ -347,8 +400,6 @@ class plugin_test extends \advanced_testcase { public function test_ldap_user_loggedin_event() { global $CFG, $DB, $USER; - require_once($CFG->dirroot . '/auth/ldap/auth.php'); - $this->resetAfterTest(); $this->assertFalse(isloggedin()); @@ -412,9 +463,6 @@ class plugin_test extends \advanced_testcase { $this->resetAfterTest(); - require_once($CFG->dirroot.'/auth/ldap/auth.php'); - require_once($CFG->libdir.'/ldaplib.php'); - if (!defined('TEST_AUTH_LDAP_HOST_URL') or !defined('TEST_AUTH_LDAP_BIND_DN') or !defined('TEST_AUTH_LDAP_BIND_PW') or !defined('TEST_AUTH_LDAP_DOMAIN')) { $this->markTestSkipped('External LDAP test server not configured.'); } diff --git a/auth/ldap/version.php b/auth/ldap/version.php index 315bda018e7..6ffd09e7031 100644 --- a/auth/ldap/version.php +++ b/auth/ldap/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2023100900; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2024011900; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2023100400; // Requires this Moodle version. $plugin->component = 'auth_ldap'; // Full name of the plugin (used for diagnostics)