diff options
author | dizzy <diosmosis@users.noreply.github.com> | 2021-04-24 06:28:08 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-24 06:28:08 +0300 |
commit | 6227cb05197d4dfd0aa0d695eb665b6c1ef455d6 (patch) | |
tree | 3d100156c4d2c0effc981ebb8c041a840ee76ac1 /plugins/CorePluginsAdmin | |
parent | d1422903bb698fac80e1ab0590d62c63dd6574bf (diff) |
Require password confirmation for more plugin operations. (#17345)
* Require password confirmation for more plugin operations.
* renormalize
* add optional password confirmation to CorePluginsAdmin.setSystemSettings
* Add developer changelog entry.
* ask for password confirmation when saving plugin settings and use onOpenEnd materializecss modal event handler instead of ready since ready no longer exists in used version
* Fix redirectTo==referrer for other plugin actions that now have password confirmation.
* fix build
* try fixing build again
Diffstat (limited to 'plugins/CorePluginsAdmin')
7 files changed, 231 insertions, 8 deletions
diff --git a/plugins/CorePluginsAdmin/API.php b/plugins/CorePluginsAdmin/API.php index 1de34393d4..5b4450b52e 100644 --- a/plugins/CorePluginsAdmin/API.php +++ b/plugins/CorePluginsAdmin/API.php @@ -7,9 +7,12 @@ */ namespace Piwik\Plugins\CorePluginsAdmin; +use Piwik\Common; use Piwik\Piwik; use Piwik\Plugin\SettingsProvider; use Exception; +use Piwik\Plugins\Login\PasswordVerifier; +use Piwik\Version; /** * API for plugin CorePluginsAdmin @@ -28,10 +31,16 @@ class API extends \Piwik\Plugin\API */ private $settingsProvider; - public function __construct(SettingsProvider $settingsProvider, SettingsMetadata $settingsMetadata) + /** + * @var PasswordVerifier + */ + private $passwordVerifier; + + public function __construct(SettingsProvider $settingsProvider, SettingsMetadata $settingsMetadata, PasswordVerifier $passwordVerifier) { $this->settingsProvider = $settingsProvider; $this->settingsMetadata = $settingsMetadata; + $this->passwordVerifier = $passwordVerifier; } /** @@ -39,10 +48,15 @@ class API extends \Piwik\Plugin\API * @param array $settingValues Format: array('PluginName' => array(array('name' => 'SettingName1', 'value' => 'SettingValue1), ..)) * @throws Exception */ - public function setSystemSettings($settingValues) + public function setSystemSettings($settingValues, $passwordConfirmation = false) { Piwik::checkUserHasSuperUserAccess(); + $skipPasswordConfirm = $passwordConfirmation === false && version_compare(Version::VERSION, '4.4.0-b1', '<'); + if (!$skipPasswordConfirm) { + $this->confirmCurrentUserPassword($passwordConfirmation); + } + $pluginsSettings = $this->settingsProvider->getAllSystemSettings(); $this->settingsMetadata->setPluginSettings($pluginsSettings, $settingValues); @@ -110,4 +124,17 @@ class API extends \Piwik\Plugin\API return $this->settingsMetadata->formatSettings($userSettings); } + private function confirmCurrentUserPassword($passwordConfirmation) + { + if (empty($passwordConfirmation)) { + throw new Exception(Piwik::translate('UsersManager_ConfirmWithPassword')); + } + + $passwordConfirmation = Common::unsanitizeInputValue($passwordConfirmation); + + $loginCurrentUser = Piwik::getCurrentUserLogin(); + if (!$this->passwordVerifier->isPasswordCorrect($loginCurrentUser, $passwordConfirmation)) { + throw new Exception(Piwik::translate('UsersManager_CurrentPasswordNotCorrect')); + } + } } diff --git a/plugins/CorePluginsAdmin/Controller.php b/plugins/CorePluginsAdmin/Controller.php index df3f1de171..3501de4737 100644 --- a/plugins/CorePluginsAdmin/Controller.php +++ b/plugins/CorePluginsAdmin/Controller.php @@ -425,9 +425,23 @@ class Controller extends Plugin\ControllerAdmin public function activate($redirectAfter = true) { - $pluginName = $this->initPluginModification(static::ACTIVATE_NONCE); $this->dieIfPluginsAdminIsDisabled(); + $params = [ + 'module' => 'CorePluginsAdmin', + 'action' => 'activate', + 'pluginName' => Common::getRequestVar('pluginName'), + 'nonce' => Common::getRequestVar('nonce'), + 'redirectTo' => Common::getRequestVar('redirectTo'), + 'referrer' => urlencode(Url::getReferrer()), + ]; + + if (!$this->passwordVerify->requirePasswordVerifiedRecently($params)) { + return; + } + + $pluginName = $this->initPluginModification(static::ACTIVATE_NONCE); + $this->pluginManager->activatePlugin($pluginName); if ($redirectAfter) { @@ -469,6 +483,18 @@ class Controller extends Plugin\ControllerAdmin public function deactivate($redirectAfter = true) { + $params = [ + 'module' => 'CorePluginsAdmin', + 'action' => 'deactivate', + 'pluginName' => Common::getRequestVar('pluginName'), + 'nonce' => Common::getRequestVar('nonce'), + 'redirectTo' => Common::getRequestVar('redirectTo'), + 'referrer' => urlencode(Url::getReferrer()), + ]; + if (!$this->passwordVerify->requirePasswordVerifiedRecently($params)) { + return; + } + if($this->isAllowedToTroubleshootAsSuperUser()) { Access::doAsSuperUser(function() use ($redirectAfter) { $this->doDeactivatePlugin($redirectAfter); @@ -480,9 +506,21 @@ class Controller extends Plugin\ControllerAdmin public function uninstall($redirectAfter = true) { - $pluginName = $this->initPluginModification(static::UNINSTALL_NONCE); $this->dieIfPluginsAdminIsDisabled(); + $params = [ + 'module' => 'CorePluginsAdmin', + 'action' => 'uninstall', + 'pluginName' => Common::getRequestVar('pluginName'), + 'nonce' => Common::getRequestVar('nonce'), + 'referrer' => urlencode(Url::getReferrer()), + ]; + if (!$this->passwordVerify->requirePasswordVerifiedRecently($params)) { + return; + } + + $pluginName = $this->initPluginModification(static::UNINSTALL_NONCE); + $uninstalled = $this->pluginManager->uninstallPlugin($pluginName); if (!$uninstalled) { @@ -552,7 +590,17 @@ class Controller extends Plugin\ControllerAdmin protected function redirectAfterModification($redirectAfter) { - if ($redirectAfter) { + if (!$redirectAfter) { + return; + } + + $referrer = Common::getRequestVar('referrer', false); + $referrer = Common::unsanitizeInputValue($referrer); + if (!empty($referrer) + && Url::isLocalUrl($referrer) + ) { + Url::redirectToUrl($referrer); + } else { Url::redirectToReferrer(); } } diff --git a/plugins/CorePluginsAdmin/angularjs/plugin-settings/plugin-settings.controller.js b/plugins/CorePluginsAdmin/angularjs/plugin-settings/plugin-settings.controller.js index 01bb921004..5806abe3a3 100644 --- a/plugins/CorePluginsAdmin/angularjs/plugin-settings/plugin-settings.controller.js +++ b/plugins/CorePluginsAdmin/angularjs/plugin-settings/plugin-settings.controller.js @@ -7,15 +7,17 @@ (function () { angular.module('piwikApp').controller('PluginSettingsController', PluginSettingsController); - PluginSettingsController.$inject = ['$scope', 'piwikApi']; + PluginSettingsController.$inject = ['$scope', 'piwikApi', '$element']; - function PluginSettingsController($scope, piwikApi) { + function PluginSettingsController($scope, piwikApi, $element) { // remember to keep controller very simple. Create a service/factory (model) if needed var self = this; this.isLoading = true; this.isSaving = {}; + this.passwordConfirmation = ''; + this.settingsToSave = null; var apiMethod = 'CorePluginsAdmin.getUserSettings'; @@ -34,6 +36,27 @@ var apiMethod = 'CorePluginsAdmin.setUserSettings'; if ($scope.mode === 'admin') { apiMethod = 'CorePluginsAdmin.setSystemSettings'; + + if (!this.passwordConfirmation) { + this.settingsToSave = settings; + + function onEnter(event){ + var keycode = (event.keyCode ? event.keyCode : event.which); + if (keycode == '13'){ + $element.find('.confirm-password-modal').modal('close'); + self.save(); + } + } + + $element.find('.confirm-password-modal').modal({ dismissible: false, onOpenEnd: function () { + $('.modal.open #currentUserPassword').focus(); + $('.modal.open #currentUserPassword').off('keypress').keypress(onEnter); + }}).modal('open'); + + return; + } else { + settings = this.settingsToSave; + } } this.isSaving[settings.pluginName] = true; @@ -56,7 +79,7 @@ }); }); - piwikApi.post({method: apiMethod}, {settingValues: values}).then(function (success) { + piwikApi.post({method: apiMethod}, {settingValues: values, passwordConfirmation: this.passwordConfirmation}).then(function (success) { self.isSaving[settings.pluginName] = false; var UI = require('piwik/UI'); @@ -69,6 +92,9 @@ }, function () { self.isSaving[settings.pluginName] = false; }); + + this.passwordConfirmation = ''; + this.settingsToSave = null; }; } })(); diff --git a/plugins/CorePluginsAdmin/angularjs/plugin-settings/plugin-settings.directive.html b/plugins/CorePluginsAdmin/angularjs/plugin-settings/plugin-settings.directive.html index 6f77711ecc..4e082e94bf 100644 --- a/plugins/CorePluginsAdmin/angularjs/plugin-settings/plugin-settings.directive.html +++ b/plugins/CorePluginsAdmin/angularjs/plugin-settings/plugin-settings.directive.html @@ -22,4 +22,20 @@ </div> + <div class="confirm-password-modal modal"> + <div class="modal-content"> + <h2>{{:: 'UsersManager_ConfirmWithPassword'|translate }}</h2> + + <div piwik-field uicontrol="password" name="currentUserPassword" autocomplete="off" + ng-model="pluginSettings.passwordConfirmation" + full-width="true" + title="{{:: 'UsersManager_YourCurrentPassword'|translate }}" + value=""> + </div> + </div> + <div class="modal-footer"> + <a href="" class="modal-action modal-close btn" ng-disabled="!pluginSettings.passwordConfirmation" ng-click="pluginSettings.save()">{{:: 'General_Yes'|translate }}</a> + <a href="" class="modal-action modal-close modal-no">{{:: 'General_No'|translate }}</a> + </div> + </div> </div> diff --git a/plugins/CorePluginsAdmin/angularjs/plugin-settings/plugin-settings.directive.less b/plugins/CorePluginsAdmin/angularjs/plugin-settings/plugin-settings.directive.less index 3b86a59aba..affc515e84 100644 --- a/plugins/CorePluginsAdmin/angularjs/plugin-settings/plugin-settings.directive.less +++ b/plugins/CorePluginsAdmin/angularjs/plugin-settings/plugin-settings.directive.less @@ -2,6 +2,14 @@ textarea { display: block; } + + .confirm-password-modal { + .modal-no { + margin-left: 1em; + margin-right: 1em; + margin-top: 1em; + } + } } .pluginsSettingsSubmit { margin-top: 30px; diff --git a/plugins/CorePluginsAdmin/tests/Integration/ApiTest.php b/plugins/CorePluginsAdmin/tests/Integration/ApiTest.php new file mode 100644 index 0000000000..b497f999d4 --- /dev/null +++ b/plugins/CorePluginsAdmin/tests/Integration/ApiTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\CorePluginsAdmin\tests\Integration; + +use Piwik\Access; +use Piwik\Auth; +use Piwik\Container\StaticContainer; +use Piwik\Piwik; +use Piwik\Plugins\CoreUpdater\SystemSettings; +use Piwik\Plugins\UsersManager\API; +use Piwik\Tests\Framework\Fixture; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; +use Piwik\Version; + +class ApiTest extends IntegrationTestCase +{ + const TEST_USER = 'atestuser'; + const TEST_PASSWORD = 'testpassword'; + + private $testSystemSettingsPayload = [ + 'CoreUpdater' => [ + ['name' => 'release_channel', 'value' => 'latest_beta'], + ], + ]; + + protected static function beforeTableDataCached() + { + parent::beforeTableDataCached(); + + API::getInstance()->addUser(self::TEST_USER, self::TEST_PASSWORD, 'someuser@email.com'); + API::getInstance()->setSuperUserAccess(self::TEST_USER, true, Fixture::ADMIN_USER_PASSWORD); + } + + public function setUp(): void + { + parent::setUp(); + + Access::getInstance()->setSuperUserAccess(false); + $auth = StaticContainer::get(Auth::class); + $auth->setLogin(self::TEST_USER); + $auth->setPassword(self::TEST_PASSWORD); + Access::getInstance()->reloadAccess($auth); + } + + public function test_setSystemSettings_throwsIfNoPasswordConfirmation() + { + if (version_compare(Version::VERSION, '4.4.0-b1', '<')) { + $this->markTestSkipped('Skipping test since passwordConfirmation is optional until version 4.4.'); + } + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('UsersManager_ConfirmWithPassword'); + + $settingValues = $this->testSystemSettingsPayload; + \Piwik\Plugins\CorePluginsAdmin\API::getInstance()->setSystemSettings($settingValues); + } + + public function test_setSystemSettings_throwsIfPasswordConfirmationWrong() + { + if (version_compare(Version::VERSION, '4.4.0-b1', '<')) { + $this->markTestSkipped('Skipping test since passwordConfirmation is optional until version 4.4.'); + } + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('UsersManager_CurrentPasswordNotCorrect'); + + $settingValues = $this->testSystemSettingsPayload; + \Piwik\Plugins\CorePluginsAdmin\API::getInstance()->setSystemSettings($settingValues, 'blahblah'); + } + + public function test_setSystemSettings_correctlySetsSettings() + { + $settingValues = $this->testSystemSettingsPayload; + \Piwik\Plugins\CorePluginsAdmin\API::getInstance()->setSystemSettings($settingValues, self::TEST_PASSWORD); + + $coreUpdaterSettings = StaticContainer::get(SystemSettings::class); + $value = $coreUpdaterSettings->releaseChannel->getValue(); + $this->assertEquals('latest_beta', $value); + } + + protected static function configureFixture($fixture) + { + parent::configureFixture($fixture); + $fixture->createSuperUser = true; + } +}
\ No newline at end of file diff --git a/plugins/CorePluginsAdmin/tests/UI/TagManagerTeaser_spec.js b/plugins/CorePluginsAdmin/tests/UI/TagManagerTeaser_spec.js index 8020bb159b..a9ac485f90 100644 --- a/plugins/CorePluginsAdmin/tests/UI/TagManagerTeaser_spec.js +++ b/plugins/CorePluginsAdmin/tests/UI/TagManagerTeaser_spec.js @@ -58,7 +58,13 @@ describe("TagManagerTeaser", function () { it('should be possible to activate plugin and redirect to tag manager', async function () { await page.click('.activateTagManager .activateTagManagerPlugin'); await page.waitForNetworkIdle(); + + await page.type('#login_form_password', 'superUserPass'); + await page.click('#login_form_submit'); + + await page.waitForNetworkIdle(); await page.waitFor(250); + expect(await page.screenshotSelector('.pageWrap')).to.matchImage('super_user_activate_plugin'); }); |