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

Created new setting to restrict access to MTM #887

Merged
merged 9 commits into from
Sep 24, 2024
23 changes: 23 additions & 0 deletions API.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ public function __construct(Tag $tags, Trigger $triggers, Variable $variables, C
public function getAvailableContexts()
{
Piwik::checkUserHasSomeViewAccess();
$this->checkUserHasTagManagerAccess();

$contexts = $this->contextProvider->getAllContexts();

Expand All @@ -177,6 +178,7 @@ public function getAvailableContexts()
public function getAvailableEnvironments()
{
Piwik::checkUserHasSomeViewAccess();
$this->checkUserHasTagManagerAccess();

return $this->environment->getEnvironments();
}
Expand All @@ -190,6 +192,7 @@ public function getAvailableEnvironments()
public function getAvailableEnvironmentsWithPublishCapability($idSite)
{
Piwik::checkUserHasSomeViewAccess();
$this->checkUserHasTagManagerAccess($idSite);

$environments = $this->environment->getEnvironments();

Expand All @@ -210,6 +213,7 @@ public function getAvailableEnvironmentsWithPublishCapability($idSite)
public function getAvailableTagFireLimits()
{
Piwik::checkUserHasSomeViewAccess();
$this->checkUserHasTagManagerAccess();

return $this->tags->getFireLimits();
}
Expand All @@ -222,6 +226,7 @@ public function getAvailableTagFireLimits()
public function getAvailableComparisons()
{
Piwik::checkUserHasSomeViewAccess();
$this->checkUserHasTagManagerAccess();

return $this->comparisons->getSupportedComparisons();
}
Expand All @@ -234,6 +239,7 @@ public function getAvailableComparisons()
public function getAvailableTagTypesInContext($idContext)
{
Piwik::checkUserHasSomeViewAccess();
$this->checkUserHasTagManagerAccess();

$this->contextProvider->checkIsValidContext($idContext);

Expand Down Expand Up @@ -262,6 +268,7 @@ public function getAvailableTagTypesInContext($idContext)
public function getAvailableTriggerTypesInContext($idContext)
{
Piwik::checkUserHasSomeViewAccess();
$this->checkUserHasTagManagerAccess();

$this->contextProvider->checkIsValidContext($idContext);

Expand All @@ -286,6 +293,7 @@ public function getAvailableTriggerTypesInContext($idContext)
public function getAvailableVariableTypesInContext($idContext)
{
Piwik::checkUserHasSomeViewAccess();
$this->checkUserHasTagManagerAccess();

$this->contextProvider->checkIsValidContext($idContext);

Expand Down Expand Up @@ -1448,4 +1456,19 @@ private function decodeQuotes($value)
{
return htmlspecialchars_decode($value, ENT_QUOTES);
}

/**
* Check whether the current user has MTM access. If the site ID isn't provided, try looking it up on the request
*
* @param $idSite
* @return void
* @throws \Piwik\NoAccessException
*/
private function checkUserHasTagManagerAccess($idSite = null)
{
if (empty($idSite)) {
$idSite = \Piwik\Request::fromRequest()->getIntegerParameter('idSite', 0);
}
$this->accessValidator->checkUserHasTagManagerAccess($idSite);
}
}
10 changes: 10 additions & 0 deletions Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use Piwik\API\Request;
use Piwik\Common;
use Piwik\Container\StaticContainer;
use Piwik\DataTable\Filter\SafeDecodeLabel;
use Piwik\Filechecks;
use Piwik\Menu\MenuTop;
Expand Down Expand Up @@ -156,6 +157,8 @@ public function dashboard()

public function manageTags()
{
$this->accessValidator->checkUserHasTagManagerAccess($this->idSite);

$tagsHelpText = $this->renderTemplate('helpContent', [
'subHeading' => Piwik::translate('TagManager_ManageTagsHelp1'),
'paragraphs' => [
Expand All @@ -176,6 +179,8 @@ public function manageTags()

public function manageTriggers()
{
$this->accessValidator->checkUserHasTagManagerAccess($this->idSite);

$triggersHelpText = $this->renderTemplate('helpContent', [
'subHeading' => Piwik::translate('TagManager_ManageTriggersHelp1'),
'paragraphs' => [
Expand All @@ -198,6 +203,8 @@ public function manageTriggers()

public function manageVariables()
{
$this->accessValidator->checkUserHasTagManagerAccess($this->idSite);

$variablesHelpText = $this->renderTemplate('helpContent', [
'subHeading' => Piwik::translate('TagManager_ManageVariablesHelp1'),
'paragraphs' => [
Expand All @@ -220,6 +227,8 @@ public function manageVariables()

public function manageVersions()
{
$this->accessValidator->checkUserHasTagManagerAccess($this->idSite);

$idSite = Common::getRequestVar('idSite', null, 'int');
$this->accessValidator->checkWriteCapability($idSite);
$path = TagManager::getAbsolutePathToContainerDirectory();
Expand Down Expand Up @@ -316,6 +325,7 @@ private function renderManageContainerTemplate($template, $variables = array())
public function exportContainerVersion()
{
$this->checkSitePermission();
$this->accessValidator->checkUserHasTagManagerAccess($this->idSite);

$jsonCallback = Common::getRequestVar('callback', false);

Expand Down
16 changes: 16 additions & 0 deletions Input/AccessValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

namespace Piwik\Plugins\TagManager\Input;

use Piwik\Container\StaticContainer;
use Piwik\NoAccessException;
use Piwik\Plugins\TagManager\Access\Capability\PublishLiveContainer;
use Piwik\Plugins\TagManager\Access\Capability\TagManagerWrite;
use Piwik\Plugins\TagManager\Access\Capability\UseCustomTemplates;
Expand All @@ -31,18 +33,21 @@ public function checkViewPermission($idSite)
{
$this->checkSiteExists($idSite);
Piwik::checkUserHasViewAccess($idSite);
$this->checkUserHasTagManagerAccess($idSite);
}

public function checkWriteCapability($idSite)
{
$this->checkSiteExists($idSite);
Piwik::checkUserHasCapability($idSite, TagManagerWrite::ID);
$this->checkUserHasTagManagerAccess($idSite);
}

public function checkPublishLiveEnvironmentCapability($idSite)
{
$this->checkSiteExists($idSite);
Piwik::checkUserHasCapability($idSite, PublishLiveContainer::ID);
$this->checkUserHasTagManagerAccess($idSite);
}

public function checkUseCustomTemplatesCapability($idSite)
Expand All @@ -59,6 +64,17 @@ public function checkUseCustomTemplatesCapability($idSite)
}
}

public function checkUserHasTagManagerAccess($idSite): void
{
// If the user has access, return before the exception is thrown
if (StaticContainer::get(SystemSettings::class)->doesCurrentUserHaveTagManagerAccess(intval($idSite))) {
return;
}

$minimumRole = StaticContainer::get(SystemSettings::class)->restrictTagManagerAccess->getValue();
throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', ["'{$minimumRole}'", $idSite]));
}

public function hasUseCustomTemplatesCapability($idSite)
{
try {
Expand Down
6 changes: 6 additions & 0 deletions Menu.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ public function __construct(AccessValidator $accessValidator)

public function configureTopMenu(MenuTop $menu)
{
// Check whether to show the MTM top menu. If not, simply return early
$idSite = \Piwik\Request::fromRequest()->getIntegerParameter('idSite', 0);
if (!StaticContainer::get(SystemSettings::class)->doesCurrentUserHaveTagManagerAccess($idSite)) {
return;
}

list($defaultAction, $defaultParams) = self::getDefaultAction();
if ($defaultAction) {
$menu->addItem('TagManager_TagManager', null, $this->urlForAction($defaultAction, $defaultParams), $orderId = 30);
Expand Down
77 changes: 77 additions & 0 deletions SystemSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
*/
namespace Piwik\Plugins\TagManager;

use Piwik\Access;
use Piwik\Container\StaticContainer;
use Piwik\Date;
use Piwik\Piwik;
use Piwik\Plugins\TagManager\Context\BaseContext;
use Piwik\Plugins\TagManager\Model\Environment;
use Piwik\Settings\Plugin\SystemSetting;
use Piwik\Settings\Setting;
use Piwik\Settings\FieldConfig;

Expand All @@ -21,6 +23,16 @@ class SystemSettings extends \Piwik\Settings\Plugin\SystemSettings
const CUSTOM_TEMPLATES_ADMIN = 'admin';
const CUSTOM_TEMPLATES_SUPERUSER = 'superuser';

const USER_PERMISSON_LIST = [
\Piwik\Access\Role\View::ID,
snake14 marked this conversation as resolved.
Show resolved Hide resolved
\Piwik\Access\Role\Write::ID,
\Piwik\Access\Role\Admin::ID,
self::CUSTOM_TEMPLATES_SUPERUSER
];

/** @var Setting */
public $restrictTagManagerAccess;

/** @var Setting */
public $restrictCustomTemplates;

Expand All @@ -31,10 +43,75 @@ class SystemSettings extends \Piwik\Settings\Plugin\SystemSettings

protected function init()
{
$this->restrictTagManagerAccess = $this->createRestrictAccessSetting();
$this->restrictCustomTemplates = $this->createCustomTemplatesSetting();
$this->environments = $this->createEnvironmentsSetting();
}

private function createRestrictAccessSetting(): SystemSetting
{
return $this->makeSetting('restrictTagManagerAccess', \Piwik\Access\Role\View::ID, FieldConfig::TYPE_STRING, function (FieldConfig $field) {
$field->title = Piwik::translate('TagManager_SettingRestrictAccessTitle');
$field->uiControl = FieldConfig::UI_CONTROL_SINGLE_SELECT;
$field->description = Piwik::translate('TagManager_SettingRestrictAccessDescription');
$field->availableValues = [
self::USER_PERMISSON_LIST[$this->getPermissionIndex('view')] => Piwik::translate('TagManager_SettingRestrictAccessView'),
self::USER_PERMISSON_LIST[$this->getPermissionIndex('write')] => Piwik::translate('TagManager_SettingRestrictAccessWrite'),
self::USER_PERMISSON_LIST[$this->getPermissionIndex('admin')] => Piwik::translate('TagManager_SettingRestrictAccessAdmin'),
self::USER_PERMISSON_LIST[$this->getPermissionIndex('superuser')] => Piwik::translate('TagManager_SettingRestrictAccessSuperUser')
];
});
}

private function getPermissionIndex(string $permission): int
{
$index = array_search($permission, self::USER_PERMISSON_LIST);
if ($index !== false) {
return $index;
}

throw new \Exception('Permission \'' . $permission . '\' not found');
}

/**
* Check whether the currently logged-in user has access to MTM. This expects the site ID, but allows 0 for the
* Administration area, where there isn't necessarily a specific site selected.
*
* @param int $idSite ID of the site currently being viewed. 0 or nothing should be passed if in Administration area
* @return bool Whether the user has access to MTM
*/
public function doesCurrentUserHaveTagManagerAccess(int $idSite = 0): bool
{
// First check for superuser access, since the setting won't matter at that point
$access = StaticContainer::get(Access::class);
if ($access->hasSuperUserAccess()) {
return true;
}

$settingValue = $this->restrictTagManagerAccess->getValue();

// We need to allow checks with no site ID since we might be in the Administration section
if ($idSite === 0) {
switch ($settingValue) {
case self::USER_PERMISSON_LIST[$this->getPermissionIndex('view')]:
return !empty($access->getSitesIdWithAtLeastViewAccess());
case self::USER_PERMISSON_LIST[$this->getPermissionIndex('write')]:
return $access->isUserHasSomeWriteAccess();
case self::USER_PERMISSON_LIST[$this->getPermissionIndex('admin')]:
return $access->isUserHasSomeAdminAccess();
// Those should be the only available options, since we already checked for superuser
default:
return false;
}
}

$role = $access->getRoleForSite($idSite);
$roleIndex = in_array($role, self::USER_PERMISSON_LIST) ? array_search($role, self::USER_PERMISSON_LIST) : 0;
$settingIndex = in_array($settingValue, self::USER_PERMISSON_LIST) ? array_search($settingValue, self::USER_PERMISSON_LIST) : 0;

return $roleIndex >= $settingIndex;
}

private function createCustomTemplatesSetting()
{
return $this->makeSetting('restrictCustomTemplates', self::CUSTOM_TEMPLATES_ADMIN, FieldConfig::TYPE_STRING, function (FieldConfig $field) {
Expand Down
27 changes: 27 additions & 0 deletions TagManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,23 @@ public function getQueryParametersToExclude(&$parametersToExclude)

public function endTrackingCodePageTableOfContents(&$out)
{
// Check whether to show the MTM code. If not, simply return early
if ($this->isAccessRestrictedForUser()) {
return;
}

$out .= '<a href="#/tagmanager">' . Piwik::translate('TagManager_TagManager') . '</a>';
}

public function addTagManagerCode(&$out)
{
Piwik::checkUserHasSomeViewAccess();

// Check whether to show the MTM code. If not, simply return early
if ($this->isAccessRestrictedForUser()) {
return;
}

$model = $this->getContainerModel();
$view = new View("@TagManager/trackingCode");
$view->action = Piwik::getAction();
Expand All @@ -277,6 +288,11 @@ public function addTagManagerCode(&$out)

public function setTagManagerCode(&$out)
{
// Check whether to show the MTM code. If not, simply return early
if ($this->isAccessRestrictedForUser()) {
return;
}

$newContent = '<h2>' . Piwik::translate('SitesManager_StepByStepGuide') . '</h2>';
$this->addTagManagerCode($newContent);
$out = $newContent;
Expand All @@ -285,6 +301,12 @@ public function setTagManagerCode(&$out)
public function embedReactTagManagerTrackingCode(&$out, SiteContentDetector $detector)
{
Piwik::checkUserHasSomeViewAccess();

// Check whether to show the MTM code. If not, simply return early
if ($this->isAccessRestrictedForUser()) {
return;
}

$model = $this->getContainerModel();
$view = new View("@TagManager/trackingCodeReact");
$view->action = Piwik::getAction();
Expand Down Expand Up @@ -998,4 +1020,9 @@ public function getMessagesToWarnOnSiteRemoval(&$messages, $idSite)
}
}

private function isAccessRestrictedForUser(): bool
{
$idSite = \Piwik\Request::fromRequest()->getIntegerParameter('idSite', 0);
return !StaticContainer::get(SystemSettings::class)->doesCurrentUserHaveTagManagerAccess($idSite);
}
}
8 changes: 7 additions & 1 deletion lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,12 @@
"DeleteWebsiteExplanationLine2": "To save a copy of container configurations, click on the container name below and export the desired version:",
"DeleteWebsiteExplanationLine3": "Click Confirm when you are ready to delete this website.",
"ActivelySyncGtmDataLayerTitle": "Actively synchronise from the Google Tag Manager data layer",
"ActivelySyncGtmDataLayerDescription": "When enabled, any new values pushed to the Google Tag Manager data layer will be synchronised to the Matomo Tag Manager data layer."
"ActivelySyncGtmDataLayerDescription": "When enabled, any new values pushed to the Google Tag Manager data layer will be synchronised to the Matomo Tag Manager data layer.",
"SettingRestrictAccessTitle": "Restrict access to Matomo Tag Manager",
"SettingRestrictAccessDescription": "Define who can see the Matomo Tag Manager section. This can be helpful when you have many users with the View permission, or if your Tag Manager configuration is very complex.",
"SettingRestrictAccessView": "Users with at least view permission",
"SettingRestrictAccessWrite": "Users with at least write permission",
"SettingRestrictAccessAdmin": "Users with at least admin permission",
"SettingRestrictAccessSuperUser": "Users with at least superuser permission"
}
}
Loading
Loading