diff --git a/core/Access.php b/core/Access.php index a5f6f604c043e9f5c623b1d97f6f97b576adbc57..d6706237558be0a2df4d683d72971e6a528c96eb 100644 --- a/core/Access.php +++ b/core/Access.php @@ -117,6 +117,11 @@ class Access * Constructor */ public function __construct() + { + $this->resetSites(); + } + + private function resetSites() { $this->idsitesByAccess = array( 'view' => array(), @@ -138,19 +143,15 @@ class Access */ public function reloadAccess(Auth $auth = null) { - if (!is_null($auth)) { - $this->auth = $auth; - } + $this->resetSites(); - // if the Auth wasn't set, we may be in the special case of setSuperUser(), otherwise we fail - if (is_null($this->auth)) { - if ($this->hasSuperUserAccess()) { - return $this->reloadAccessSuperUser(); - } + if (isset($auth)) { + $this->auth = $auth; } if ($this->hasSuperUserAccess()) { - return $this->reloadAccessSuperUser(); + $this->makeSureLoginNameIsSet(); + return true; } // if the Auth wasn't set, we may be in the special case of setSuperUser(), otherwise we fail TODO: docs + review @@ -170,18 +171,7 @@ class Access // case the superUser is logged in if ($result->hasSuperUserAccess()) { - return $this->reloadAccessSuperUser(); - } - - // in case multiple calls to API using different tokens, we ensure we reset it as not SU - $this->setSuperUserAccess(false); - - // we join with site in case there are rows in access for an idsite that doesn't exist anymore - // (backward compatibility ; before we deleted the site without deleting rows in _access table) - $accessRaw = $this->getRawSitesWithSomeViewAccess($this->login); - - foreach ($accessRaw as $access) { - $this->idsitesByAccess[$access['access']][] = $access['idsite']; + $this->setSuperUserAccess(true); } return true; @@ -210,27 +200,42 @@ class Access } /** - * Reload Super User access + * Make sure a login name is set * - * @return bool + * @return true */ - protected function reloadAccessSuperUser() + protected function makeSureLoginNameIsSet() { - $this->hasSuperUserAccess = true; - - try { - $allSitesId = Plugins\SitesManager\API::getInstance()->getAllSitesId(); - } catch (\Exception $e) { - $allSitesId = array(); - } - $this->idsitesByAccess['superuser'] = $allSitesId; - - if(empty($this->login)) { + if (empty($this->login)) { // flag to force non empty login so Super User is not mistaken for anonymous $this->login = 'super user was set'; } + } - return true; + private function loadSitesIfNeeded() + { + if ($this->hasSuperUserAccess) { + if (empty($this->idsitesByAccess['superuser'])) { + try { + $allSitesId = Plugins\SitesManager\API::getInstance()->getAllSitesId(); + } catch (\Exception $e) { + $allSitesId = array(); + } + $this->idsitesByAccess['superuser'] = $allSitesId; + } + } else if (isset($this->login)) { + if (empty($this->idsitesByAccess['view']) + && empty($this->idsitesByAccess['admin'])) { + + // we join with site in case there are rows in access for an idsite that doesn't exist anymore + // (backward compatibility ; before we deleted the site without deleting rows in _access table) + $accessRaw = $this->getRawSitesWithSomeViewAccess($this->login); + + foreach ($accessRaw as $access) { + $this->idsitesByAccess[$access['access']][] = $access['idsite']; + } + } + } } /** @@ -241,11 +246,12 @@ class Access */ public function setSuperUserAccess($bool = true) { + $this->hasSuperUserAccess = (bool) $bool; + if ($bool) { - $this->reloadAccessSuperUser(); + $this->makeSureLoginNameIsSet(); } else { - $this->hasSuperUserAccess = false; - $this->idsitesByAccess['superuser'] = array(); + $this->resetSites(); } } @@ -288,6 +294,8 @@ class Access */ public function getSitesIdWithAtLeastViewAccess() { + $this->loadSitesIfNeeded(); + return array_unique(array_merge( $this->idsitesByAccess['view'], $this->idsitesByAccess['admin'], @@ -303,6 +311,8 @@ class Access */ public function getSitesIdWithAdminAccess() { + $this->loadSitesIfNeeded(); + return array_unique(array_merge( $this->idsitesByAccess['admin'], $this->idsitesByAccess['superuser']) @@ -318,6 +328,8 @@ class Access */ public function getSitesIdWithViewAccess() { + $this->loadSitesIfNeeded(); + return $this->idsitesByAccess['view']; } diff --git a/core/Settings/Setting.php b/core/Settings/Setting.php index 32883876715e9aaf8c44e4e87fc4bbfea1aad1a3..4b1b36e5a14f8dbcf49f20aff460b0261f449753 100644 --- a/core/Settings/Setting.php +++ b/core/Settings/Setting.php @@ -147,8 +147,6 @@ abstract class Setting protected $key; protected $name; - protected $writableByCurrentUser = false; - protected $readableByCurrentUser = false; /** * @var StorageInterface @@ -188,7 +186,7 @@ abstract class Setting */ public function isWritableByCurrentUser() { - return $this->writableByCurrentUser; + return false; } /** @@ -198,7 +196,7 @@ abstract class Setting */ public function isReadableByCurrentUser() { - return $this->readableByCurrentUser; + return false; } /** diff --git a/core/Settings/SystemSetting.php b/core/Settings/SystemSetting.php index 935aec13883ba96a346c4917495f5c307d46954e..94f7416c19c70e7d2e0dd94f2d380af79c2b18fb 100644 --- a/core/Settings/SystemSetting.php +++ b/core/Settings/SystemSetting.php @@ -31,6 +31,11 @@ class SystemSetting extends Setting */ public $readableByCurrentUser = false; + /** + * @var bool + */ + private $writableByCurrentUser = false; + /** * Constructor. * @@ -45,6 +50,27 @@ class SystemSetting extends Setting $this->readableByCurrentUser = $this->writableByCurrentUser; } + /** + * Returns `true` if this setting is writable for the current user, `false` if otherwise. In case it returns + * writable for the current user it will be visible in the Plugin settings UI. + * + * @return bool + */ + public function isWritableByCurrentUser() + { + return $this->writableByCurrentUser; + } + + /** + * Returns `true` if this setting can be displayed for the current user, `false` if otherwise. + * + * @return bool + */ + public function isReadableByCurrentUser() + { + return $this->readableByCurrentUser; + } + /** * Returns the display order. System settings are displayed before user settings. * diff --git a/core/Settings/UserSetting.php b/core/Settings/UserSetting.php index 80cf66ebf21a63a7053149b1a1a58e8a0851e563..60f63e3037b8b33a2acb9e1ce8be7518d3786a78 100644 --- a/core/Settings/UserSetting.php +++ b/core/Settings/UserSetting.php @@ -22,6 +22,12 @@ class UserSetting extends Setting { private $userLogin = null; + /** + * Null while not initialized, bool otherwise. + * @var null|bool + */ + private $hasReadAndWritePermission = null; + /** * Constructor. * @@ -34,9 +40,32 @@ class UserSetting extends Setting parent::__construct($name, $title); $this->setUserLogin($userLogin); + } + + /** + * Returns `true` if this setting can be displayed for the current user, `false` if otherwise. + * + * @return bool + */ + public function isReadableByCurrentUser() + { + return $this->isWritableByCurrentUser(); + } + + /** + * Returns `true` if this setting can be displayed for the current user, `false` if otherwise. + * + * @return bool + */ + public function isWritableByCurrentUser() + { + if (isset($this->hasReadAndWritePermission)) { + return $this->hasReadAndWritePermission; + } + + $this->hasReadAndWritePermission = Piwik::isUserHasSomeViewAccess(); - $this->writableByCurrentUser = Piwik::isUserHasSomeViewAccess(); - $this->readableByCurrentUser = Piwik::isUserHasSomeViewAccess(); + return $this->hasReadAndWritePermission; } /** diff --git a/plugins/SitesManager/API.php b/plugins/SitesManager/API.php index 8457778ab5b984b703e1341d62b29bacca0264fb..94b53a2fa0d71168ce24df6fa0b2e11fae9d45e1 100644 --- a/plugins/SitesManager/API.php +++ b/plugins/SitesManager/API.php @@ -240,7 +240,7 @@ class API extends \Piwik\Plugin\API { Piwik::checkUserHasSuperUserAccess(); try { - return API::getInstance()->getSitesId(); + return $this->getSitesId(); } catch (Exception $e) { // can be called before Piwik tables are created so return empty return array(); @@ -287,7 +287,7 @@ class API extends \Piwik\Plugin\API if ($fetchAliasUrls) { foreach ($sites as &$site) { - $site['alias_urls'] = API::getInstance()->getSiteUrlsFromId($site['idsite']); + $site['alias_urls'] = $this->getSiteUrlsFromId($site['idsite']); } } @@ -593,7 +593,7 @@ class API extends \Piwik\Plugin\API { Piwik::checkUserHasSuperUserAccess(); - $idSites = API::getInstance()->getSitesId(); + $idSites = $this->getSitesId(); if (!in_array($idSite, $idSites)) { throw new Exception("website id = $idSite not found"); } @@ -1048,7 +1048,8 @@ class API extends \Piwik\Plugin\API { Piwik::checkUserHasAdminAccess($idSite); - $idSites = API::getInstance()->getSitesId(); + $idSites = $this->getSitesId(); + if (!in_array($idSite, $idSites)) { throw new Exception("website id = $idSite not found"); } diff --git a/plugins/UsersManager/API.php b/plugins/UsersManager/API.php index ccb4dac126e6c6b4ad54a00f91ec5869ab7d3b14..69986e57eb1c0a2a5ec26475596defe433755d57 100644 --- a/plugins/UsersManager/API.php +++ b/plugins/UsersManager/API.php @@ -107,7 +107,13 @@ class API extends \Piwik\Plugin\API return $optionValue; } - return $this->getDefaultUserPreference($preferenceName, $userLogin); + $defaultPreference = $this->getDefaultUserPreference($preferenceName, $userLogin); + + if ($defaultPreference !== false && $preferenceName === self::PREFERENCE_DEFAULT_REPORT) { + $this->setUserPreference($userLogin, $preferenceName, $defaultPreference); + } + + return $defaultPreference; } /** diff --git a/tests/PHPUnit/Integration/AccessTest.php b/tests/PHPUnit/Integration/AccessTest.php index cb2d201c846ab074de2b99b1eeeaeb9b623ba807..a105b7287455610774bb2aa57bcae7b922c1e440 100644 --- a/tests/PHPUnit/Integration/AccessTest.php +++ b/tests/PHPUnit/Integration/AccessTest.php @@ -11,6 +11,8 @@ namespace Piwik\Tests\Integration; use Exception; use Piwik\Access; use Piwik\AuthResult; +use Piwik\Db; +use Piwik\NoAccessException; use Piwik\Tests\Framework\TestCase\IntegrationTestCase; /** @@ -121,16 +123,49 @@ class AccessTest extends IntegrationTestCase $access->checkUserHasSomeAdminAccess(); } + /** + * @expectedException \Piwik\NoAccessException + */ + public function test_CheckUserHasSomeAdminAccessWithSomeAccessFails_IfUserHasPermissionsToSitesButIsNotAuthenticated() + { + $mock = $this->createAccessMockWithAccessToSitesButUnauthenticated(array(2, 9)); + $mock->checkUserHasSomeAdminAccess(); + } + + /** + * @expectedException \Piwik\NoAccessException + */ + public function test_checkUserHasAdminAccessFails_IfUserHasPermissionsToSitesButIsNotAuthenticated() + { + $mock = $this->createAccessMockWithAccessToSitesButUnauthenticated(array(2, 9)); + $mock->checkUserHasAdminAccess('2'); + } + + /** + * @expectedException \Piwik\NoAccessException + */ + public function test_checkUserHasSomeViewAccessFails_IfUserHasPermissionsToSitesButIsNotAuthenticated() + { + $mock = $this->createAccessMockWithAccessToSitesButUnauthenticated(array(2, 9)); + $mock->checkUserHasSomeViewAccess(); + } + + /** + * @expectedException \Piwik\NoAccessException + */ + public function test_checkUserHasViewAccessFails_IfUserHasPermissionsToSitesButIsNotAuthenticated() + { + $mock = $this->createAccessMockWithAccessToSitesButUnauthenticated(array(2, 9)); + $mock->checkUserHasViewAccess('2'); + } + public function testCheckUserHasSomeAdminAccessWithSomeAccess() { - $mock = $this->getMock( - 'Piwik\Access', - array('getSitesIdWithAdminAccess') - ); + $mock = $this->createAccessMockWithAuthenticatedUser(array('getRawSitesWithSomeViewAccess')); $mock->expects($this->once()) - ->method('getSitesIdWithAdminAccess') - ->will($this->returnValue(array(2, 9))); + ->method('getRawSitesWithSomeViewAccess') + ->will($this->returnValue($this->buildAdminAccessForSiteIds(array(2, 9)))); $mock->checkUserHasSomeAdminAccess(); } @@ -153,14 +188,11 @@ class AccessTest extends IntegrationTestCase public function testCheckUserHasSomeViewAccessWithSomeAccess() { - $mock = $this->getMock( - 'Piwik\Access', - array('getSitesIdWithAtLeastViewAccess') - ); + $mock = $this->createAccessMockWithAuthenticatedUser(array('getRawSitesWithSomeViewAccess')); $mock->expects($this->once()) - ->method('getSitesIdWithAtLeastViewAccess') - ->will($this->returnValue(array(1, 2, 3, 4))); + ->method('getRawSitesWithSomeViewAccess') + ->will($this->returnValue($this->buildViewAccessForSiteIds(array(1, 2, 3, 4)))); $mock->checkUserHasSomeViewAccess(); } @@ -183,28 +215,24 @@ class AccessTest extends IntegrationTestCase public function testCheckUserHasViewAccessWithSomeAccessSuccessIdSitesAsString() { - $mock = $this->getMock( - 'Piwik\Access', - array('getSitesIdWithAtLeastViewAccess') - ); + /** @var Access $mock */ + $mock = $this->createAccessMockWithAuthenticatedUser(array('getRawSitesWithSomeViewAccess')); $mock->expects($this->once()) - ->method('getSitesIdWithAtLeastViewAccess') - ->will($this->returnValue(array(1, 2, 3, 4))); + ->method('getRawSitesWithSomeViewAccess') + ->will($this->returnValue($this->buildViewAccessForSiteIds(array(1, 2, 3, 4)))); $mock->checkUserHasViewAccess('1,3'); } public function testCheckUserHasViewAccessWithSomeAccessSuccessAllSites() { - $mock = $this->getMock( - 'Piwik\Access', - array('getSitesIdWithAtLeastViewAccess') - ); + /** @var Access $mock */ + $mock = $this->createAccessMockWithAuthenticatedUser(array('getRawSitesWithSomeViewAccess')); $mock->expects($this->any()) - ->method('getSitesIdWithAtLeastViewAccess') - ->will($this->returnValue(array(1, 2, 3, 4))); + ->method('getRawSitesWithSomeViewAccess') + ->will($this->returnValue($this->buildViewAccessForSiteIds(array(1, 2, 3, 4)))); $mock->checkUserHasViewAccess('all'); } @@ -306,8 +334,7 @@ class AccessTest extends IntegrationTestCase public function testReloadAccessWithMockedAuthValid() { - $mock = $this->getMock('Piwik\\Auth', array('authenticate', 'getName', 'getTokenAuthSecret', 'getLogin', 'setTokenAuth', 'setLogin', - 'setPassword', 'setPasswordHash')); + $mock = $this->createPiwikAuthMockInstance(); $mock->expects($this->once()) ->method('authenticate') ->will($this->returnValue(new AuthResult(AuthResult::SUCCESS, 'login', 'token'))); @@ -319,6 +346,43 @@ class AccessTest extends IntegrationTestCase $this->assertFalse($access->hasSuperUserAccess()); } + public function test_reloadAccess_loadSitesIfNeeded_doesActuallyResetAllSiteIdsAndRequestThemAgain() + { + /** @var Access $mock */ + $mock = $this->createAccessMockWithAuthenticatedUser(array('getRawSitesWithSomeViewAccess')); + + $mock->expects($this->at(0)) + ->method('getRawSitesWithSomeViewAccess') + ->will($this->returnValue($this->buildAdminAccessForSiteIds(array(1,2,3,4)))); + + $mock->expects($this->at(1)) + ->method('getRawSitesWithSomeViewAccess') + ->will($this->returnValue($this->buildAdminAccessForSiteIds(array(1)))); + + // should succeed as permission to 1,2,3,4 + $mock->checkUserHasAdminAccess('1,3'); + + // should clear permissions + $mock->reloadAccess(); + + try { + // should fail as now only permission to site 1 + $mock->checkUserHasAdminAccess('1,3'); + $this->fail('An expected exception has not been triggered. Permissions were not resetted'); + } catch (NoAccessException $e) { + + } + + $mock->checkUserHasAdminAccess('1'); // it should have access to site "1" + + $mock->setSuperUserAccess(true); + + $mock->reloadAccess(); + + // should now have permission as it is a superuser + $mock->checkUserHasAdminAccess('1,3'); + } + public function test_doAsSuperUser_ChangesSuperUserAccessCorrectly() { Access::getInstance()->setSuperUserAccess(false); @@ -372,4 +436,65 @@ class AccessTest extends IntegrationTestCase AccessTest::assertTrue($access->hasSuperUserAccess()); }); } + + private function buildAdminAccessForSiteIds($siteIds) + { + $access = array(); + + foreach ($siteIds as $siteId) { + $access[] = array('access' => 'admin', 'idsite' => $siteId); + } + + return $access; + } + + private function buildViewAccessForSiteIds($siteIds) + { + $access = array(); + + foreach ($siteIds as $siteId) { + $access[] = array('access' => 'admin', 'idsite' => $siteId); + } + + return $access; + } + + private function createPiwikAuthMockInstance() + { + return $this->getMock('Piwik\\Auth', array('authenticate', 'getName', 'getTokenAuthSecret', 'getLogin', 'setTokenAuth', 'setLogin', + 'setPassword', 'setPasswordHash')); + } + + private function createAccessMockWithAccessToSitesButUnauthenticated($idSites) + { + $mock = $this->getMock('Piwik\Access', array('getRawSitesWithSomeViewAccess', 'loadSitesIfNeeded')); + + // this method will be actually never called as it is unauthenticated. The tests are supposed to fail if it + // suddenly does get called as we should not query for sites if it is not authenticated. + $mock->expects($this->any()) + ->method('getRawSitesWithSomeViewAccess') + ->will($this->returnValue($this->buildAdminAccessForSiteIds($idSites))); + + return $mock; + } + + private function createAccessMockWithAuthenticatedUser($methodsToMock = array()) + { + $methods = array('authenticate'); + + foreach ($methodsToMock as $methodToMock) { + $methods[] = $methodToMock; + } + + $authMock = $this->createPiwikAuthMockInstance(); + $authMock->expects($this->atLeast(1)) + ->method('authenticate') + ->will($this->returnValue(new AuthResult(AuthResult::SUCCESS, 'login', 'token'))); + + $mock = $this->getMock('Piwik\Access', $methods); + $mock->reloadAccess($authMock); + + return $mock; + } + }