MDL-47834 auth: Allow enforcing of login limits
authorPetr Skoda <petr.skoda@totaralms.com>
Thu, 23 Oct 2014 02:33:53 +0000 (15:33 +1300)
committerPetr Skoda <petr.skoda@totaralms.com>
Thu, 20 Nov 2014 21:00:01 +0000 (10:00 +1300)
admin/settings/plugins.php
lang/en/auth.php
lib/classes/session/manager.php
lib/tests/session_manager_test.php
login/confirm.php
login/index.php
login/lib.php

index bee9753..7a1551a 100644 (file)
@@ -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'),
index 8c2fa93..0bfe51b 100644 (file)
@@ -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.';
index 1852696..0d461c2 100644 (file)
@@ -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.
      *
index 9090389..a5a15d9 100644 (file)
@@ -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();
index d765361..133aa9b 100644 (file)
@@ -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);
index ab2b802..59c55e4 100644 (file)
@@ -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
index 2996a4c..ac2a1cf 100644 (file)
@@ -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);