Skip to content

Commit

Permalink
MDL-35238 Implement deployment authorization
Browse files Browse the repository at this point in the history
The caller of the mdeploy.php utility is expected to create a file in
the data directory. The name of such file and the passphrase in it are
then sent to mdeploy.php as a part of the request. The submitted and
stored values are then compared.
  • Loading branch information
mudrd8mz committed Nov 8, 2012
1 parent 6aa2e28 commit 3daedb5
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 8 deletions.
60 changes: 58 additions & 2 deletions lib/pluginlib.php
Original file line number Diff line number Diff line change
Expand Up @@ -1589,6 +1589,8 @@ public function make_execution_widget(available_update_info $info) {
throw new coding_exception('Unknown plugin type root location', $plugintype);
}

list($passfile, $password) = $this->prepare_authorization();

$params = array(
'upgrade' => true,
'type' => $plugintype,
Expand All @@ -1597,8 +1599,8 @@ public function make_execution_widget(available_update_info $info) {
'download' => $info->download,
'dataroot' => $CFG->dataroot,
'dirroot' => $CFG->dirroot,
'passfile' => '', // TODO
'password' => '', // TODO
'passfile' => $passfile,
'password' => $password,
);

$widget = new single_button(
Expand Down Expand Up @@ -1698,6 +1700,42 @@ public function __call($name, array $arguments = array()) {
}
}

/**
* Generates a random token and stores it in a file in moodledata directory.
*
* @return array of the (string)filename and (string)password in this order
*/
public function prepare_authorization() {
global $CFG;

make_upload_directory('mdeploy/auth/');

$attempts = 0;
$success = false;

while (!$success and $attempts < 5) {
$attempts++;

$passfile = $this->generate_passfile();
$password = $this->generate_password();
$now = time();

$filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;

if (!file_exists($filepath)) {
$success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
}
}

if ($success) {
return array($passfile, $password);

} else {
throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
}
}


// End of external API

/**
Expand Down Expand Up @@ -1753,6 +1791,24 @@ protected function params_to_data(array $params) {

return $data;
}

/**
* Returns a random string to be used as a filename of the password storage.
*
* @return string
*/
protected function generate_passfile() {
return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
}

/**
* Returns a random string to be used as the authorization token
*
* @return string
*/
protected function generate_password() {
return complex_random_string();
}
}


Expand Down
24 changes: 23 additions & 1 deletion lib/tests/pluginlib_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -545,15 +545,37 @@ class testable_available_update_checker_cron_executed extends Exception {
}


/**
* Modified {@link available_update_deployer} suitable for testing purposes
*/
class testable_available_update_deployer extends available_update_deployer {

}


/**
* Test cases for {@link available_update_deployer} class
*/
class available_update_deployer_test extends advanced_testcase {

public function test_magic_setters() {
$deployer = available_update_deployer::instance();
$deployer = testable_available_update_deployer::instance();
$value = new moodle_url('/');
$deployer->set_returnurl($value);
$this->assertSame($deployer->get_returnurl(), $value);
}

public function test_prepare_authorization() {
global $CFG;

$deployer = testable_available_update_deployer::instance();
list($passfile, $password) = $deployer->prepare_authorization();
$filename = $CFG->phpunit_dataroot.'/mdeploy/auth/'.$passfile;
$this->assertFileExists($filename);
$stored = file($filename, FILE_IGNORE_NEW_LINES);
$this->assertEquals(count($stored), 2);
$this->assertGreaterThan(23, strlen($stored[0]));
$this->assertSame($stored[0], $password);
$this->assertTrue(time() - (int)$stored[1] < 60);
}
}
29 changes: 24 additions & 5 deletions mdeploy.php
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ class input_http_provider extends input_provider {
* @return array of raw values passed via HTTP request
*/
protected function parse_raw_options() {
return $_GET; // TODO switch to $_POST
return $_POST;
}
}

Expand Down Expand Up @@ -629,15 +629,34 @@ protected function authorize() {
$passfile = $this->input->get_option('passfile');
$password = $this->input->get_option('password');

$passpath = $dataroot.'/temp/mdeploy/'.$passfile;
$passpath = $dataroot.'/mdeploy/auth/'.$passfile;

if (!is_readable($passpath)) {
throw new unauthorized_access_exception('Unable to read passphrase file.');
throw new unauthorized_access_exception('Unable to read the passphrase file.');
}

$stored = file_get_contents($passpath);
$stored = file($passpath, FILE_IGNORE_NEW_LINES);

if ($password !== $stored) {
// "This message will self-destruct in five seconds." -- Mission Commander Swanbeck, Mission: Impossible II
unlink($passpath);

if (is_readable($passpath)) {
throw new unauthorized_access_exception('Unable to remove the passphrase file.');
}

if (count($stored) < 2) {
throw new unauthorized_access_exception('Invalid format of the passphrase file.');
}

if (time() - (int)$stored[1] > 30 * 60) {
throw new unauthorized_access_exception('Passphrase timeout.');
}

if (strlen($stored[0]) < 24) {
throw new unauthorized_access_exception('Session passphrase not long enough.');
}

if ($password !== $stored[0]) {
throw new unauthorized_access_exception('Session passphrase does not match the stored one.');
}
}
Expand Down

0 comments on commit 3daedb5

Please sign in to comment.