Skip to content

Commit

Permalink
Fix issue #5 - add an option to prevent auth for all users if ip fail…
Browse files Browse the repository at this point in the history
…s whitelist.
  • Loading branch information
dmitriim committed Jul 14, 2016
1 parent c5b0f4b commit 39e5bb8
Show file tree
Hide file tree
Showing 14 changed files with 1,003 additions and 223 deletions.
67 changes: 67 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -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

8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down
232 changes: 169 additions & 63 deletions auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand All @@ -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));
}
}

}
Loading

0 comments on commit 39e5bb8

Please sign in to comment.