mirror of
https://github.com/catalyst/moodle-auth_outage.git
synced 2026-05-17 05:48:43 +02:00
Merge pull request #341 from catalyst/access-key
[#340] Access key exclusion method
This commit is contained in:
35
README.md
35
README.md
@@ -1,16 +1,21 @@
|
||||

|
||||
|
||||
# Moodle Outage manager plugin
|
||||
* [Version Support](#version-support)
|
||||
* [What is this?](#what-is-this)
|
||||
* [Moodle Requirements](#moodle-requirements)
|
||||
* [Screenshots](#screenshots)
|
||||
* [Installation](#installation)
|
||||
* [Theme configuration](#theme-configuration)
|
||||
* [How to use](#how-to-use)
|
||||
* [Quick Guide](#quick-guide)
|
||||
* [Why is it an auth plugin?](#why-it-is-an-auth-plugin)
|
||||
* [Feedback and issues](#feedback-and-issues)
|
||||
- [Moodle Outage manager plugin](#moodle-outage-manager-plugin)
|
||||
- [What is this?](#what-is-this)
|
||||
- [Moodle Requirements](#moodle-requirements)
|
||||
- [Branches](#branches)
|
||||
- [Screenshots](#screenshots)
|
||||
- [Installation](#installation)
|
||||
- [Theme configuration](#theme-configuration)
|
||||
- [Custom Theme Additional SCSS](#custom-theme-additional-scss)
|
||||
- [How to use](#how-to-use)
|
||||
- [Quick Guide](#quick-guide)
|
||||
- [Why it is an auth plugin?](#why-it-is-an-auth-plugin)
|
||||
- [Tester restriction options](#tester-restriction-options)
|
||||
- [IP restriction](#ip-restriction)
|
||||
- [Access key](#access-key)
|
||||
- [Feedback and issues](#feedback-and-issues)
|
||||
|
||||
What is this?
|
||||
-------------
|
||||
@@ -178,6 +183,16 @@ Why it is an auth plugin?
|
||||
|
||||
One of the graduated stages this plugin introduces is a 'tester only' mode which disables login for most normal users. This is conceptually similar to the maintenance mode but enables testers to login and confirm the state after an upgrade without needing full admin privileges.
|
||||
|
||||
Tester restriction options
|
||||
------------
|
||||
Two options are available to restrict the site to only let testers in during the tester phase.
|
||||
Note: these restrictions build on each other; If both are enabled, users must meet both criteria to be allowed in.
|
||||
|
||||
## IP restriction
|
||||
Only allow users from a certain IP or range of ips to enter.
|
||||
## Access key
|
||||
Users provide an access key in the URL params on first page load, which is then stored as a cookie for 24 hours. If the access key matches the one setup for the outage, they are allowed in.
|
||||
|
||||
|
||||
Feedback and issues
|
||||
-------------------
|
||||
|
||||
@@ -77,13 +77,15 @@ if (!empty($_SERVER['REQUEST_URI'])) {
|
||||
$url = $path.'/auth/outage/info.php';
|
||||
$outageinfo = strpos($_SERVER['REQUEST_URI'], $url) === 0 ? true : false;
|
||||
}
|
||||
|
||||
$allowed = !file_exists($CFG->dataroot.'/climaintenance.php') // Not in maintenance mode.
|
||||
|| (defined('ABORT_AFTER_CONFIG') && ABORT_AFTER_CONFIG) // Only config requested.
|
||||
|| (defined('CLI_SCRIPT') && CLI_SCRIPT) // Allow CLI scripts.
|
||||
|| $outageinfo // Allow outage info requests.
|
||||
|| (defined('NO_AUTH_OUTAGE') && NO_AUTH_OUTAGE); // Allow any page should not be blocked by maintenance mode.
|
||||
if (!$allowed) {
|
||||
// Call the climaintenance.php which will check for allowed IPs.
|
||||
// Call the climaintenance.php which will check for the conditions
|
||||
// that have been baked into it from the frontend (ip, accesskey, etc...).
|
||||
$CFG->dirroot = dirname(dirname(dirname(__FILE__))); // It is not defined yet but the script below needs it.
|
||||
require($CFG->dataroot.'/climaintenance.php'); // This call may terminate the script here or not.
|
||||
}
|
||||
|
||||
@@ -75,6 +75,14 @@ class edit extends moodleform {
|
||||
$mform->addElement('static', 'usagehints', '', get_string('textplaceholdershint', 'auth_outage'));
|
||||
$mform->addElement('static', 'warningreenablemaintenancemode', '');
|
||||
|
||||
$mform->addElement('advcheckbox', 'useaccesskey', get_string('useaccesskey', 'auth_outage'),
|
||||
get_string('useaccesskey:desc', 'auth_outage'), 0);
|
||||
|
||||
$mform->addElement('text', 'accesskey', get_string('accesskey', 'auth_outage'));
|
||||
$mform->setType('accesskey', PARAM_TEXT);
|
||||
$mform->disabledIf('accesskey', 'useaccesskey');
|
||||
$mform->addHelpButton('accesskey', 'accesskey', 'auth_outage');
|
||||
|
||||
$this->add_action_buttons();
|
||||
}
|
||||
|
||||
@@ -128,6 +136,7 @@ class edit extends moodleform {
|
||||
'warntime' => $data->starttime - $data->warningduration,
|
||||
'title' => $data->title,
|
||||
'description' => $data->description['text'],
|
||||
'accesskey' => $data->useaccesskey ? $data->accesskey : null,
|
||||
];
|
||||
return new outage($outagedata);
|
||||
}
|
||||
@@ -151,6 +160,8 @@ class edit extends moodleform {
|
||||
'warningduration' => $outage->get_warning_duration(),
|
||||
'title' => $outage->title,
|
||||
'description' => ['text' => $outage->description, 'format' => '1'],
|
||||
'accesskey' => $outage->accesskey,
|
||||
'useaccesskey' => !empty($outage->accesskey),
|
||||
]);
|
||||
|
||||
// If the default_autostart is configured in config, then force autostart to be the default value.
|
||||
|
||||
@@ -108,6 +108,11 @@ class outage {
|
||||
*/
|
||||
public $lastmodified = null;
|
||||
|
||||
/**
|
||||
* @var string|null access key, or null if not enabled.
|
||||
*/
|
||||
public $accesskey = null;
|
||||
|
||||
/**
|
||||
* outage constructor.
|
||||
* @param stdClass|array|null $data The data for the outage.
|
||||
|
||||
@@ -279,57 +279,103 @@ class outagelib {
|
||||
* @param int $starttime Outage start time.
|
||||
* @param int $stoptime Outage stop time.
|
||||
* @param string $allowedips List of IPs allowed.
|
||||
* @param string|null $accesskey access key, or null if no access key set.
|
||||
*
|
||||
* @return string
|
||||
* @throws invalid_parameter_exception
|
||||
*/
|
||||
public static function create_climaintenancephp_code($starttime, $stoptime, $allowedips) {
|
||||
public static function create_climaintenancephp_code($starttime, $stoptime, $allowedips, $accesskey = null) {
|
||||
global $CFG;
|
||||
if (!is_int($starttime) || !is_int($stoptime)) {
|
||||
throw new invalid_parameter_exception('Make sure $startime and $stoptime are integers.');
|
||||
}
|
||||
if (!is_string($allowedips) || (trim($allowedips) == '')) {
|
||||
throw new invalid_parameter_exception('$allowedips must be a valid string.');
|
||||
}
|
||||
// I know Moodle validation would clean up this field, but just in case, let's ensure no
|
||||
// single-quotes (and double for the sake of it) are present otherwise it would break the code.
|
||||
$allowedips = addslashes($allowedips);
|
||||
|
||||
$cookiesecure = is_moodle_cookie_secure();
|
||||
|
||||
// Since Moodle 4.3 cookiehttponly is default to true and this CFG is not set.
|
||||
// so if not set, default to true.
|
||||
$cookiehttponly = isset($CFG->cookiehttponly) ? (bool) $CFG->cookiehttponly : true;
|
||||
|
||||
$code = <<<'EOT'
|
||||
<?php
|
||||
if ((time() >= {{STARTTIME}}) && (time() < {{STOPTIME}})) {
|
||||
define('MOODLE_INTERNAL', true);
|
||||
if (!defined('MOODLE_INTERNAL')) {
|
||||
define('MOODLE_INTERNAL', true);
|
||||
}
|
||||
require_once($CFG->dirroot.'/lib/moodlelib.php');
|
||||
if (file_exists($CFG->dirroot.'/lib/classes/ip_utils.php')) {
|
||||
require_once($CFG->dirroot.'/lib/classes/ip_utils.php');
|
||||
}
|
||||
if (!remoteip_in_list('{{ALLOWEDIPS}}')) {
|
||||
header($_SERVER['SERVER_PROTOCOL'] . ' 503 Moodle under maintenance');
|
||||
header('Status: 503 Moodle under maintenance');
|
||||
header('Retry-After: 300');
|
||||
header('Content-type: text/html; charset=utf-8');
|
||||
header('X-UA-Compatible: IE=edge');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Cache-Control: post-check=0, pre-check=0', false);
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: Mon, 20 Aug 1969 09:23:00 GMT');
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
|
||||
header('Accept-Ranges: none');
|
||||
header('X-Moodle-Maintenance: manager');
|
||||
if ((defined('AJAX_SCRIPT') && AJAX_SCRIPT) || (defined('WS_SERVER') && WS_SERVER)) {
|
||||
// Put access key as a cookie if given. This stops the need to put it as a url param on every request.
|
||||
$urlaccesskey = optional_param('accesskey', null, PARAM_TEXT);
|
||||
|
||||
if (!empty($urlaccesskey)) {
|
||||
setcookie('auth_outage_accesskey', $urlaccesskey, time() + 86400, '/', '', {{COOKIESECURE}}, {{COOKIEHTTPONLY}});
|
||||
}
|
||||
|
||||
// Use url access key if given, else the cookie, else null.
|
||||
$useraccesskey = $urlaccesskey ?: $_COOKIE['auth_outage_accesskey'] ?? null;
|
||||
|
||||
$ipblocked = !remoteip_in_list('{{ALLOWEDIPS}}');
|
||||
$accesskeyblocked = $useraccesskey != '{{ACCESSKEY}}';
|
||||
$blocked = ({{USEACCESSKEY}} && $accesskeyblocked) || ({{USEALLOWEDIPS}} && $ipblocked);
|
||||
$isphpunit = defined('PHPUNIT_TEST');
|
||||
|
||||
if ($blocked) {
|
||||
if (!$isphpunit) {
|
||||
header($_SERVER['SERVER_PROTOCOL'] . ' 503 Moodle under maintenance');
|
||||
header('Status: 503 Moodle under maintenance');
|
||||
header('Retry-After: 300');
|
||||
header('Content-type: text/html; charset=utf-8');
|
||||
header('X-UA-Compatible: IE=edge');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Cache-Control: post-check=0, pre-check=0', false);
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: Mon, 20 Aug 1969 09:23:00 GMT');
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
|
||||
header('Accept-Ranges: none');
|
||||
header('X-Moodle-Maintenance: manager');
|
||||
}
|
||||
|
||||
if (!$isphpunit && ((defined('AJAX_SCRIPT') && AJAX_SCRIPT) || (defined('WS_SERVER') && WS_SERVER))) {
|
||||
exit(0);
|
||||
}
|
||||
echo '<!-- Blocked by ip, your ip: '.getremoteaddr('n/a').' -->';
|
||||
if (file_exists($CFG->dataroot.'/climaintenance.template.html')) {
|
||||
require($CFG->dataroot.'/climaintenance.template.html');
|
||||
exit(0);
|
||||
|
||||
if ({{USEALLOWEDIPS}} && $ipblocked) {
|
||||
echo '<!-- Blocked by ip, your ip: '.getremoteaddr('n/a').' -->';
|
||||
}
|
||||
|
||||
if ({{USEALLOWEDIPS}} && !$ipblocked) {
|
||||
echo '<!-- Your IP is allowed: '.getremoteaddr('n/a').' -->';
|
||||
}
|
||||
|
||||
if ({{USEACCESSKEY}} && $accesskeyblocked) {
|
||||
echo '<!-- Blocked by missing or incorrect access key, access key given: '. $useraccesskey .' -->';
|
||||
}
|
||||
|
||||
if ({{USEACCESSKEY}} && !$accesskeyblocked) {
|
||||
echo '<!-- Your access key is allowed: '. $useraccesskey .' -->';
|
||||
}
|
||||
|
||||
if (!$isphpunit) {
|
||||
if (file_exists($CFG->dataroot.'/climaintenance.template.html')) {
|
||||
require($CFG->dataroot.'/climaintenance.template.html');
|
||||
exit(0);
|
||||
}
|
||||
// The file above should always exist, but just in case...
|
||||
die('We are currently under maintentance, please try again later.');
|
||||
}
|
||||
// The file above should always exist, but just in case...
|
||||
die('We are currently under maintentance, please try again later.');
|
||||
}
|
||||
}
|
||||
EOT;
|
||||
$search = ['{{STARTTIME}}', '{{STOPTIME}}', '{{ALLOWEDIPS}}', '{{YOURIP}}'];
|
||||
$replace = [$starttime, $stoptime, $allowedips, getremoteaddr('n/a')];
|
||||
$search = ['{{STARTTIME}}', '{{STOPTIME}}', '{{USEALLOWEDIPS}}', '{{ALLOWEDIPS}}', '{{USEACCESSKEY}}', '{{ACCESSKEY}}',
|
||||
'{{YOURIP}}', '{{COOKIESECURE}}', '{{COOKIEHTTPONLY}}'];
|
||||
// Note that var_export is required because (string) false == '', not 'false'.
|
||||
$replace = [$starttime, $stoptime, var_export(!empty($allowedips), true), $allowedips, var_export(!empty($accesskey), true),
|
||||
$accesskey, getremoteaddr('n/a'), var_export($cookiesecure, true), var_export($cookiehttponly, true)];
|
||||
return str_replace($search, $replace, $code);
|
||||
}
|
||||
|
||||
@@ -351,13 +397,15 @@ EOT;
|
||||
|
||||
$config = self::get_config();
|
||||
$allowedips = trim($config->allowedips);
|
||||
$accesskey = $outage->accesskey ?? null;
|
||||
|
||||
if (is_null($outage) || ($allowedips == '')) {
|
||||
// If no outage, or allowed ips is null and access key is null (i.e. no blocking required).
|
||||
if (is_null($outage) || ($allowedips == '' && empty($accesskey))) {
|
||||
if (file_exists($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
} else {
|
||||
$code = self::create_climaintenancephp_code($outage->starttime, $outage->stoptime, $allowedips);
|
||||
$code = self::create_climaintenancephp_code($outage->starttime, $outage->stoptime, $allowedips, $accesskey);
|
||||
|
||||
$dir = dirname($file);
|
||||
if (!file_exists($dir) || !is_dir($dir)) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<FIELD NAME="modifiedby" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Who last modified this entry."/>
|
||||
<FIELD NAME="lastmodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="When was this entry last modified."/>
|
||||
<FIELD NAME="finished" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Timestamp of when the outage really finished."/>
|
||||
<FIELD NAME="accesskey" TYPE="char" LENGTH="16" NOTNULL="false" SEQUENCE="false" COMMENT="Unique key used to access during outage"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
|
||||
@@ -47,5 +47,20 @@ function xmldb_auth_outage_upgrade($oldversion) {
|
||||
upgrade_plugin_savepoint(true, 2016092200, 'auth', 'outage');
|
||||
}
|
||||
|
||||
if ($oldversion < 2024081900) {
|
||||
|
||||
// Define field accesskey to be added to auth_outage.
|
||||
$table = new xmldb_table('auth_outage');
|
||||
$field = new xmldb_field('accesskey', XMLDB_TYPE_CHAR, '16', null, null, null, null, 'finished');
|
||||
|
||||
// Conditionally launch add field accesskey.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Outage savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2024081900, 'auth', 'outage');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -103,9 +103,9 @@ $string['infostartofwarning'] = 'start of warning';
|
||||
$string['infostaticpage'] = 'static page';
|
||||
$string['infopagestaticgenerated'] = 'This warning was generated on {$a->time}.';
|
||||
$string['ips_combine'] = 'The IPs listed above will be combined with the IPs listed below.';
|
||||
$string['allowedipsempty'] = 'When the allowed IPs list is empty we will not block anyone. You can add your own IP address (<i>{$a->ip}</i>) and block all other IPs.';
|
||||
$string['allowedipshasmyip'] = 'Your IP (<i>{$a->ip}</i>) is in the list and you will not be blocked out during an Outage.';
|
||||
$string['allowedipshasntmyip'] = 'Your IP (<i>{$a->ip}</i>) is not in the list and you will be blocked out during an outage.';
|
||||
$string['allowedipsempty'] = 'No one will be blocked by IP because the list is empty. You can add your own IP address (<i>{$a->ip}</i>) and block all other IPs. IP blocking is in addition to access key blocking (if setup in outage)';
|
||||
$string['allowedipshasmyip'] = 'Your IP (<i>{$a->ip}</i>) is in the list and your IP will not be blocked out during an Outage.';
|
||||
$string['allowedipshasntmyip'] = 'Your IP (<i>{$a->ip}</i>) is not in the list and your IP will be blocked out during an outage.';
|
||||
$string['allowedipsnoconfig'] = 'Your config.php does not have the extra setup to allow blocking via IP.<br />Please refer to our <a href="https://github.com/catalyst/moodle-auth_outage#installation" target="_blank">README.md</a> file for more information.';
|
||||
$string['logformaintmodeconfig'] = 'Update maintenance mode configuration.';
|
||||
$string['logformaintmodeconfigcomplete'] = 'Updating maintenance mode configuration complete.';
|
||||
@@ -162,6 +162,10 @@ $string['warningdurationerrorinvalid'] = 'Warning duration must be positive.';
|
||||
$string['warningduration'] = 'Warning duration';
|
||||
$string['warningduration_help'] = 'How long before the start of the outage should the warning be displayed.';
|
||||
$string['warningreenablemaintenancemode'] = 'Please note that saving this outage will re-enable maintenance mode.<br />Untick "Auto start maintenance mode" if you want to prevent this.';
|
||||
$string['accesskey'] = 'Access key';
|
||||
$string['accesskey_help'] = 'Testers should pass the access key initially in the url parameters e.g. ?accesskey=xyz. This will then be stored in a cookie for 24 hours, during which the url parameter will not be necessary.<br /><b>Note:</b> the access key is in addition to any IP restrictions setup.';
|
||||
$string['useaccesskey'] = 'Use access key';
|
||||
$string['useaccesskey:desc'] = 'Require testers to access site during outage by providing the access key below';
|
||||
|
||||
/*
|
||||
* Privacy provider (GDPR)
|
||||
|
||||
@@ -308,43 +308,84 @@ class outagelib_test extends \auth_outage\base_testcase {
|
||||
* Test create maintenance php code
|
||||
*/
|
||||
public function test_createmaintenancephpcode() {
|
||||
global $CFG;
|
||||
$CFG->cookiehttponly = false;
|
||||
|
||||
$expected = <<<'EOT'
|
||||
<?php
|
||||
if ((time() >= 123) && (time() < 456)) {
|
||||
define('MOODLE_INTERNAL', true);
|
||||
if (!defined('MOODLE_INTERNAL')) {
|
||||
define('MOODLE_INTERNAL', true);
|
||||
}
|
||||
require_once($CFG->dirroot.'/lib/moodlelib.php');
|
||||
if (file_exists($CFG->dirroot.'/lib/classes/ip_utils.php')) {
|
||||
require_once($CFG->dirroot.'/lib/classes/ip_utils.php');
|
||||
}
|
||||
if (!remoteip_in_list('hey\'\"you
|
||||
// Put access key as a cookie if given. This stops the need to put it as a url param on every request.
|
||||
$urlaccesskey = optional_param('accesskey', null, PARAM_TEXT);
|
||||
|
||||
if (!empty($urlaccesskey)) {
|
||||
setcookie('auth_outage_accesskey', $urlaccesskey, time() + 86400, '/', '', true, false);
|
||||
}
|
||||
|
||||
// Use url access key if given, else the cookie, else null.
|
||||
$useraccesskey = $urlaccesskey ?: $_COOKIE['auth_outage_accesskey'] ?? null;
|
||||
|
||||
$ipblocked = !remoteip_in_list('hey\'\"you
|
||||
a.b.c.d
|
||||
e.e.e.e/20')) {
|
||||
header($_SERVER['SERVER_PROTOCOL'] . ' 503 Moodle under maintenance');
|
||||
header('Status: 503 Moodle under maintenance');
|
||||
header('Retry-After: 300');
|
||||
header('Content-type: text/html; charset=utf-8');
|
||||
header('X-UA-Compatible: IE=edge');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Cache-Control: post-check=0, pre-check=0', false);
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: Mon, 20 Aug 1969 09:23:00 GMT');
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
|
||||
header('Accept-Ranges: none');
|
||||
header('X-Moodle-Maintenance: manager');
|
||||
if ((defined('AJAX_SCRIPT') && AJAX_SCRIPT) || (defined('WS_SERVER') && WS_SERVER)) {
|
||||
e.e.e.e/20');
|
||||
$accesskeyblocked = $useraccesskey != '12345';
|
||||
$blocked = (true && $accesskeyblocked) || (true && $ipblocked);
|
||||
$isphpunit = defined('PHPUNIT_TEST');
|
||||
|
||||
if ($blocked) {
|
||||
if (!$isphpunit) {
|
||||
header($_SERVER['SERVER_PROTOCOL'] . ' 503 Moodle under maintenance');
|
||||
header('Status: 503 Moodle under maintenance');
|
||||
header('Retry-After: 300');
|
||||
header('Content-type: text/html; charset=utf-8');
|
||||
header('X-UA-Compatible: IE=edge');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Cache-Control: post-check=0, pre-check=0', false);
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: Mon, 20 Aug 1969 09:23:00 GMT');
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
|
||||
header('Accept-Ranges: none');
|
||||
header('X-Moodle-Maintenance: manager');
|
||||
}
|
||||
|
||||
if (!$isphpunit && ((defined('AJAX_SCRIPT') && AJAX_SCRIPT) || (defined('WS_SERVER') && WS_SERVER))) {
|
||||
exit(0);
|
||||
}
|
||||
echo '<!-- Blocked by ip, your ip: '.getremoteaddr('n/a').' -->';
|
||||
if (file_exists($CFG->dataroot.'/climaintenance.template.html')) {
|
||||
require($CFG->dataroot.'/climaintenance.template.html');
|
||||
exit(0);
|
||||
|
||||
if (true && $ipblocked) {
|
||||
echo '<!-- Blocked by ip, your ip: '.getremoteaddr('n/a').' -->';
|
||||
}
|
||||
|
||||
if (true && !$ipblocked) {
|
||||
echo '<!-- Your IP is allowed: '.getremoteaddr('n/a').' -->';
|
||||
}
|
||||
|
||||
if (true && $accesskeyblocked) {
|
||||
echo '<!-- Blocked by missing or incorrect access key, access key given: '. $useraccesskey .' -->';
|
||||
}
|
||||
|
||||
if (true && !$accesskeyblocked) {
|
||||
echo '<!-- Your access key is allowed: '. $useraccesskey .' -->';
|
||||
}
|
||||
|
||||
if (!$isphpunit) {
|
||||
if (file_exists($CFG->dataroot.'/climaintenance.template.html')) {
|
||||
require($CFG->dataroot.'/climaintenance.template.html');
|
||||
exit(0);
|
||||
}
|
||||
// The file above should always exist, but just in case...
|
||||
die('We are currently under maintentance, please try again later.');
|
||||
}
|
||||
// The file above should always exist, but just in case...
|
||||
die('We are currently under maintentance, please try again later.');
|
||||
}
|
||||
}
|
||||
EOT;
|
||||
$found = outagelib::create_climaintenancephp_code(123, 456, "hey'\"you\na.b.c.d\ne.e.e.e/20");
|
||||
$found = outagelib::create_climaintenancephp_code(123, 456, "hey'\"you\na.b.c.d\ne.e.e.e/20", '12345');
|
||||
self::assertSame($expected, $found);
|
||||
}
|
||||
|
||||
@@ -357,44 +398,84 @@ EOT;
|
||||
public function test_createmaintenancephpcode_withoutage($configkey) {
|
||||
global $CFG;
|
||||
$this->resetAfterTest(true);
|
||||
$CFG->cookiehttponly = false;
|
||||
|
||||
$expected = <<<'EOT'
|
||||
<?php
|
||||
if ((time() >= 123) && (time() < 456)) {
|
||||
define('MOODLE_INTERNAL', true);
|
||||
if (!defined('MOODLE_INTERNAL')) {
|
||||
define('MOODLE_INTERNAL', true);
|
||||
}
|
||||
require_once($CFG->dirroot.'/lib/moodlelib.php');
|
||||
if (file_exists($CFG->dirroot.'/lib/classes/ip_utils.php')) {
|
||||
require_once($CFG->dirroot.'/lib/classes/ip_utils.php');
|
||||
}
|
||||
if (!remoteip_in_list('127.0.0.1')) {
|
||||
header($_SERVER['SERVER_PROTOCOL'] . ' 503 Moodle under maintenance');
|
||||
header('Status: 503 Moodle under maintenance');
|
||||
header('Retry-After: 300');
|
||||
header('Content-type: text/html; charset=utf-8');
|
||||
header('X-UA-Compatible: IE=edge');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Cache-Control: post-check=0, pre-check=0', false);
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: Mon, 20 Aug 1969 09:23:00 GMT');
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
|
||||
header('Accept-Ranges: none');
|
||||
header('X-Moodle-Maintenance: manager');
|
||||
if ((defined('AJAX_SCRIPT') && AJAX_SCRIPT) || (defined('WS_SERVER') && WS_SERVER)) {
|
||||
// Put access key as a cookie if given. This stops the need to put it as a url param on every request.
|
||||
$urlaccesskey = optional_param('accesskey', null, PARAM_TEXT);
|
||||
|
||||
if (!empty($urlaccesskey)) {
|
||||
setcookie('auth_outage_accesskey', $urlaccesskey, time() + 86400, '/', '', true, false);
|
||||
}
|
||||
|
||||
// Use url access key if given, else the cookie, else null.
|
||||
$useraccesskey = $urlaccesskey ?: $_COOKIE['auth_outage_accesskey'] ?? null;
|
||||
|
||||
$ipblocked = !remoteip_in_list('127.0.0.1');
|
||||
$accesskeyblocked = $useraccesskey != '5678';
|
||||
$blocked = (true && $accesskeyblocked) || (true && $ipblocked);
|
||||
$isphpunit = defined('PHPUNIT_TEST');
|
||||
|
||||
if ($blocked) {
|
||||
if (!$isphpunit) {
|
||||
header($_SERVER['SERVER_PROTOCOL'] . ' 503 Moodle under maintenance');
|
||||
header('Status: 503 Moodle under maintenance');
|
||||
header('Retry-After: 300');
|
||||
header('Content-type: text/html; charset=utf-8');
|
||||
header('X-UA-Compatible: IE=edge');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Cache-Control: post-check=0, pre-check=0', false);
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: Mon, 20 Aug 1969 09:23:00 GMT');
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
|
||||
header('Accept-Ranges: none');
|
||||
header('X-Moodle-Maintenance: manager');
|
||||
}
|
||||
|
||||
if (!$isphpunit && ((defined('AJAX_SCRIPT') && AJAX_SCRIPT) || (defined('WS_SERVER') && WS_SERVER))) {
|
||||
exit(0);
|
||||
}
|
||||
echo '<!-- Blocked by ip, your ip: '.getremoteaddr('n/a').' -->';
|
||||
if (file_exists($CFG->dataroot.'/climaintenance.template.html')) {
|
||||
require($CFG->dataroot.'/climaintenance.template.html');
|
||||
exit(0);
|
||||
|
||||
if (true && $ipblocked) {
|
||||
echo '<!-- Blocked by ip, your ip: '.getremoteaddr('n/a').' -->';
|
||||
}
|
||||
|
||||
if (true && !$ipblocked) {
|
||||
echo '<!-- Your IP is allowed: '.getremoteaddr('n/a').' -->';
|
||||
}
|
||||
|
||||
if (true && $accesskeyblocked) {
|
||||
echo '<!-- Blocked by missing or incorrect access key, access key given: '. $useraccesskey .' -->';
|
||||
}
|
||||
|
||||
if (true && !$accesskeyblocked) {
|
||||
echo '<!-- Your access key is allowed: '. $useraccesskey .' -->';
|
||||
}
|
||||
|
||||
if (!$isphpunit) {
|
||||
if (file_exists($CFG->dataroot.'/climaintenance.template.html')) {
|
||||
require($CFG->dataroot.'/climaintenance.template.html');
|
||||
exit(0);
|
||||
}
|
||||
// The file above should always exist, but just in case...
|
||||
die('We are currently under maintentance, please try again later.');
|
||||
}
|
||||
// The file above should always exist, but just in case...
|
||||
die('We are currently under maintentance, please try again later.');
|
||||
}
|
||||
}
|
||||
EOT;
|
||||
$outage = new outage([
|
||||
'starttime' => 123,
|
||||
'stoptime' => 456,
|
||||
'accesskey' => '5678',
|
||||
]);
|
||||
$file = $CFG->dataroot.'/climaintenance.php';
|
||||
set_config($configkey, '127.0.0.1', 'auth_outage');
|
||||
@@ -414,15 +495,16 @@ EOT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test create maintenance php code without IPs
|
||||
* Test create maintenance php code without IPs or accesskey
|
||||
*/
|
||||
public function test_createmaintenancephpcode_withoutips() {
|
||||
public function test_createmaintenancephpcode_withoutips_or_accesskey() {
|
||||
global $CFG;
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$outage = new outage([
|
||||
'starttime' => 123,
|
||||
'stoptime' => 456,
|
||||
'accesskey' => null,
|
||||
]);
|
||||
$file = $CFG->dataroot.'/climaintenance.php';
|
||||
set_config('allowedips', '', 'auth_outage');
|
||||
@@ -591,4 +673,140 @@ EOT;
|
||||
\core\session\manager::gc(); // Remove stale sessions.
|
||||
\core_plugin_manager::reset_caches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides values to test_evaluation_maintenancepage
|
||||
* @return array
|
||||
*/
|
||||
public static function evaluation_maintenancepage_provider(): array {
|
||||
$allowedipout = '<!-- Your IP is allowed:';
|
||||
$blockedipout = '<!-- Blocked by ip, your ip:';
|
||||
$allowedaccesskeyout = '<!-- Your access key is allowed:';
|
||||
$blockedaccesskeyout = '<!-- Blocked by missing or incorrect access key, access key given:';
|
||||
|
||||
return [
|
||||
'ip allowed, no access key setup' => [
|
||||
'allowedips' => '127.0.0.1',
|
||||
'iptouse' => '127.0.0.1',
|
||||
'accesskey' => null,
|
||||
'accesskeytouse' => null,
|
||||
'expectedoutputs' => [],
|
||||
],
|
||||
'ip not allowed, no access key setup' => [
|
||||
'allowedips' => '5.5.5.5',
|
||||
'iptouse' => '127.0.0.1',
|
||||
'accesskey' => null,
|
||||
'accesskeytouse' => null,
|
||||
'expectedoutputs' => [$blockedipout],
|
||||
],
|
||||
'access key incorrect, no ip setup' => [
|
||||
'allowedips' => null,
|
||||
'iptouse' => null,
|
||||
'accesskey' => '12345',
|
||||
'accesskeytouse' => 'wrong',
|
||||
'expectedoutputs' => [$blockedaccesskeyout],
|
||||
],
|
||||
'access key correct, no ip setup' => [
|
||||
'allowedips' => null,
|
||||
'iptouse' => null,
|
||||
'accesskey' => '12345',
|
||||
'accesskeytouse' => '12345',
|
||||
'expectedoutputs' => [],
|
||||
],
|
||||
'access key correct, ip incorrect' => [
|
||||
'allowedips' => '127.0.0.1',
|
||||
'iptouse' => '5.5.5.5',
|
||||
'accesskey' => '12345',
|
||||
'accesskeytouse' => '12345',
|
||||
'expectedoutputs' => [$allowedaccesskeyout, $blockedipout],
|
||||
],
|
||||
'access key incorrect, ip correct' => [
|
||||
'allowedips' => '127.0.0.1',
|
||||
'iptouse' => '127.0.0.1',
|
||||
'accesskey' => '12345',
|
||||
'accesskeytouse' => 'wrong',
|
||||
'expectedoutputs' => [$blockedaccesskeyout, $allowedipout],
|
||||
],
|
||||
'access key correct, ip correct' => [
|
||||
'allowedips' => '127.0.0.1',
|
||||
'iptouse' => '127.0.0.1',
|
||||
'accesskey' => '12345',
|
||||
'accesskeytouse' => '12345',
|
||||
'expectedoutputs' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the evaluation logic of the generated maintenance page.
|
||||
*
|
||||
* @param string|null $allowedips config to set as allowed ips - null to not set
|
||||
* @param string|null $iptouse ip to 'fake' as the remote ip, or null to not set.
|
||||
* @param string|null $accesskey config to set as the access key in the outage - null to not set
|
||||
* @param string|null $accesskeytouse access key to pass in as fake url params - null to not set
|
||||
* @param array $expectedoutputs expected output strings, if empty will test that the output was also empty.
|
||||
*
|
||||
* @dataProvider evaluation_maintenancepage_provider
|
||||
*
|
||||
* We need this because we modify the request headers,
|
||||
* see https://github.com/sebastianbergmann/phpunit/issues/720#issuecomment-10421092
|
||||
* @runInSeparateProcess
|
||||
*/
|
||||
public function test_evaluation_maintenancepage(?string $allowedips, ?string $iptouse, ?string $accesskey,
|
||||
?string $accesskeytouse, array $expectedoutputs) {
|
||||
|
||||
global $CFG, $_SERVER, $_GET;
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
self::setAdminUser();
|
||||
$now = time();
|
||||
$outage = new outage([
|
||||
'autostart' => false,
|
||||
'warntime' => $now - 200,
|
||||
'starttime' => $now - 100,
|
||||
'stoptime' => $now + 200,
|
||||
'title' => 'Title',
|
||||
'description' => 'Description',
|
||||
'accesskey' => $accesskey,
|
||||
]);
|
||||
|
||||
if (!is_null($allowedips)) {
|
||||
set_config('allowedips', $allowedips, 'auth_outage');
|
||||
}
|
||||
// Ensure if the file exists we clean it (e.g. from a previous test run).
|
||||
$file = $CFG->dataroot.'/climaintenance.php';
|
||||
if (file_exists($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
|
||||
// This basically sets the output of getremoteaddr().
|
||||
if (!is_null($iptouse)) {
|
||||
$_SERVER['REMOTE_ADDR'] = $iptouse;
|
||||
}
|
||||
|
||||
// This sets the output of optional_param().
|
||||
if (!is_null($accesskeytouse)) {
|
||||
$_GET['accesskey'] = $accesskeytouse;
|
||||
}
|
||||
|
||||
outagelib::update_climaintenance_code($outage);
|
||||
self::assertFileExists($file);
|
||||
|
||||
// Require the file to execute it.
|
||||
// Normally this would die, but we have baked some goodies in there
|
||||
// that stop it die'ing during a unit test.
|
||||
ob_start();
|
||||
require($file);
|
||||
$contents = ob_get_clean();
|
||||
|
||||
// Check each output is as expected.
|
||||
foreach ($expectedoutputs as $expectedoutput) {
|
||||
$this->assertStringContainsString($expectedoutput, $contents);
|
||||
}
|
||||
|
||||
// Ensure if nothing was expected, that it is empty.
|
||||
if (empty($expectedoutputs)) {
|
||||
$this->assertEmpty($contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = "auth_outage";
|
||||
$plugin->version = 2024052400; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->release = 2024052400; // Human-readable release information.
|
||||
$plugin->version = 2024081900; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->release = 2024081900; // Human-readable release information.
|
||||
$plugin->requires = 2017111309; // 2017111309 = T13, but this really requires 3.9 and higher.
|
||||
$plugin->maturity = MATURITY_STABLE; // Suitable for PRODUCTION environments!
|
||||
$plugin->supported = [39, 404]; // A range of branch numbers of supported moodle versions.
|
||||
|
||||
Reference in New Issue
Block a user