Select Git revision
-
Alexander Bias authoredAlexander Bias authored
auth.php 28.63 KiB
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Auth plugin "LDAP SyncPlus"
*
* @package auth_ldap_syncplus
* @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
// @codingStandardsIgnoreFile
// Let codechecker ignore this file. This code mostly re-used from auth_ldap and the problems are already there and not made by us.
global $CFG;
require_once($CFG->libdir.'/authlib.php');
require_once($CFG->libdir.'/ldaplib.php');
require_once($CFG->dirroot.'/user/lib.php');
require_once($CFG->dirroot.'/auth/ldap/locallib.php');
require_once(__DIR__.'/../ldap/auth.php');
require_once(__DIR__.'/locallib.php');
/**
* Auth plugin "LDAP SyncPlus" - Auth class
*
* @package auth_ldap_syncplus
* @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class auth_plugin_ldap_syncplus extends auth_plugin_ldap {
/**
* Constructor with initialisation.
*/
public function __construct() {
$this->authtype = 'ldap_syncplus';
$this->roleauth = 'auth_ldap';
$this->errorlogtag = '[AUTH LDAP SYNCPLUS] ';
$this->init_plugin($this->authtype);
}
/**
* Old syntax of class constructor. Deprecated in PHP7.
*
* @deprecated since Moodle 3.1
*/
public function auth_plugin_ldap_syncplus() {
debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
self::__construct();
}
/**
* Syncronizes user fron external LDAP server to moodle user table
*
* 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
*/
function sync_users($do_updates=true) {
global $CFG, $DB;
require_once($CFG->dirroot . '/user/profile/lib.php');
mtrace(get_string('connectingldap', 'auth_ldap'));
$ldapconnection = $this->ldap_connect();
$dbman = $DB->get_manager();
// Define table user to be created.
$table = new xmldb_table('tmp_extuser');
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('username', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
$table->add_field('mnethostid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$table->add_index('username', XMLDB_INDEX_UNIQUE, array('mnethostid', 'username'));
mtrace(get_string('creatingtemptable', 'auth_ldap', 'tmp_extuser'));
$dbman->create_temp_table($table);
// Get user's list from ldap to sql in a scalable fashion.
// Prepare some data we'll need.
$filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')';
$servercontrols = array();
$contexts = explode(';', $this->config->contexts);
if (!empty($this->config->create_context)) {
array_push($contexts, $this->config->create_context);
}
$ldappagedresults = ldap_paged_results_supported($this->config->ldap_version, $ldapconnection);
$ldapcookie = '';
foreach ($contexts as $context) {
$context = trim($context);
if (empty($context)) {
continue;
}
do {
if ($ldappagedresults) {
// TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
if (version_compare(PHP_VERSION, '7.3.0', '<')) {
// Before 7.3, use this function that was deprecated in PHP 7.4.
ldap_control_paged_result($ldapconnection, $this->config->pagesize, true, $ldapcookie);
} else {
// PHP 7.3 and up, use server controls.
$servercontrols = array(array(
'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array(
'size' => $this->config->pagesize, 'cookie' => $ldapcookie)));
}
}
if ($this->config->search_sub) {
// Use ldap_search to find first user from subtree.
// TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
if (version_compare(PHP_VERSION, '7.3.0', '<')) {
$ldapresult = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute));
} else {
$ldapresult = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute),
0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
}
} else {
// Search only in this context.
// TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
if (version_compare(PHP_VERSION, '7.3.0', '<')) {
$ldapresult = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute));
} else {
$ldapresult = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute),
0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
}
}
if(!$ldapresult) {
continue;
}
if ($ldappagedresults) {
// Get next server cookie to know if we'll need to continue searching.
$ldapcookie = '';
// TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
if (version_compare(PHP_VERSION, '7.3.0', '<')) {
// Before 7.3, use this function that was deprecated in PHP 7.4.
$pagedresp = ldap_control_paged_result_response($ldapconnection, $ldapresult, $ldapcookie);
// Function ldap_control_paged_result_response() does not overwrite $ldapcookie if it fails, by
// setting this to null we avoid an infinite loop.
if ($pagedresp === false) {
$ldapcookie = null;
}
} else {
// Get next cookie from controls.
ldap_parse_result($ldapconnection, $ldapresult, $errcode, $matcheddn,
$errmsg, $referrals, $controls);
if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
$ldapcookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
}
}
}
if ($entry = @ldap_first_entry($ldapconnection, $ldapresult)) {
do {
$value = ldap_get_values_len($ldapconnection, $entry, $this->config->user_attribute);
$value = core_text::convert($value[0], $this->config->ldapencoding, 'utf-8');
$value = trim($value);
$this->ldap_bulk_insert($value);
} while ($entry = ldap_next_entry($ldapconnection, $entry));
}
unset($ldapresult); // Free mem.
} while ($ldappagedresults && $ldapcookie !== null && $ldapcookie != '');
}
// If LDAP paged results were used, the current connection must be completely
// closed and a new one created, to work without paged results from here on.
if ($ldappagedresults) {
$this->ldap_close(true);
$ldapconnection = $this->ldap_connect();
}
// Preserve our user database.
// If the temp table is empty, it probably means that something went wrong, exit
// so as to avoid mass deletion of users; which is hard to undo.
$count = $DB->count_records_sql('SELECT COUNT(username) AS count, 1 FROM {tmp_extuser}');
if ($count < 1) {
mtrace(get_string('didntgetusersfromldap', 'auth_ldap'));
$dbman->drop_table($table);
$this->ldap_close();
return false;
} else {
mtrace(get_string('gotcountrecordsfromldap', 'auth_ldap', $count));
}
// Non Grace Period Synchronisation.
if ($this->config->removeuser != AUTH_REMOVEUSER_DELETEWITHGRACEPERIOD) {
// User removal.
// Find users in DB that aren't in ldap -- to be removed!
// this is still not as scalable (but how often do we mass delete?).
if ($this->config->removeuser == AUTH_REMOVEUSER_FULLDELETE) {
$sql = "SELECT u.*
FROM {user} u
LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid)
WHERE u.auth = :auth
AND u.deleted = 0
AND e.username IS NULL";
$remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype));
if (!empty($remove_users)) {
mtrace(get_string('userentriestoremove', 'auth_ldap', count($remove_users)));
foreach ($remove_users as $user) {
if (delete_user($user)) {
mtrace("\t".get_string('auth_dbdeleteuser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)));
} else {
mtrace("\t".get_string('auth_dbdeleteusererror', 'auth_db', $user->username));
}
}
} else {
mtrace(get_string('nouserentriestoremove', 'auth_ldap'));
}
unset($remove_users); // Free mem!
} else if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) {
$sql = "SELECT u.*
FROM {user} u
LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid)
WHERE u.auth = :auth
AND u.deleted = 0
AND u.suspended = 0
AND e.username IS NULL";
$remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype));
if (!empty($remove_users)) {
mtrace(get_string('userentriestoremove', 'auth_ldap', count($remove_users)));
foreach ($remove_users as $user) {
$updateuser = new stdClass();
$updateuser->id = $user->id;
$updateuser->suspended = 1;
user_update_user($updateuser, false);
mtrace("\t".get_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)));
\core\session\manager::kill_user_sessions($user->id);
}
} else {
mtrace(get_string('nouserentriestoremove', 'auth_ldap'));
}
unset($remove_users); // Free mem!
}
// Revive suspended users.
if (!empty($this->config->removeuser) and $this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) {
$sql = "SELECT u.id, u.username
FROM {user} u
JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid)
WHERE (u.auth = 'nologin' OR (u.auth = ? AND u.suspended = 1)) AND u.deleted = 0";
// Note: 'nologin' is there for backwards compatibility.
$revive_users = $DB->get_records_sql($sql, array($this->authtype));
if (!empty($revive_users)) {
mtrace(get_string('userentriestorevive', 'auth_ldap', count($revive_users)));
foreach ($revive_users as $user) {
$updateuser = new stdClass();
$updateuser->id = $user->id;
$updateuser->auth = $this->authtype;
$updateuser->suspended = 0;
user_update_user($updateuser, false);
mtrace("\t".get_string('auth_dbreviveduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)));
}
} else {
mtrace(get_string('nouserentriestorevive', 'auth_ldap'));
}
unset($revive_users);
}
}
// Grace Period Synchronisation.
else if (!empty($this->config->removeuser) and $this->config->removeuser == AUTH_REMOVEUSER_DELETEWITHGRACEPERIOD) {
// Revive suspended users.
$sql = "SELECT u.id, u.username
FROM {user} u
JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid)
WHERE (u.auth = 'nologin' OR (u.auth = ? AND u.suspended = 1)) AND u.deleted = 0";
// Note: 'nologin' is there for backwards compatibility.
$revive_users = $DB->get_records_sql($sql, array($this->authtype));
if (!empty($revive_users)) {
mtrace(get_string('userentriestorevive', 'auth_ldap', count($revive_users)));
foreach ($revive_users as $user) {
$updateuser = new stdClass();
$updateuser->id = $user->id;
$updateuser->auth = $this->authtype;
$updateuser->suspended = 0;
user_update_user($updateuser, false);
mtrace("\t".get_string('auth_dbreviveduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)));
}
} else {
mtrace(get_string('nouserentriestorevive', 'auth_ldap'));
}
unset($revive_users);
// User temporary suspending.
$sql = "SELECT u.*
FROM {user} u
LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid)
WHERE u.auth = :auth
AND u.deleted = 0
AND u.suspended = 0
AND e.username IS NULL";
$remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype));
if (!empty($remove_users)) {
mtrace(get_string('userentriestosuspend', 'auth_ldap_syncplus', count($remove_users)));
foreach ($remove_users as $user) {
$updateuser = new stdClass();
$updateuser->id = $user->id;
$updateuser->suspended = 1;
$updateuser->timemodified = time(); // Remember suspend time, abuse timemodified column for this.
user_update_user($updateuser, false);
mtrace("\t".get_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)));
\core\session\manager::kill_user_sessions($user->id);
}
} else {
mtrace(get_string('nouserentriestosuspend', 'auth_ldap_syncplus'));
}
unset($remove_users); // Free mem!
// User complete removal.
$sql = "SELECT u.*
FROM {user} u
LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid)
WHERE u.auth = :auth
AND u.deleted = 0
AND e.username IS NULL";
$remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype));
if (!empty($remove_users)) {
mtrace(get_string('userentriestoremove', 'auth_ldap', count($remove_users)));
foreach ($remove_users as $user) {
// Do only if user was suspended before grace period.
$graceperiod = max(intval($this->config->removeuser_graceperiod), 0);
// Fix problems if grace period setting was negative or no number.
if (time() - $user->timemodified >= $graceperiod * 24 * 3600) {
if (delete_user($user)) {
mtrace("\t".get_string('auth_dbdeleteuser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)));
} else {
mtrace("\t".get_string('auth_dbdeleteusererror', 'auth_db', $user->username));
}
// Otherwise inform about ongoing grace period.
} else {
mtrace("\t".get_string('waitinginremovalqueue', 'auth_ldap_syncplus', array('days'=>$graceperiod, 'name'=>$user->username, 'id'=>$user->id)));
}
}
} else {
mtrace(get_string('nouserentriestoremove', 'auth_ldap'));
}
unset($remove_users); // Free mem!
}
// User Updates - time-consuming (optional).
if ($do_updates) {
// Narrow down what fields we need to update.
$updatekeys = $this->get_profile_keys();
} else {
mtrace(get_string('noupdatestobedone', 'auth_ldap'));
}
if ($do_updates and !empty($updatekeys)) { // 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)) {
mtrace(get_string('userentriestoupdate', 'auth_ldap', count($users)));
$transaction = $DB->start_delegated_transaction();
$xcount = 0;
$maxxcount = 100;
foreach ($users as $user) {
$userinfo = $this->get_userinfo($user->username);
if (!$this->update_user_record($user->username, $updatekeys, true,
$this->is_user_suspended((object) $userinfo))) {
$skipped = ' - '.get_string('skipped');
}
else {
$skipped = '';
}
mtrace("\t".get_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)).$skipped);
$xcount++;
// Update system roles, if needed.
$this->sync_roles($user);
}
$transaction->allow_commit();
unset($users); // free mem.
}
} else { // end do updates.
mtrace(get_string('noupdatestobedone', 'auth_ldap'));
}
// User Additions.
// Find users missing in DB that are in LDAP
// and gives me a nifty object I don't want.
// note: we do not care about deleted accounts anymore, this feature was replaced by suspending to nologin auth plugin.
if (!empty($this->config->sync_script_createuser_enabled) and $this->config->sync_script_createuser_enabled == 1) {
$sql = 'SELECT e.id, e.username
FROM {tmp_extuser} e
LEFT JOIN {user} u ON (e.username = u.username AND e.mnethostid = u.mnethostid)
WHERE u.id IS NULL';
$add_users = $DB->get_records_sql($sql);
if (!empty($add_users)) {
mtrace(get_string('userentriestoadd', 'auth_ldap', count($add_users)));
$transaction = $DB->start_delegated_transaction();
foreach ($add_users as $user) {
$user = $this->get_userinfo_asobj($user->username);
// Prep a few params.
$user->modified = time();
$user->confirmed = 1;
$user->auth = $this->authtype;
$user->mnethostid = $CFG->mnet_localhost_id;
// get_userinfo_asobj() might have replaced $user->username with the value
// from the LDAP server (which can be mixed-case). Make sure it's lowercase.
$user->username = trim(core_text::strtolower($user->username));
// It isn't possible to just rely on the configured suspension attribute since
// things like active directory use bit masks, other things using LDAP might
// do different stuff as well.
//
// The cast to int is a workaround for MDL-53959.
$user->suspended = (int)$this->is_user_suspended($user);
if (empty($user->lang)) {
$user->lang = $CFG->lang;
}
if (empty($user->calendartype)) {
$user->calendartype = $CFG->calendartype;
}
$id = user_create_user($user, false);
mtrace("\t".get_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)));
$euser = $DB->get_record('user', array('id' => $id));
if (!empty($this->config->forcechangepassword)) {
set_user_preference('auth_forcepasswordchange', 1, $id);
}
// Save custom profile fields.
$this->update_user_record($user->username, $this->get_profile_keys(true), false);
// Add roles if needed.
$this->sync_roles($euser);
}
$transaction->allow_commit();
unset($add_users); // free mem
} else {
mtrace(get_string('nouserstobeadded', 'auth_ldap'));
}
} else {
mtrace(get_string('nouserstobeadded', 'auth_ldap'));
}
$dbman->drop_table($table);
$this->ldap_close();
return true;
}
/**
* Support login via email ($CFG->authloginviaemail) for first-time LDAP logins
* @return void
*/
public function loginpage_hook() {
global $CFG, $frm, $DB;
// If $CFG->authloginviaemail is not set, users don't want to login by mail, call parent hook and return.
if ($CFG->authloginviaemail != 1) {
parent::loginpage_hook(); // Call parent function to retain its functionality.
return;
}
// Get submitted form data.
$frm = data_submitted();
// If there is no username submitted, there's nothing to do, call parent hook and return.
if (empty($frm->username)) {
parent::loginpage_hook(); // Call parent function to retain its functionality.
return;
}
// Clean username parameter to make sure that its an email address.
$email = clean_param($frm->username, PARAM_EMAIL);
// If we don't have an email adress, there's nothing to do, call parent hook and return.
if ($email == '' || strpos($email, '@') == false) {
parent::loginpage_hook(); // Call parent function to retain its functionality.
return;
}
// If there is an existing useraccount with this email adress as email address (then a Moodle account already exists and
// the standard mechanism of $CFG->authloginviaemail will kick in automatically) or if there is an existing useraccount
// with this email adress as username (which is not forbidden, so this useraccount has to be used), call parent hook and
// return.
if ($DB->count_records_select('user', '(username = :p1 OR email = :p2) AND deleted = 0',
array('p1' => $email, 'p2' => $email)) > 0) {
parent::loginpage_hook(); // Call parent function to retain its functionality.
return;
}
// Get auth plugin.
$authplugin = get_auth_plugin('ldap_syncplus');
// If there is no email field mapping configured, we don't know where we can find the email adress in LDAP,
// call parent hook and return.
if (empty($authplugin->config->field_map_email)) {
parent::loginpage_hook(); // Call parent function to retain its functionality.
return;
}
// Prepare LDAP search.
$contexts = explode(';', $authplugin->config->contexts);
$filter = '(&('.$authplugin->config->field_map_email.'='.ldap_filter_addslashes($email).')'.
$authplugin->config->objectclass.')';
// Connect to LDAP.
$ldapconnection = $authplugin->ldap_connect();
// Array for saving the user's ids which are found in the configured LDAP contexts.
$uidsfound = array();
// Look for users matching the given email adress in LDAP.
foreach ($contexts as $context) {
// Verify that the given context is valid.
$context = trim($context);
if (empty($context)) {
continue;
}
// Search LDAP.
if ($authplugin->config->search_sub) {
// Use ldap_search to find first user from subtree.
$ldapresult = ldap_search($ldapconnection, $context, $filter, array($authplugin->config->user_attribute));
} else {
// Search only in this context.
$ldapresult = ldap_list($ldapconnection, $context, $filter, array($authplugin->config->user_attribute));
}
// If there is no LDAP result or if the user was not found in this context, continue with next context.
if (!$ldapresult || ldap_count_entries($ldapconnection, $ldapresult) == 0) {
continue;
}
// If there is not exactly one matching user, we can't continue, call parent hook and return.
if (ldap_count_entries($ldapconnection, $ldapresult) != 1) {
parent::loginpage_hook(); // Call parent function to retain its functionality.
return;
}
// Get this one matching user entry.
if (!$ldapentry = ldap_first_entry($ldapconnection, $ldapresult)) {
parent::loginpage_hook(); // Call parent function to retain its functionality.
return;
}
// Get the uid attribute's value(s) from this user entry.
$values = ldap_get_values($ldapconnection, $ldapentry, $authplugin->config->user_attribute);
// If there is not exactly one copy of the uid attribute in the LDAP user entry, we don't know which one to use,
// call parent hook and return.
if ($values['count'] != 1) {
parent::loginpage_hook(); // Call parent function to retain its functionality.
return;
}
// Remember this one user's uid attribute.
$uidsfound[] = $values[0];
unset($ldapresult); // Free mem!
}
// After we have checked all contexts, verify that we have found only one user in total.
// If not, we can't continue, call parent hook and return.
if (count($uidsfound) != 1) {
parent::loginpage_hook(); // Call parent function to retain its functionality.
return;
// Success!
// Replace the form data's username with the user attribute from LDAP, it will be held in the global $frm variable.
} else {
$frm->username = $uidsfound[0];
parent::loginpage_hook(); // Call parent function to retain its functionality.
return;
}
}
}