Commit 89e9321f authored by Petr Skoda's avatar Petr Skoda
Browse files

MDL-47834 auth: Allow enforcing of login limits

parent ca0e301c
......@@ -77,6 +77,10 @@ if ($hassiteconfig) {
$temp->add(new admin_setting_configcheckbox('loginpageautofocus', new lang_string('loginpageautofocus', 'admin'), new lang_string('loginpageautofocus_help', 'admin'), 0));
$temp->add(new admin_setting_configselect('guestloginbutton', new lang_string('guestloginbutton', 'auth'),
new lang_string('showguestlogin', 'auth'), '1', array('0'=>new lang_string('hide'), '1'=>new lang_string('show'))));
$options = array(0 => get_string('no'), 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 10 => 10, 20 => 20, 50 => 50);
$temp->add(new admin_setting_configselect('limitconcurrentlogins',
new lang_string('limitconcurrentlogins', 'core_auth'),
new lang_string('limitconcurrentlogins_desc', 'core_auth'), 0, $options));
$temp->add(new admin_setting_configtext('alternateloginurl', new lang_string('alternateloginurl', 'auth'),
new lang_string('alternatelogin', 'auth', htmlspecialchars(get_login_url())), ''));
$temp->add(new admin_setting_configtext('forgottenpasswordurl', new lang_string('forgottenpasswordurl', 'auth'),
......
......@@ -107,6 +107,8 @@ $string['informminpasswordupper'] = 'at least {$a} upper case letter(s)';
$string['informpasswordpolicy'] = 'The password must have {$a}';
$string['instructions'] = 'Instructions';
$string['internal'] = 'Internal';
$string['limitconcurrentlogins'] = 'Limit concurrent logins';
$string['limitconcurrentlogins_desc'] = 'If enabled the number of concurrent browser logins for each user is restricted. The oldest session is terminated after reaching the limit, please note that users may lose all unsaved work. This setting is not compatible with single sign-on (SSO) authentication plugins.';
$string['locked'] = 'Locked';
$string['authloginviaemail'] = 'Allow login via email';
$string['authloginviaemail_desc'] = 'Allow users to use both username and email address (if unique) for site login.';
......
......@@ -622,6 +622,62 @@ class manager {
}
}
/**
* Terminate other sessions of current user depending
* on $CFG->limitconcurrentlogins restriction.
*
* This is expected to be called right after complete_user_login().
*
* NOTE:
* * Do not use from SSO auth plugins, this would not work.
* * Do not use from web services because they do not have sessions.
*
* @param int $userid
* @param string $sid session id to be always keep, usually the current one
* @return void
*/
public static function apply_concurrent_login_limit($userid, $sid = null) {
global $CFG, $DB;
// NOTE: the $sid parameter is here mainly to allow testing,
// in most cases it should be current session id.
if (isguestuser($userid) or empty($userid)) {
// This applies to real users only!
return;
}
if (empty($CFG->limitconcurrentlogins) or $CFG->limitconcurrentlogins < 0) {
return;
}
$count = $DB->count_records('sessions', array('userid' => $userid));
if ($count <= $CFG->limitconcurrentlogins) {
return;
}
$i = 0;
$select = "userid = :userid";
$params = array('userid' => $userid);
if ($sid) {
if ($DB->record_exists('sessions', array('sid' => $sid, 'userid' => $userid))) {
$select .= " AND sid <> :sid";
$params['sid'] = $sid;
$i = 1;
}
}
$sessions = $DB->get_records_select('sessions', $select, $params, 'timecreated DESC', 'id, sid');
foreach ($sessions as $session) {
$i++;
if ($i <= $CFG->limitconcurrentlogins) {
continue;
}
self::kill_session($session->sid);
}
}
/**
* Set current user.
*
......
......@@ -326,6 +326,129 @@ class core_session_manager_testcase extends advanced_testcase {
$this->assertEquals(1, $DB->count_records('sessions', array('userid' => $userid, 'sid' => md5('pokus5'))));
}
public function test_apply_concurrent_login_limit() {
global $DB;
$this->resetAfterTest();
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$guest = guest_user();
$record = new \stdClass();
$record->state = 0;
$record->sessdata = null;
$record->userid = $user1->id;
$record->timemodified = time();
$record->firstip = $record->lastip = '10.0.0.1';
$record->sid = md5('hokus1');
$record->timecreated = 20;
$DB->insert_record('sessions', $record);
$record->sid = md5('hokus2');
$record->timecreated = 10;
$DB->insert_record('sessions', $record);
$record->sid = md5('hokus3');
$record->timecreated = 30;
$DB->insert_record('sessions', $record);
$record->userid = $user2->id;
$record->sid = md5('pokus1');
$record->timecreated = 20;
$DB->insert_record('sessions', $record);
$record->sid = md5('pokus2');
$record->timecreated = 10;
$DB->insert_record('sessions', $record);
$record->sid = md5('pokus3');
$record->timecreated = 30;
$DB->insert_record('sessions', $record);
$record->timecreated = 10;
$record->userid = $guest->id;
$record->sid = md5('g1');
$DB->insert_record('sessions', $record);
$record->sid = md5('g2');
$DB->insert_record('sessions', $record);
$record->sid = md5('g3');
$DB->insert_record('sessions', $record);
$record->userid = 0;
$record->sid = md5('nl1');
$DB->insert_record('sessions', $record);
$record->sid = md5('nl2');
$DB->insert_record('sessions', $record);
$record->sid = md5('nl3');
$DB->insert_record('sessions', $record);
set_config('limitconcurrentlogins', 0);
$this->assertCount(12, $DB->get_records('sessions'));
\core\session\manager::apply_concurrent_login_limit($user1->id);
\core\session\manager::apply_concurrent_login_limit($user2->id);
\core\session\manager::apply_concurrent_login_limit($guest->id);
\core\session\manager::apply_concurrent_login_limit(0);
$this->assertCount(12, $DB->get_records('sessions'));
set_config('limitconcurrentlogins', -1);
\core\session\manager::apply_concurrent_login_limit($user1->id);
\core\session\manager::apply_concurrent_login_limit($user2->id);
\core\session\manager::apply_concurrent_login_limit($guest->id);
\core\session\manager::apply_concurrent_login_limit(0);
$this->assertCount(12, $DB->get_records('sessions'));
set_config('limitconcurrentlogins', 2);
\core\session\manager::apply_concurrent_login_limit($user1->id);
$this->assertCount(11, $DB->get_records('sessions'));
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 20)));
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 30)));
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 10)));
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 20)));
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 30)));
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 10)));
set_config('limitconcurrentlogins', 2);
\core\session\manager::apply_concurrent_login_limit($user2->id, md5('pokus2'));
$this->assertCount(10, $DB->get_records('sessions'));
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 20)));
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 30)));
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 10)));
\core\session\manager::apply_concurrent_login_limit($guest->id);
\core\session\manager::apply_concurrent_login_limit(0);
$this->assertCount(10, $DB->get_records('sessions'));
set_config('limitconcurrentlogins', 1);
\core\session\manager::apply_concurrent_login_limit($user1->id, md5('grrr'));
$this->assertCount(9, $DB->get_records('sessions'));
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 20)));
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 30)));
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 10)));
\core\session\manager::apply_concurrent_login_limit($user1->id);
$this->assertCount(9, $DB->get_records('sessions'));
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 20)));
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 30)));
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 10)));
\core\session\manager::apply_concurrent_login_limit($user2->id, md5('pokus2'));
$this->assertCount(8, $DB->get_records('sessions'));
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 20)));
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 30)));
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 10)));
\core\session\manager::apply_concurrent_login_limit($user2->id);
$this->assertCount(8, $DB->get_records('sessions'));
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 20)));
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 30)));
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 10)));
\core\session\manager::apply_concurrent_login_limit($guest->id);
\core\session\manager::apply_concurrent_login_limit(0);
$this->assertCount(8, $DB->get_records('sessions'));
}
public function test_kill_all_sessions() {
global $DB, $USER;
$this->resetAfterTest();
......
......@@ -80,6 +80,8 @@ if (!empty($data) || (!empty($p) && !empty($s))) {
complete_user_login($user);
\core\session\manager::apply_concurrent_login_limit($user->id, session_id());
if ( ! empty($SESSION->wantsurl) ) { // Send them where they were going
$goto = $SESSION->wantsurl;
unset($SESSION->wantsurl);
......
......@@ -184,6 +184,8 @@ if ($frm and isset($frm->username)) { // Login WITH
/// Let's get them all set up.
complete_user_login($user);
\core\session\manager::apply_concurrent_login_limit($user->id, session_id());
// sets the username cookie
if (!empty($CFG->nolastloggedin)) {
// do not store last logged in user in cookie
......
......@@ -254,6 +254,8 @@ function core_login_process_password_set($token) {
}
complete_user_login($user); // Triggers the login event.
\core\session\manager::apply_concurrent_login_limit($user->id, session_id());
$urltogo = core_login_get_return_url();
unset($SESSION->wantsurl);
redirect($urltogo, get_string('passwordset'), 1);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment