Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing a new role "write" and possibility to define capabilities #13163

Merged
merged 25 commits into from Jul 18, 2018
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Expand Up @@ -9,8 +9,15 @@ The Product Changelog at **[matomo.org/changelog](https://matomo.org/changelog)*
### New APIs

* Added new event `API.addGlossaryItems` which lets you add items to the glossary.
* A new role has introduced called "write" which has less permissions than an admin but more than a view only user.
* Added new API method `UsersManager.getAvailableRoles` to fetch a list of all available roles that can be granted to a user.
* Added new API method `UsersManager.getAvailableCapabilities` to fetch a list of all available capabilities that can be granted to a user.
* Added new API method `UsersManager.addCapabilities` to grant one or multiple capabilities to a user.
* Added new API method `UsersManager.removeCapabilities` to remove one or multiple capabilities from a user.
* The API method `UsersManager.setUserAccess` now accepts an array to pass a role and multiple capabilities at once.
* Plugin classes can overwrite the method `requiresInternetConnection` to define if they should be automatically unloaded if no internet connection is available (enable_internet_features = 0)


### Breaking Changes
* Changed some menu items to use translation keys instead (see [PR #12885](https://github.com/matomo-org/matomo/pull/12885)).
* The methods `assertResponseCode()` and `assertHttpResponseText()` in `Piwik\Tests\Framework\TestCase\SystemTestCase` have been deprecated and will be removed in Matomo 4.0. Please use `Piwik\Http` instead.
Expand Down
11 changes: 11 additions & 0 deletions config/environment/test.php
Expand Up @@ -45,13 +45,21 @@
if ($testUseMockAuth) {
$idSitesAdmin = $c->get('test.vars.idSitesAdminAccess');
$idSitesView = $c->get('test.vars.idSitesViewAccess');
$idSitesWrite = $c->get('test.vars.idSitesWriteAccess');
$idSitesCapabilities = $c->get('test.vars.idSitesCapabilities');
$access = new FakeAccess();

if (!empty($idSitesView)) {
FakeAccess::$superUser = false;
FakeAccess::$idSitesView = $idSitesView;
FakeAccess::$idSitesWrite = !empty($idSitesWrite) ? $idSitesWrite : array();
FakeAccess::$idSitesAdmin = !empty($idSitesAdmin) ? $idSitesAdmin : array();
FakeAccess::$identity = 'viewUserLogin';
} elseif (!empty($idSitesWrite)) {
FakeAccess::$superUser = false;
FakeAccess::$idSitesWrite = !empty($idSitesWrite) ? $idSitesWrite : array();
FakeAccess::$idSitesAdmin = !empty($idSitesAdmin) ? $idSitesAdmin : array();
FakeAccess::$identity = 'writeUserLogin';
} elseif (!empty($idSitesAdmin)) {
FakeAccess::$superUser = false;
FakeAccess::$idSitesAdmin = $idSitesAdmin;
Expand All @@ -60,6 +68,9 @@
FakeAccess::$superUser = true;
FakeAccess::$superUserLogin = 'superUserLogin';
}
if (!empty($idSitesCapabilities)) {
FakeAccess::$idSitesCapabilities = (array) $idSitesCapabilities;
}
return $access;
} else {
return $previous;
Expand Down
166 changes: 147 additions & 19 deletions core/Access.php
Expand Up @@ -9,6 +9,8 @@
namespace Piwik;

use Exception;
use Piwik\Access\CapabilitiesProvider;
use Piwik\Access\RolesProvider;
use Piwik\Container\StaticContainer;
use Piwik\Plugins\SitesManager\API as SitesManagerApi;

Expand Down Expand Up @@ -65,13 +67,6 @@ class Access
*/
protected $hasSuperUserAccess = false;

/**
* List of available permissions in Piwik
*
* @var array
*/
private static $availableAccess = array('noaccess', 'view', 'admin', 'superuser');

/**
* Authentification object (see Auth)
*
Expand All @@ -90,29 +85,37 @@ public static function getInstance()
}

/**
* Returns the list of the existing Access level.
* Useful when a given API method requests a given acccess Level.
* We first check that the required access level exists.
*
* @return array
* @var CapabilitiesProvider
*/
public static function getListAccess()
{
return self::$availableAccess;
}
protected $capabilityProvider;

/**
* @var RolesProvider
*/
private $roleProvider;

/**
* Constructor
*/
public function __construct()
public function __construct(RolesProvider $roleProvider = null, CapabilitiesProvider $capabilityProvider = null)
{
if (!isset($roleProvider)) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was needed as some system tests otherwise fail... it would try to use DI before the environment is set up etc.

$roleProvider = StaticContainer::get('Piwik\Access\RolesProvider');
}
if (!isset($capabilityProvider)) {
$capabilityProvider = StaticContainer::get('Piwik\Access\CapabilitiesProvider');
}
$this->roleProvider = $roleProvider;
$this->capabilityProvider = $capabilityProvider;

$this->resetSites();
}

private function resetSites()
{
$this->idsitesByAccess = array(
'view' => array(),
'write' => array(),
'admin' => array(),
'superuser' => array()
);
Expand Down Expand Up @@ -217,14 +220,28 @@ protected function loadSitesIfNeeded()
}
} elseif (isset($this->login)) {
if (empty($this->idsitesByAccess['view'])
&& empty($this->idsitesByAccess['write'])
&& 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'];
$accessType = $access['access'];
$this->idsitesByAccess[$accessType][] = $access['idsite'];

if ($this->roleProvider->isValidRole($access)) {
foreach ($this->capabilityProvider->getAllCapabilities() as $capability) {
if ($capability->hasRoleCapability($accessType)) {
// we automatically add this capability
if (!isset($this->idsitesByAccess[$capability->getId()])) {
$this->idsitesByAccess[$capability->getId()] = array();
}
$this->idsitesByAccess[$capability->getId()][] = $access['idsite'];
}
}
}
}
}
}
Expand Down Expand Up @@ -279,7 +296,7 @@ public function getTokenAuth()

/**
* Returns an array of ID sites for which the user has at least a VIEW access.
* Which means VIEW or ADMIN or SUPERUSER.
* Which means VIEW OR WRITE or ADMIN or SUPERUSER.
*
* @return array Example if the user is ADMIN for 4
* and has VIEW access for 1 and 7, it returns array(1, 4, 7);
Expand All @@ -290,6 +307,25 @@ public function getSitesIdWithAtLeastViewAccess()

return array_unique(array_merge(
$this->idsitesByAccess['view'],
$this->idsitesByAccess['write'],
$this->idsitesByAccess['admin'],
$this->idsitesByAccess['superuser'])
);
}

/**
* Returns an array of ID sites for which the user has at least a WRITE access.
* Which means WRITE or ADMIN or SUPERUSER.
*
* @return array Example if the user is WRITE for 4 and 8
* and has VIEW access for 1 and 7, it returns array(4, 8);
*/
public function getSitesIdWithAtLeastWriteAccess()
{
$this->loadSitesIfNeeded();

return array_unique(array_merge(
$this->idsitesByAccess['write'],
$this->idsitesByAccess['admin'],
$this->idsitesByAccess['superuser'])
);
Expand Down Expand Up @@ -325,6 +361,20 @@ public function getSitesIdWithViewAccess()
return $this->idsitesByAccess['view'];
}

/**
* Returns an array of ID sites for which the user has a WRITE access only.
*
* @return array Example if the user is ADMIN for 4
* and has WRITE access for 1 and 7, it returns array(1, 7);
* @see getSitesIdWithAtLeastWriteAccess()
*/
public function getSitesIdWithWriteAccess()
{
$this->loadSitesIfNeeded();

return $this->idsitesByAccess['write'];
}

/**
* Throws an exception if the user is not the SuperUser
*
Expand All @@ -337,6 +387,22 @@ public function checkUserHasSuperUserAccess()
}
}

/**
* Returns `true` if the current user has admin access to at least one site.
*
* @return bool
*/
public function isUserHasSomeWriteAccess()
{
if ($this->hasSuperUserAccess()) {
return true;
}

$idSitesAccessible = $this->getSitesIdWithAtLeastWriteAccess();

return count($idSitesAccessible) > 0;
}

/**
* Returns `true` if the current user has admin access to at least one site.
*
Expand All @@ -353,6 +419,18 @@ public function isUserHasSomeAdminAccess()
return count($idSitesAccessible) > 0;
}

/**
* If the user doesn't have an WRITE access for at least one website, throws an exception
*
* @throws \Piwik\NoAccessException
*/
public function checkUserHasSomeWriteAccess()
{
if (!$this->isUserHasSomeWriteAccess()) {
throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAtLeastOneWebsite', array('write')));
}
}

/**
* If the user doesn't have an ADMIN access for at least one website, throws an exception
*
Expand Down Expand Up @@ -429,6 +507,56 @@ public function checkUserHasViewAccess($idSites)
}
}

/**
* This method checks that the user has VIEW or ADMIN access for the given list of websites.
* If the user doesn't have VIEW or ADMIN access for at least one website of the list, we throw an exception.
*
* @param int|array|string $idSites List of ID sites to check (integer, array of integers, string comma separated list of integers)
* @throws \Piwik\NoAccessException If for any of the websites the user doesn't have an VIEW or ADMIN access
*/
public function checkUserHasWriteAccess($idSites)
{
if ($this->hasSuperUserAccess()) {
return;
}

$idSites = $this->getIdSites($idSites);
$idSitesAccessible = $this->getSitesIdWithAtLeastWriteAccess();

foreach ($idSites as $idsite) {
if (!in_array($idsite, $idSitesAccessible, true)) {
throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'write'", $idsite)));
}
}
}

private function getSitesIdWithCapability($capability)
{
if (!empty($this->idsitesByAccess[$capability])) {
return $this->idsitesByAccess[$capability];
}
return array();
}

public function checkUserHasCapability($idSites, $capability)
{
if ($this->hasSuperUserAccess()) {
return;
}

$idSites = $this->getIdSites($idSites);
$idSitesAccessible = $this->getSitesIdWithCapability($capability);

foreach ($idSites as $idsite) {
if (!in_array($idsite, $idSitesAccessible, true)) {
throw new NoAccessException(Piwik::translate('ExceptionCapabilityAccessWebsite', array("'" . $capability ."'", $idsite)));
}
}

// a capability applies only when the user also has at least view access
$this->checkUserHasViewAccess($idSites);
}

/**
* @param int|array|string $idSites
* @return array
Expand Down