From 39e5bb8a0be23fceeed1197a2cfe0608233c632f Mon Sep 17 00:00:00 2001 From: Dmitrii Metelkin Date: Thu, 7 Jul 2016 12:07:28 +1000 Subject: [PATCH] Fix issue #5 - add an option to prevent auth for all users if ip fails whitelist. --- .travis.yml | 67 ++++++ README.md | 8 +- auth.php | 232 +++++++++++++++------ cli/check_before_logging.php | 70 +++++++ config.html | 52 ----- db/upgrade.php | 58 ++++++ lang/ca/auth_ip.php | 33 --- lang/en/auth_ip.php | 19 +- lang/es/auth_ip.php | 33 --- renderer.php | 135 +++++++++++++ settings.php | 62 ++++++ tests/ip_test.php | 382 ++++++++++++++++++++++++++++++++--- users.php | 69 +++++++ version.php | 6 +- 14 files changed, 1003 insertions(+), 223 deletions(-) create mode 100644 .travis.yml create mode 100644 cli/check_before_logging.php delete mode 100755 config.html create mode 100644 db/upgrade.php delete mode 100644 lang/ca/auth_ip.php delete mode 100644 lang/es/auth_ip.php create mode 100644 renderer.php create mode 100644 settings.php create mode 100644 users.php diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..cfe194a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,67 @@ + +language: php + +notifications: + email: + recipients: + +sudo: false + +cache: + directories: + - $HOME/.composer/cache + +php: + - 5.6 + +env: + matrix: + - DB=pgsql MOODLE_BRANCH=MOODLE_27_STABLE + - DB=pgsql MOODLE_BRANCH=MOODLE_28_STABLE + - DB=pgsql MOODLE_BRANCH=MOODLE_29_STABLE + - DB=pgsql MOODLE_BRANCH=MOODLE_30_STABLE + - DB=pgsql MOODLE_BRANCH=MOODLE_31_STABLE + - DB=pgsql MOODLE_BRANCH=master + - DB=mysqli MOODLE_BRANCH=MOODLE_27_STABLE + - DB=mysqli MOODLE_BRANCH=MOODLE_28_STABLE + - DB=mysqli MOODLE_BRANCH=MOODLE_29_STABLE + - DB=mysqli MOODLE_BRANCH=MOODLE_30_STABLE + - DB=mysqli MOODLE_BRANCH=MOODLE_31_STABLE + - DB=mysqli MOODLE_BRANCH=master + +matrix: + include: + - php: 7.0 + env: DB=pgsql MOODLE_BRANCH=MOODLE_30_STABLE + - php: 7.0 + env: DB=pgsql MOODLE_BRANCH=MOODLE_31_STABLE + - php: 7.0 + env: DB=pgsql MOODLE_BRANCH=master + - php: 7.0 + env: DB=mysqli MOODLE_BRANCH=MOODLE_30_STABLE + - php: 7.0 + env: DB=mysqli MOODLE_BRANCH=MOODLE_31_STABLE + - php: 7.0 + env: DB=mysqli MOODLE_BRANCH=master + +before_install: + - cd ../.. + - composer selfupdate + - composer create-project -n --no-dev moodlerooms/moodle-plugin-ci ci ^1 + - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" + +install: + - moodle-plugin-ci install + +script: + - moodle-plugin-ci phplint + - moodle-plugin-ci phpcpd + - moodle-plugin-ci phpmd + - moodle-plugin-ci codechecker + - moodle-plugin-ci validate + - moodle-plugin-ci csslint + - moodle-plugin-ci shifter + - moodle-plugin-ci jshint + - moodle-plugin-ci phpunit + - moodle-plugin-ci behat + diff --git a/README.md b/README.md index d344a68..7f6a07e 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,11 @@ Updating the list of restricted IPs: * Go to Administration->Plugins->Authentication->Manage plugins * Update the list of IPs -NOTE: After updating the list of IPs, an email will be sent to the administrator email, -just for security. - +Preventing all users() log in if their ip fails whitelist +* Go to Administration->Plugins->Authentication->Manage plugins +* Set "Check IP before logging in" to yes. +* Update Error text message. +* Optionally you can log out all currently logged in users who's ip address fail whitelist. License --- diff --git a/auth.php b/auth.php index dd6577c..2d6a645 100755 --- a/auth.php +++ b/auth.php @@ -38,7 +38,7 @@ */ class auth_plugin_ip extends auth_plugin_manual { - function __construct() { + public function __construct() { $this->authtype = 'ip'; $this->config = get_config('auth_ip'); } @@ -51,115 +51,221 @@ function __construct() { * @param string $password password * @return bool */ - function user_login($username, $password) { + public function user_login($username, $password) { global $DB, $CFG; - if (($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id)))) { - // Check if IP is one of the restricted ones. - $userIp = filter_input(INPUT_SERVER, 'REMOTE_ADDR'); - if (isset($userIp) && $this->is_ip_valid($userIp)) { - return validate_internal_user_password($user, $password); - } else { - return false; + if ($this->should_display_error()) { + $this->print_error_message(); + } else { + if (($user = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id)))) { + if (remoteip_in_list($this->config->valid_ips)) { + return validate_internal_user_password($user, $password); + } } } + // If no valid username, we do not allow to create a new user using this auth type. return false; } /** - * Determine if the $ip is in the allowed list of IP or CIDR. + * Returns true if this authentication plugin is 'internal'. * - * @see https://secure.php.net/manual/en/ref.network.php#74656 - * @param $ip * @return bool */ - function is_ip_valid($ip) { - // List of allowed IP addresses or CIDR ranges - $valid_ips_or_cidrs = explode(',', str_replace(' ', '', $this->config->valid_ips)); + public function is_internal() { + return false; + } + + /** + * Implements loginpage_hook(). + */ + public function loginpage_hook() { + if ($this->should_display_error()) { + $this->print_error_message(); + } + } + + /** + * Implements pre_loginpage_hook(). + */ + public function pre_loginpage_hook() { + if ($this->should_display_error()) { + $this->print_error_message(); + } + } + + /** + * Check if we should display error message to a user. + * + * @return bool True | false. + */ + public function should_display_error() { + if (!$this->config->check_before_login) { + return false; + } - // Check all the allowed IP or CIDR for matches - foreach ($valid_ips_or_cidrs as $valid_ip_or_cidr) { - // If CIDR check if in range - if ($this->is_cidr($valid_ip_or_cidr)) { - list ($net, $mask) = explode('/', $valid_ip_or_cidr); + if (remoteip_in_list($this->config->valid_ips)) { + return false; + } + + return true; + } - $ip_net = ip2long($net); - $ip_mask = ~((1 << (32 - $mask)) - 1); + /** + * Prints an error message. + */ + public function print_error_message() { + global $SITE, $PAGE, $OUTPUT, $SESSION; - $ip_ip = ip2long($ip); + header('HTTP/1.0 403 Forbidden'); - $ip_ip_net = $ip_ip & $ip_mask; + if (!isset($PAGE->context)) { + $PAGE->set_context(context_system::instance()); + } - if ($ip_ip_net === $ip_net) { - return true; - } - // Simple IP compare with equality - } elseif ($valid_ip_or_cidr === $ip) { - return true; + if (!isset($PAGE->url)) { + if (isset($SESSION->wantsurl)) { + $PAGE->set_url($SESSION->wantsurl); + } else { + $PAGE->set_url('/'); } } - // No match found mark as not allowed - return false; + $PAGE->set_pagetype('maintenance-message'); + $PAGE->set_pagelayout('standard'); + $PAGE->set_title(strip_tags($SITE->fullname)); + + echo $OUTPUT->header(); + + $renderer = $PAGE->get_renderer('auth_ip'); + + if (isset($this->config->error_text) and !html_is_blank($this->config->error_text)) { + echo $renderer->render_error_message($this->config->error_text); + } + + echo $OUTPUT->footer(); + + die; } /** - * Check if a string is a CIDR. + * Check if provided IP is in provided list of IPs. + * + * @param string $list A list of IPs or subnet addresses. + * @param string $ip IP address. * - * @param string $ip_or_cidr * @return bool */ - function is_cidr($ip_or_cidr) { - return strpos($ip_or_cidr, '/') > 0; + public static function is_ip_in_list($list, $ip) { + $inlist = false; + + $list = explode("\n", $list); + foreach ($list as $subnet) { + $subnet = trim($subnet); + if (address_in_subnet($ip, $subnet)) { + $inlist = true; + break; + } + } + + return $inlist; } /** - * Returns true if this authentication plugin is 'internal'. + * Return SQL data to get all active user sessions from DB. * - * @return bool + * @return array Array of the first element is SQL and the second element is params. */ - function is_internal() { - return false; + protected function get_active_sessions_sql_data() { + global $CFG; + + $sql = "SELECT s.id, s.sid, s.userid, s.timecreated, s.timemodified, s.firstip, s.lastip + FROM {sessions} s + WHERE s.timemodified > :activebefore"; + + $params = array( + 'activebefore' => time() - $CFG->sessiontimeout, + ); + + return array($sql, $params); } /** - * Prints a form for configuring this authentication plugin. + * Return a record set of all active user sessions. * - * This function is called from admin/auth.php, and outputs a full page with - * a form for configuring this plugin. + * @return moodle_recordset A moodle_recordset instance of active sessions. + */ + public function get_active_sessions_recordset() { + global $DB; + $sqldata = $this->get_active_sessions_sql_data(); + + return $DB->get_recordset_sql($sqldata[0], $sqldata[1]); + } + + /** + * Return a number of currently active user sessions. * - * @param array $config An object containing all the data for this page. - * @param string $error - * @param array $user_fields - * @return void + * @return int A number of sessions. */ - function config_form($config, $error, $user_fields) { - include 'config.html'; + public function count_active_sessions() { + global $DB; + + $sqldata = $this->get_active_sessions_sql_data(); + + return $DB->count_records_select('sessions', 'timemodified > :activebefore', $sqldata[1]); } /** - * Updates the list of IPs and sends a notification by email. + * Check if provided user session should be killed. + * + * @param object $session A record from {sessions} table. * - * @param object $config configuration settings - * @return boolean always true. + * @return bool */ - function process_config($config) { + public function should_kill_session($session) { + global $USER; - global $CFG; + if ($session->userid == $USER->id) { + return false; + } - // set to defaults if undefined - if (!isset ($config->valid_ips)) { - $config->valid_ips = ''; + if (self::is_ip_in_list($this->config->valid_ips, $session->lastip)) { + return false; } - //saving new configuration settings - set_config('valid_ips', str_replace(' ', '', $config->valid_ips), 'auth_ip'); + return true; + } - //notify administrator for the settings changed for security. - mail($CFG->supportemail, get_string('auth_ipmailsubject', 'auth_ip'), - get_string('auth_ipmailtext', 'auth_ip').' : '.$config->valid_ips); + /** + * Kill all required active sessions. + * + * @param \progress_bar|null $progressbar Optional progress bar instance for using in UI. + */ + public function kill_active_sessions(progress_bar $progressbar = null) { + $sessions = $this->get_active_sessions_recordset(); + $sessionscount = $this->count_active_sessions(); - return true; + $done = 0; + $strinprogress = get_string('auth_iplogoutinprogress', 'auth_ip'); + + foreach ($sessions as $session) { + if ($this->should_kill_session($session)) { + \core\session\manager::kill_session($session->sid); + + if (!is_null($progressbar)) { + $done++; + $donepercent = floor(min($done, $sessionscount) / $sessionscount * 100); + $progressbar->update_full($donepercent, $strinprogress); + } + } + } + + $sessions->close(); + + if (!is_null($progressbar)) { + $progressbar->update_full(100, get_string('auth_iplogoutdone', 'auth_ip', $done)); + } } + } diff --git a/cli/check_before_logging.php b/cli/check_before_logging.php new file mode 100644 index 0000000..63eb1cf --- /dev/null +++ b/cli/check_before_logging.php @@ -0,0 +1,70 @@ +. + +/** + * CLI script to enable/disable "Check IP before logging in" setting. + * + * @package auth + * @subpackage ip + * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('CLI_SCRIPT', true); + +require(dirname(dirname(dirname(dirname(__FILE__)))).'/config.php'); +require_once($CFG->libdir.'/clilib.php'); + +list($options, $unrecognized) = cli_get_params( + array( + 'enable' => false, + 'disable' => false, + 'help' => false, + ), + array( + 'h' => 'help' + ) +); + +$status = !empty($options['enable']) ? 1 : 0; + +if ($unrecognized) { + $unrecognized = implode("\n ", $unrecognized); + cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); +} + + +if ($options['help'] || (empty($options['enable']) && empty($options['disable']))) { + $help = " +Command line script to enable/disable \"Check IP before logging in\" setting for auth/ip plugin. + +Options: +--enable Enable the setting +--disable Disable the setting +-h, --help Print out this help + +Example: +\$sudo -u www-data /usr/bin/php auth/ip/cli/check_before_logging.php --disable +"; + echo $help; + die; +} + +set_config('check_before_login', $status, 'auth_ip'); + +echo get_string('auth_ipclistatuschanged', 'auth_ip', $status)."\n"; + +exit(0); diff --git a/config.html b/config.html deleted file mode 100755 index 86b4240..0000000 --- a/config.html +++ /dev/null @@ -1,52 +0,0 @@ -. - -/** - * Configuration settings form - * - * @package auth - * @subpackage ip - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @author Robert Boloc - * @author Jordi Pujol-Ahulló - * @copyright 2013 onwards Servei de Recursos Educatius (http://www.sre.urv.cat) - */ - - -// set to defaults if undefined -if (!isset($config->valid_ips)) { - $config->valid_ips = ''; -} - -?> - - - - - - - -
- -
- -
- diff --git a/db/upgrade.php b/db/upgrade.php new file mode 100644 index 0000000..44a31be --- /dev/null +++ b/db/upgrade.php @@ -0,0 +1,58 @@ +. + +/** + * IP authentication plugin upgrade code. + * + * @package auth + * @subpackage ip + * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +/** + * Perform upgrade. + * + * @param int $oldversion the version we are upgrading from + * @return bool result + */ +function xmldb_auth_ip_upgrade($oldversion) { + + if ($oldversion < 2016070701) { + + $validips = get_config('auth_ip', 'valid_ips'); + + if (!empty($validips)) { + $updatedvalidips = ''; + $ips = explode(",", $validips); + + foreach ($ips as $ip) { + $ip = trim($ip); + + if (!empty($ip)) { + $updatedvalidips .= $ip . "\r\n"; + } + } + + set_config('valid_ips', $updatedvalidips, 'auth_ip'); + } + + upgrade_plugin_savepoint(true, 2016070700, 'auth', 'ip'); + } + + return true; +} diff --git a/lang/ca/auth_ip.php b/lang/ca/auth_ip.php deleted file mode 100644 index 47ab9a8..0000000 --- a/lang/ca/auth_ip.php +++ /dev/null @@ -1,33 +0,0 @@ -. - -/** - * Catalan strings - * - * @package auth - * @subpackage ip - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @author Robert Boloc - * @author Jordi Pujol-Ahulló - * @copyright 2013 onwards Servei de Recursos Educatius (http://www.sre.urv.cat) - */ - -$string['auth_ipdescription'] = 'Plugin d\'autenticació restringit per IP'; -$string['auth_ipexampleips'] = 'Llista d\'IPs separada per comes. Exemples: X.X.X.X o X.X.X.X,Y.Y.Y.Y'; -$string['auth_ipmailsubject'] = 'Plugin autenticació restringit per IP: IPs canviades'; -$string['auth_ipmailtext'] = 'S\'han actualitzat les IPs acceptades pel plugin d\'autenticació restringit per IP'; -$string['auth_ipvalidips'] = 'IPs vàlides'; -$string['pluginname'] = 'Autenticació per IP'; diff --git a/lang/en/auth_ip.php b/lang/en/auth_ip.php index 231f041..8294b00 100644 --- a/lang/en/auth_ip.php +++ b/lang/en/auth_ip.php @@ -25,9 +25,20 @@ * @copyright 2013 onwards Servei de Recursos Educatius (http://www.sre.urv.cat) */ -$string['auth_ipdescription'] = 'Auth plugin restricting login by the given IPs'; -$string['auth_ipexampleips'] = 'List of IPs in comma-separated format. Examples: X.X.X.X o X.X.X.X,Y.Y.Y.Y'; -$string['auth_ipmailsubject'] = 'IPs changed on authentication plugin by IP'; -$string['auth_ipmailtext'] = 'Accepted IPs for the authentication plugin by IP have been updated.'; +$string['auth_ipdescription'] = 'Put every entry on one line. Valid entries are either full IP address (such as 192.168.10.1) which matches a single host; or partial address (such as 192.168) which matches any address starting with those numbers; or CIDR notation (such as 231.54.211.0/20); or a range of IP addresses (such as 231.3.56.10-20) where the range applies to the last part of the address. Text domain names (like \'example.com\') are not supported. IPV6 address are not supported. Blank lines are not supported.'; $string['auth_ipvalidips'] = 'Valid IPs'; +$string['auth_ipcheckbeforelogin'] = 'Check IP before logging in'; +$string['auth_ipcheckbeforelogin_desc'] = 'If this setting is enabled then users will see error message before they saw a login page. You will be able to log out anyone currently logged in who\'s ip address doesn\'t match valid IPs.'; +$string['auth_iperrortext'] = 'Error text'; +$string['auth_iperrortext_desc'] = 'This text will be displayed to users if "Check IP before logging in" option is enabled.
Placeholders can be used: {$a}'; +$string['auth_ipcheckbeforelogindisabled'] = '"Check IP before logging in" setting is disabled. Please enable this settings and come back to the page.'; +$string['auth_iplogoutuserstext'] = '"Check IP before logging in" setting is enabled. You can log out anyone currently logged in who\'s ip address doesn\'t match valid IPs. Please use following link {$a}'; +$string['auth_iplogoutheading'] = 'Log out active users'; +$string['auth_iplogoutlink'] = 'Log out active users'; +$string['auth_iplogoutbutton'] = 'Log out active users'; +$string['auth_iplogoutinprogress'] = 'Logging out active users '; +$string['auth_iplogoutdone'] = 'Completed. Total users: {$a}.'; +$string['auth_iplogoutdescription'] = 'You can logout anyone currently logged in who\'s ip address doesn\'t match valid IPs. This will not affect your current user session.
Total number of all active users: {$a}'; +$string['auth_iplogoutwarning'] = 'Your IP {$a} is not in Valid IPs list. You will not be able to login once you are logged out.'; +$string['auth_ipclistatuschanged'] = 'The setting "Check IP before logging in" is set to {$a}'; $string['pluginname'] = 'Authentication by IP'; diff --git a/lang/es/auth_ip.php b/lang/es/auth_ip.php deleted file mode 100644 index bc2fa13..0000000 --- a/lang/es/auth_ip.php +++ /dev/null @@ -1,33 +0,0 @@ -. - -/** - * Spanish strings - * - * @package auth - * @subpackage ip - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @author Robert Boloc - * @author Jordi Pujol-Ahulló - * @copyright 2013 onwards Servei de Recursos Educatius (http://www.sre.urv.cat) - */ - -$string['auth_ipdescription'] = 'Plugin de autenticación restringido por IP'; -$string['auth_ipexampleips'] = 'Lista de IPs separada por comas. Ejemplos: X.X.X.X o X.X.X.X,Y.Y.Y.Y'; -$string['auth_ipmailsubject'] = 'Plugin de autenticación restringido por IP: IPs canviadas'; -$string['auth_ipmailtext'] = 'Se han actualizado las IPs aceptadas por el plugin de autenticación restringido por IP'; -$string['auth_ipvalidips'] = 'IPs válidas'; -$string['pluginname'] = 'Autenticación por IP'; diff --git a/renderer.php b/renderer.php new file mode 100644 index 0000000..18db2a8 --- /dev/null +++ b/renderer.php @@ -0,0 +1,135 @@ +. + +/** + * Renderer code. + * + * @package auth + * @subpackage ip + * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +class auth_ip_renderer extends plugin_renderer_base { + /** + * Render an error message. + * + * @param string $message Error message HTML text. + * + * @return string HTML to display. + */ + public function render_error_message($message) { + $html = ''; + + $html .= $this->output->box_start(); + $html .= $this->replace_placeholders($message); + $html .= $this->output->box_end(); + + return $html; + } + + /** + * Return a list placeholders with their values. + * + * @return array An array of placeholders. + */ + public function get_placeholders_data() { + $config = get_config('auth_ip'); + + return array( + '[[valid_ips]]' => isset($config->valid_ips) ? $config->valid_ips : '', + '[[your_ip]]' => getremoteaddr(), + ); + } + + /** + * Replace placeholders by related values in the provided message. + * + * @param string $message A message HTML text. + * + * @return string A message after replacing the placeholders. + */ + public function replace_placeholders($message) { + $placeholders = array_keys($this->get_placeholders_data()); + $values = array_values($this->get_placeholders_data()); + + $message = str_replace($placeholders, $values, $message); + + return $message; + } + + /** + * Get a link to a page for logging out all active users. + * + * @return string HTML link. + */ + public function get_user_logout_page_link() { + $url = new moodle_url('/auth/ip/users.php'); + $link = html_writer::link($url, get_string('auth_iplogoutlink', 'auth_ip')); + + return $link; + } + + /** + * Get description for the Log out active users page. + * + * @return string + */ + public function get_settings_user_logout_page_link_description() { + return $this->output->notification( + get_string('auth_iplogoutuserstext', 'auth_ip', $this->get_user_logout_page_link()), + 'info'); + } + + /** + * Get a error message to display. + * + * @return string + */ + public function get_your_ip_not_in_range_error_message() { + $html = ''; + + $config = get_config('auth_ip'); + $ip = getremoteaddr(); + $list = isset($config->valid_ips) ? $config->valid_ips : ''; + + if (!auth_plugin_ip::is_ip_in_list($list, $ip)) { + $html = $this->output->notification( + get_string('auth_iplogoutwarning', 'auth_ip', $ip), + 'error' + ); + } + + return $html; + } + + /** + * Get description for the Log out active users page. + * + * @return string + */ + public function get_user_logout_description($activeusers) { + $html = ''; + $html .= html_writer::tag('p', get_string('auth_iplogoutdescription', 'auth_ip', $activeusers), + array('class' => 'auth-ip-logout-descr')); + + return $html; + } + +} diff --git a/settings.php b/settings.php new file mode 100644 index 0000000..b14a91e --- /dev/null +++ b/settings.php @@ -0,0 +1,62 @@ +. + +/** + * Configuration settings form + * + * @package auth + * @subpackage ip + * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$url = new moodle_url('/auth/ip/users.php'); +$ADMIN->add('authsettings', new admin_externalpage('auth_ip_sessions', + get_string('auth_iplogoutheading', 'auth_ip'), $url->out(), 'moodle/site:config', true)); + +if ($ADMIN->fulltree) { + + require_once($CFG->dirroot . '/auth/ip/renderer.php'); + require_once($CFG->dirroot . '/auth/ip/auth.php'); + + $renderer = $PAGE->get_renderer('auth_ip'); + + $options = array(get_string('no'), get_string('yes')); + $placeholders = implode(', ', array_keys($renderer->get_placeholders_data())); + $auth = new auth_plugin_ip(); + + if (isset($auth->config->check_before_login) && !empty($auth->config->check_before_login)) { + $settings->add(new admin_setting_heading('auth_ip/warning', '', + $renderer->get_your_ip_not_in_range_error_message())); + + $settings->add(new admin_setting_heading('auth_ip/logoutuserstext', '', + $renderer->get_settings_user_logout_page_link_description())); + } + + $settings->add(new admin_setting_configiplist('auth_ip/valid_ips', + new lang_string('auth_ipvalidips', 'auth_ip'), + new lang_string('auth_ipdescription', 'auth_ip'), '')); + + $settings->add(new admin_setting_configselect('auth_ip/check_before_login', + get_string('auth_ipcheckbeforelogin', 'auth_ip'), + get_string('auth_ipcheckbeforelogin_desc', 'auth_ip'), 0, $options)); + + $settings->add(new admin_setting_confightmleditor('auth_ip/error_text', + new lang_string('auth_iperrortext', 'auth_ip'), + new lang_string('auth_iperrortext_desc', 'auth_ip', $placeholders), '')); +} diff --git a/tests/ip_test.php b/tests/ip_test.php index 58b4344..7c31098 100644 --- a/tests/ip_test.php +++ b/tests/ip_test.php @@ -18,79 +18,397 @@ global $CFG; require_once($CFG->dirroot.'/auth/ip/auth.php'); +require_once($CFG->dirroot.'/auth/ip/renderer.php'); class auth_ip_testcase extends advanced_testcase { + /** + * An instance of auth_plugin_ip class. + * @var object + */ protected $authplugin; + /** + * An instance of auth_ip_renderer class. + * @var auth_ip_renderer + */ + protected $renderer; + protected function setUp() { + global $PAGE; + $this->authplugin = new auth_plugin_ip(); + $this->renderer = $PAGE->get_renderer('auth_ip'); + $this->resetAfterTest(true); } /** - * Test that valid IPs or IPs in range are detected as valid. + * Test that error message displayed correctly if check_before_login is enabled. * - * @dataProvider is_ip_valid_data_provider + * @dataProvider should_not_display_error_data_provider * - * @param array $valid_ips + * @param array $ips * @param string $ip - * @param boolean $is_valid + * @param boolean $display */ - public function test_is_ip_valid_detects_valid_ips($valid_ips, $ip, $is_valid) { - $this->authplugin->config->valid_ips = $valid_ips; + public function test_should_display_error_if_check_before_login_enabled($ips, $ip, $display) { + $this->authplugin->config->valid_ips = $ips; + $this->authplugin->config->check_before_login = true; + + $_SERVER['HTTP_CLIENT_IP'] = $ip; $this->assertEquals( - $is_valid, - $this->authplugin->is_ip_valid($ip) + $display, + $this->authplugin->should_display_error() + ); + + $this->authplugin->config->check_before_login = false; + + $this->assertFalse( + $this->authplugin->should_display_error() ); } /** + * Test that error message are not displayed in any cases if check_before_login is disabled. + * + * @dataProvider should_not_display_error_data_provider + * + * @param array $ips + * @param string $ip + * @param boolean $display + */ + public function test_should_not_display_error_if_check_before_login_disabled($ips, $ip, $display) { + $this->authplugin->config->valid_ips = $ips; + $this->authplugin->config->check_before_login = false; + + $_SERVER['HTTP_CLIENT_IP'] = $ip; + + $this->assertFalse( + $this->authplugin->should_display_error() + ); + } + + /** + * A list of data to test displaying an error message against. + * * @return array */ - public static function is_ip_valid_data_provider() { + public static function should_not_display_error_data_provider() { return array( - array('192.168.1.1,192.168.1.2', '192.168.1.1', true), - array('192.168.1.1,192.168.1.2', '192.168.1.3', false), - array('192.168.0.0/24', '192.168.0.200', true), - array('111.112.0.0/12,96.0.0.0/6', '192.168.0.200', false), - array('111.112.0.0/12,96.0.0.0/6', '99.255.255.254', true), - array('111.112.0.0/12,10.40.22.0/24,96.0.0.0/6', '10.40.22.50', true), - array('111.112.0.0/12, 10.40.22.0/24, 96.0.0.0/6', '10.40.22.50', true), - array(' 111.112.0.0 ', '111.112.0.0', true), // Extra spaces for testing + array("192.168.1.1\n192.168.1.2", '192.168.1.1', false), + array("192.168.1.1\n192.168.1.2", '192.168.1.3', true), + array("192.168.0.0/24", '192.168.0.200', false), + array("111.112.0.0/12\n96.0.0.0/6", '192.168.0.200', true), + array("111.112.0.0/12\n96.0.0.0/6", '99.255.255.254', false), + array("111.112.0.0/12\n10.40.22.0/24\n96.0.0.0/6", '10.40.22.50', false), + array("111.112.0.0/12\n 10.40.22.0/24\n 96.0.0.0/6", '10.40.22.50', false), + array(" 111.112.0.0 ", '111.112.0.0', false), + array("192.168.1.1-200", '192.168.1.50', false), + array("192.168.1.1-200", '192.168.1.201', true), ); } /** - * Test range detection + * Test the plugin is not marked as internal. + */ + public function test_is_not_internal() { + $this->assertFalse($this->authplugin->is_internal()); + } + + /** + * Test that placeholder data is correct. + */ + public function test_placeholders_data() { + $placeholders = $this->renderer->get_placeholders_data(); + + $this->assertTrue(is_array($placeholders)); + $this->assertEquals(2, count($placeholders)); + $this->assertTrue(array_key_exists('[[valid_ips]]', $placeholders)); + $this->assertTrue(array_key_exists('[[your_ip]]', $placeholders)); + } + + /** + * Test valid_ips placeholder if valid_ips configuration is not set. + */ + public function test_valid_ips_placeholder_if_config_is_not_set() { + $placeholders = $this->renderer->get_placeholders_data(); + + $expected = ''; + $actual = $placeholders['[[valid_ips]]']; + + $this->assertEquals($expected, $actual); + } + + /** + * Test valid_ips placeholder if valid_ips configuration is set. + */ + public function test_valid_ips_placeholder_if_config_is_set() { + set_config('valid_ips', '192.168.1.1, 192.168.1.2', 'auth_ip'); + $placeholders = $this->renderer->get_placeholders_data(); + + $expected = '192.168.1.1, 192.168.1.2'; + $actual = $placeholders['[[valid_ips]]']; + + $this->assertEquals($expected, $actual); + } + + /** + * Test your_ip placeholder value. + */ + public function test_your_ip_placeholder_value() { + $_SERVER['HTTP_CLIENT_IP'] = '192.168.1.2'; + $placeholders = $this->renderer->get_placeholders_data(); + + $expected = '192.168.1.2'; + $actual = $placeholders['[[your_ip]]']; + + $this->assertEquals($expected, $actual); + } + + /** + * Test that renderer can replace placeholders in a message. + */ + public function test_render_replace_placeholders() { + set_config('valid_ips', '192.168.1.1', 'auth_ip'); + $_SERVER['HTTP_CLIENT_IP'] = '192.168.1.3'; + $message = "Your IP is [[your_ip]] [[your_ip]] and valid ips are [[valid_ips]] [[valid_ips]]. Other placeholder [[*]] [[vi]] [[ip]]"; + + $expected = "Your IP is 192.168.1.3 192.168.1.3 and valid ips are 192.168.1.1 192.168.1.1. Other placeholder [[*]] [[vi]] [[ip]]"; + $actual = $this->renderer->replace_placeholders($message); + + $this->assertEquals($expected, $actual); + } + + /** + * Test that render display an error message. + */ + public function test_render_displays_error_message() { + $expected = "
Test error message
"; + $actual = $this->renderer->render_error_message('Test error message'); + + $this->assertEquals($expected, $actual); + } + + /** + * A list of data to test displaying an error message against. * - * @dataProvider is_cidr_data_provider + * @return array + */ + public static function is_ip_in_list_data_provider() { + return array( + array("192.168.1.1\n192.168.1.2", '192.168.1.1', true), + array("192.168.1.1\n192.168.1.2", '192.168.1.3', false), + array("192.168.0.0/24", '192.168.0.200', true), + array("111.112.0.0/12\n96.0.0.0/6", '192.168.0.200', false), + array("111.112.0.0/12\n96.0.0.0/6", '99.255.255.254', true), + array("111.112.0.0/12\n10.40.22.0/24\n96.0.0.0/6", '10.40.22.50', true), + array("111.112.0.0/12\n 10.40.22.0/24\n 96.0.0.0/6", '10.40.22.50', true), + array(" 111.112.0.0 ", '111.112.0.0', true), + array("192.168.1.1-200", '192.168.1.50', true), + array("192.168.1.1-200", '192.168.1.201', false), + ); + } + + /** + * Check that we function is_ip_in_list() can check if ip is in list. + * + * @dataProvider is_ip_in_list_data_provider * - * @param string $ip_or_cidr - * @param boolean $is_cidr + * @param array $ips + * @param string $ip + * @param boolean $inlist */ - public function test_is_cidr_detects_cidrs($ip_or_cidr, $is_cidr) { + public function test_is_ip_in_list_function($ips, $ip, $inlist) { $this->assertEquals( - $this->authplugin->is_cidr($ip_or_cidr), - $is_cidr + $inlist, + auth_plugin_ip::is_ip_in_list($ips, $ip) ); } + + public function generate_sessions() { + global $CFG, $DB, $USER; + + $this->setAdminUser(); + $adminid = $USER->id; + $this->setGuestUser(); + $guestid = $USER->id; + $user1 = $this->getDataGenerator()->create_user(); + + $this->setUser(0); + + $CFG->sessiontimeout = 60 * 10; + + $record = new \stdClass(); + $record->state = 0; + $record->firstip = $record->lastip = '192.168.1.1'; + + // Admin active. + $record->sid = md5('session1'); + $record->sessdata = null; + $record->userid = $adminid; + $record->timecreated = time() - 60 * 60; + $record->timemodified = time() - 30; + $DB->insert_record('sessions', $record); + + // Admin not active. + $record->sid = md5('session2'); + $record->userid = $adminid; + $record->timecreated = time() - 60 * 60; + $record->timemodified = time() - 60 * 20; + $DB->insert_record('sessions', $record); + + // Guest active. + $record->sid = md5('session3'); + $record->userid = $guestid; + $record->timecreated = time() - 60 * 60; + $record->timemodified = time() - 30; + $DB->insert_record('sessions', $record); + + // Guest not active. + $record->sid = md5('session4'); + $record->userid = $guestid; + $record->timecreated = time() - 60 * 60; + $record->timemodified = time() - 60 * 20; + $DB->insert_record('sessions', $record); + + // Regular user active. + $record->sid = md5('session5'); + $record->userid = $user1->id; + $record->timecreated = time() - 60 * 60; + $record->timemodified = time() - 30; + $DB->insert_record('sessions', $record); + + // Regular user not active. + $record->sid = md5('session6'); + $record->userid = $user1->id; + $record->timecreated = time() - 60 * 60; + $record->timemodified = time() - 60 * 20; + $DB->insert_record('sessions', $record); + + // Current user active. + $record->sid = md5('session7'); + $record->userid = 0; + $record->timecreated = time() - 60 * 60; + $record->timemodified = time() - 30; + $DB->insert_record('sessions', $record); + + // Current user not active. + $record->sid = md5('session8'); + $record->userid = 0; + $record->timecreated = time() - 60 * 60; + $record->timemodified = time() - 60 * 20; + $DB->insert_record('sessions', $record); + } + /** + * Test that the plugin can retrieve all current session from DB. + */ + public function test_can_get_all_active_sessions() { + global $DB; + + $this->resetAfterTest(); + + $this->generate_sessions(); + + $r1 = $DB->get_record('sessions', array('sid' => md5('session1'))); + $r3 = $DB->get_record('sessions', array('sid' => md5('session3'))); + $r5 = $DB->get_record('sessions', array('sid' => md5('session5'))); + $r7 = $DB->get_record('sessions', array('sid' => md5('session7'))); + + $activesessions = $this->authplugin->get_active_sessions_recordset(); + + $actuallist = array(); + foreach ($activesessions as $session) { + $actuallist[$session->id] = $session; + } + + $activesessions->close(); + + $this->assertEquals(4, count($actuallist)); + $this->assertTrue(array_key_exists($r1->id, $actuallist)); + $this->assertTrue(array_key_exists($r3->id, $actuallist)); + $this->assertTrue(array_key_exists($r5->id, $actuallist)); + $this->assertTrue(array_key_exists($r7->id, $actuallist)); + } + + /** + * Data provided for test of should_kill_session(). + * * @return array */ - public static function is_cidr_data_provider() { + public function should_kill_session_data_provider() { return array( - array('192.168.1.1', false), - array('192.168.1.1/24', true), - array('10.15.1.1', false), - array('10.15.1.1/2', true), + array(0, '192.168.1.1', '192.168.1.1-100', false), + array(0, '192.168.1.101', '192.168.1.1-100', false), + array(2, '192.168.1.2', '192.168.1.1-100', false), + array(2, '192.168.1.200', '192.168.1.1-100', true), ); } /** - * Test the plugin is not marked as internal. + * Test that should_kill_session() function works as expected. + * + * @dataProvider should_kill_session_data_provider + * + * @param $userid + * @param $ip + * @param $ips + * @param $expected */ - public function test_is_not_internal() { - $this->assertFalse($this->authplugin->is_internal()); + public function test_should_kill_session_($userid, $ip, $ips, $expected) { + $this->resetAfterTest(); + $this->authplugin->config->valid_ips = $ips; + $this->setUser(0); + + $session = new \stdClass(); + $session->userid = $userid; + $session->firstip = $session->lastip = $ip; + + $actual = $this->authplugin->should_kill_session($session); + $this->assertEquals($expected, $actual); + } + + /** + * Test that the plugin kills only active sessions and keeps current user's session. + */ + public function test_kill_only_active_sessions_but_keep_current_user() { + global $DB; + + $this->resetAfterTest(); + $this->generate_sessions(); + $this->authplugin->config->valid_ips = '192.168.1.5'; + + $this->authplugin->kill_active_sessions(); + + $sessions = $DB->get_records('sessions', array(), '', 'sid, id, userid'); + + $this->assertEquals(5, count($sessions)); + + $this->assertFalse(array_key_exists(md5('session1'), $sessions)); + $this->assertFalse(array_key_exists(md5('session3'), $sessions)); + $this->assertFalse(array_key_exists(md5('session5'), $sessions)); + + $this->assertTrue(array_key_exists(md5('session2'), $sessions)); + $this->assertTrue(array_key_exists(md5('session4'), $sessions)); + $this->assertTrue(array_key_exists(md5('session6'), $sessions)); + $this->assertTrue(array_key_exists(md5('session7'), $sessions)); + $this->assertTrue(array_key_exists(md5('session8'), $sessions)); } + + /** + * Test that the plugin can count all active sessions. + */ + public function test_count_of_active_sessions() { + $this->resetAfterTest(); + $this->generate_sessions(); + + $expected = 4; + $actual = $this->authplugin->count_active_sessions(); + + $this->assertTrue(is_int($actual)); + $this->assertEquals($expected, $actual); + + } + } diff --git a/users.php b/users.php new file mode 100644 index 0000000..51f01df --- /dev/null +++ b/users.php @@ -0,0 +1,69 @@ +. + +/** + * Removing active user sessions. + * + * @package auth + * @subpackage ip + * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('NO_OUTPUT_BUFFERING', true); + +require(dirname((dirname(dirname(__FILE__)))) . '/config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once('auth.php'); + +$action = optional_param('action', '', PARAM_ALPHA); + +require_login(null, false); +require_capability('moodle/site:config', context_system::instance()); + +$auth = new auth_plugin_ip(); + +if (!$auth->config->check_before_login) { + $settingsurl = new moodle_url('/admin/settings.php', array('section' => 'authsettingip')); + print_error('auth_ipcheckbeforelogindisabled', 'auth_ip', $settingsurl->out()); +} + +admin_externalpage_setup('auth_ip_sessions'); + +$output = $PAGE->get_renderer('auth_ip'); + +echo $output->header(); +echo $output->heading(get_string('auth_iplogoutheading', 'auth_ip')); + +if ($action === 'remove') { + require_sesskey(); + $PAGE->set_cacheable(false); + $progressbar = new progress_bar(); + $progressbar->create(); + core_php_time_limit::raise(HOURSECS); + raise_memory_limit(MEMORY_EXTRA); + $auth->kill_active_sessions($progressbar); + echo $output->continue_button(new moodle_url('/admin/settings.php', array('section' => 'authsettingip')), 'get'); + echo $output->footer(); + exit; +} else { + echo $output->get_your_ip_not_in_range_error_message(); + echo $output->get_user_logout_description($auth->count_active_sessions()); + echo $output->single_button(new moodle_url($PAGE->url, array('action' => 'remove')), get_string('auth_iplogoutbutton', 'auth_ip'), 'post'); + echo $output->footer(); + exit; +} + diff --git a/version.php b/version.php index 0d63cfd..7126467 100644 --- a/version.php +++ b/version.php @@ -27,9 +27,9 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2015072300; // The current plugin version (Date: YYYYMMDDXX) -$plugin->requires = 2012120300; // Requires this Moodle version (2.4) +$plugin->version = 2016070701; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2015051100; // Requires this Moodle version (2.4) $plugin->component = 'auth_ip'; // Full name of the plugin (used for diagnostics) $plugin->maturity = MATURITY_STABLE; -$plugin->release = '1.2 (Build: 2015072300)'; +$plugin->release = '2.0 (Build: 2016070700)';