Skip to content

Commit 20760cb

Browse files
committed
Add auth token privilege cap setting
1 parent 0739f5e commit 20760cb

14 files changed

Lines changed: 640 additions & 61 deletions

API.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Piwik\NoAccessException;
2121
use Piwik\Piwik;
2222
use Piwik\Http\BadRequestException;
23+
use Piwik\Plugins\McpServer\Support\Access\McpAccessLevel;
2324
use Piwik\Plugins\McpServer\Support\Api\JsonRpcErrorResponseFactory;
2425
use Piwik\Plugins\McpServer\Support\Api\JsonRpcRequestIdExtractor;
2526
use Piwik\Plugins\McpServer\Support\Api\McpEndpointGuard;
@@ -96,6 +97,10 @@ public function mcp(): ResponseInterface
9697
return $this->createDisabledResponse($requestMetadata['topLevelRequestId']);
9798
}
9899

100+
if (!$this->isCurrentUserPrivilegeLevelAllowed()) {
101+
return $this->createPrivilegeTooHighResponse($requestMetadata['topLevelRequestId']);
102+
}
103+
99104
try {
100105
$server = $this->factory->createServer();
101106
$transport = new StreamableHttpTransport($request);
@@ -137,6 +142,14 @@ protected function isMcpEnabled(): bool
137142
return $this->systemSettings->isMcpEnabled();
138143
}
139144

145+
protected function isCurrentUserPrivilegeLevelAllowed(): bool
146+
{
147+
return !McpAccessLevel::exceedsMaximumAllowed(
148+
McpAccessLevel::resolveCurrentUserLevel(),
149+
$this->systemSettings->getMaximumAllowedMcpAccessLevel()
150+
);
151+
}
152+
140153
protected function isCurrentApiRequestRoot(): bool
141154
{
142155
return ApiRequest::isCurrentApiRequestTheRootApiRequest();
@@ -175,4 +188,18 @@ protected function createDisabledResponse(string|int|null $topLevelRequestId): R
175188
$topLevelRequestId
176189
);
177190
}
191+
192+
protected function createPrivilegeTooHighResponse(string|int|null $topLevelRequestId): ResponseInterface
193+
{
194+
if ($topLevelRequestId === null) {
195+
return (new Psr17Factory())->createResponse(403);
196+
}
197+
198+
return $this->jsonRpcErrorResponseFactory->create(
199+
403,
200+
JsonRpcError::INVALID_REQUEST,
201+
McpAccessLevel::createTooHighPrivilegeMessage($this->systemSettings->getMaximumAllowedMcpAccessLevel()),
202+
$topLevelRequestId
203+
);
204+
}
178205
}

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ It provides analytics tools for sites, reports, processed report data, goals, se
1212
2. Activate **McpServer** in **Administration -> Plugins**.
1313
3. Enable MCP in **Administration -> System -> Plugin Settings -> McpServer**.
1414
4. Configure your MCP client with the endpoint and a Matomo `token_auth` that already has access to the data you want to expose.
15+
5. If needed, restrict the maximum allowed MCP privilege level in plugin settings or use a separate lower-privilege Matomo user for MCP access.
1516

1617
For the recommended end-user setup flow, use the in-product connect guide at **Administration -> Platform -> MCP Server**.
1718

@@ -21,6 +22,7 @@ For the recommended end-user setup flow, use the in-product connect guide at **A
2122
- Raw Matomo API discovery and execution tools are separately disabled by default and must be enabled by an administrator.
2223
- The plugin uses Matomo authentication.
2324
- Data access is limited to the same sites and reports the Matomo user can already access.
25+
- Administrators can optionally restrict MCP usage to users or tokens at or below a configured privilege level.
2426
- When raw API access is enabled, MCP clients can access the same Matomo API surface available to the authenticated user, including state-changing methods if an administrator has allowed them.
2527
- If features such as the Visitor Log are available to that user, MCP clients may access the same underlying data scope.
2628
- Review privacy, security, and compliance requirements before enabling raw API access.

Support/Access/McpAccessLevel.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
/**
4+
* Matomo - free/libre analytics platform
5+
*
6+
* @link https://matomo.org
7+
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Piwik\Plugins\McpServer\Support\Access;
13+
14+
use Piwik\Access;
15+
use Piwik\Piwik;
16+
use Piwik\Plugins\McpServer\Support\Api\McpEndpointSpec;
17+
18+
final class McpAccessLevel
19+
{
20+
public const UNLIMITED = 'unlimited';
21+
public const VIEW = 'view';
22+
public const WRITE = 'write';
23+
public const ADMIN = 'admin';
24+
public const SUPERUSER = 'superuser';
25+
26+
/**
27+
* @return list<string>
28+
*/
29+
public static function getConfigurableLevels(): array
30+
{
31+
return [
32+
self::UNLIMITED,
33+
self::VIEW,
34+
self::WRITE,
35+
self::ADMIN,
36+
];
37+
}
38+
39+
public static function normalizeMaximumAllowed(mixed $value): string
40+
{
41+
if (!is_scalar($value)) {
42+
return self::UNLIMITED;
43+
}
44+
45+
$normalizedValue = strtolower(trim((string) $value));
46+
47+
return in_array($normalizedValue, self::getConfigurableLevels(), true)
48+
? $normalizedValue
49+
: self::UNLIMITED;
50+
}
51+
52+
public static function resolveCurrentUserLevel(): string
53+
{
54+
$access = Access::getInstance();
55+
56+
if ($access->hasSuperUserAccess()) {
57+
return self::SUPERUSER;
58+
}
59+
60+
if (Piwik::isUserHasSomeAdminAccess()) {
61+
return self::ADMIN;
62+
}
63+
64+
if (Piwik::isUserHasSomeWriteAccess()) {
65+
return self::WRITE;
66+
}
67+
68+
return self::VIEW;
69+
}
70+
71+
public static function exceedsMaximumAllowed(string $currentLevel, string $maximumAllowedLevel): bool
72+
{
73+
$normalizedMaximum = self::normalizeMaximumAllowed($maximumAllowedLevel);
74+
75+
if ($normalizedMaximum === self::UNLIMITED) {
76+
return false;
77+
}
78+
79+
return self::getRank($currentLevel) > self::getRank($normalizedMaximum);
80+
}
81+
82+
public static function getDisplayName(string $level): string
83+
{
84+
return match ($level) {
85+
self::VIEW => 'View',
86+
self::WRITE => 'Write',
87+
self::ADMIN => 'Admin',
88+
self::SUPERUSER => 'Superuser',
89+
default => 'Unlimited',
90+
};
91+
}
92+
93+
public static function createTooHighPrivilegeMessage(string $maximumAllowedLevel): string
94+
{
95+
return sprintf(
96+
McpEndpointSpec::TOO_HIGH_PRIVILEGE_ERROR,
97+
self::getDisplayName(self::normalizeMaximumAllowed($maximumAllowedLevel))
98+
);
99+
}
100+
101+
private static function getRank(string $level): int
102+
{
103+
return match ($level) {
104+
self::VIEW => 1,
105+
self::WRITE => 2,
106+
self::ADMIN => 3,
107+
self::SUPERUSER => 4,
108+
default => 0,
109+
};
110+
}
111+
}

Support/Api/McpEndpointSpec.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,7 @@ final class McpEndpointSpec
2222
. 'Nested API calls (including API.getBulkRequest) are not supported.';
2323
public const UNAUTHORIZED_ERROR = 'Authentication required.';
2424
public const DISABLED_ERROR = 'MCP endpoint is disabled. Please contact your Matomo administrator.';
25+
public const TOO_HIGH_PRIVILEGE_ERROR =
26+
'Authenticated MCP access has too high privilege level. Maximum of %s access level is allowed.';
2527
public const INTERNAL_ERROR = 'Internal endpoint error.';
2628
}

SystemSettings.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Piwik\Settings\FieldConfig;
1616
use Piwik\Settings\Setting;
1717
use Piwik\SettingsPiwik;
18+
use Piwik\Plugins\McpServer\Support\Access\McpAccessLevel;
1819
use Piwik\Plugins\McpServer\Support\Access\RawApiAccessMode;
1920

2021
class SystemSettings extends \Piwik\Settings\Plugin\SystemSettings
@@ -26,6 +27,9 @@ class SystemSettings extends \Piwik\Settings\Plugin\SystemSettings
2627
/** @var Setting */
2728
public $enableMcp;
2829

30+
/** @var Setting */
31+
public $maximumMcpAccessLevel;
32+
2933
/** @var Setting */
3034
public $rawApiAccessScope;
3135

@@ -63,6 +67,27 @@ function (FieldConfig $field) {
6367
}
6468
);
6569

70+
$this->maximumMcpAccessLevel = $this->makeSetting(
71+
'maximum_mcp_access_level',
72+
McpAccessLevel::UNLIMITED,
73+
FieldConfig::TYPE_STRING,
74+
function (FieldConfig $field) {
75+
$field->title = Piwik::translate('McpServer_MaximumMcpAccessLevelTitle');
76+
$field->inlineHelp = implode('<br><br>', [
77+
Piwik::translate('McpServer_MaximumMcpAccessLevelHelpPurpose'),
78+
Piwik::translate('McpServer_MaximumMcpAccessLevelHelpTokens'),
79+
Piwik::translate('McpServer_MaximumMcpAccessLevelHelpSeparateUser'),
80+
]);
81+
$field->uiControl = FieldConfig::UI_CONTROL_SINGLE_SELECT;
82+
$field->availableValues = [
83+
McpAccessLevel::UNLIMITED => Piwik::translate('McpServer_MaximumMcpAccessLevelUnlimited'),
84+
McpAccessLevel::VIEW => Piwik::translate('McpServer_MaximumMcpAccessLevelView'),
85+
McpAccessLevel::WRITE => Piwik::translate('McpServer_MaximumMcpAccessLevelWrite'),
86+
McpAccessLevel::ADMIN => Piwik::translate('McpServer_MaximumMcpAccessLevelAdmin'),
87+
];
88+
}
89+
);
90+
6691
$sharedRawApiInlineHelp = implode('<br><br>', [
6792
Piwik::translate('McpServer_RawApiAccessHelpPurpose'),
6893
Piwik::translate('McpServer_RawApiAccessHelpReadFallback'),
@@ -137,6 +162,11 @@ public function isMcpEnabled(): bool
137162
return (bool) $this->enableMcp->getValue();
138163
}
139164

165+
public function getMaximumAllowedMcpAccessLevel(): string
166+
{
167+
return McpAccessLevel::normalizeMaximumAllowed($this->maximumMcpAccessLevel->getValue());
168+
}
169+
140170
public function getRawApiAccessMode(): string
141171
{
142172
$scope = $this->normalizeRawApiAccessScope($this->rawApiAccessScope->getValue());

docs/faq.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ Configure raw Matomo API tool access in **Administration -> System -> Plugin Set
3838
- Low-confidence or unclassified direct API methods require `Full API access`.
3939
- Direct API access can expose raw or personal data depending on enabled Matomo features. Review privacy and security requirements before enabling it, and consult your DPO or compliance owner when needed.
4040

41+
Configure MCP privilege limits in **Administration -> System -> Plugin Settings -> McpServer**:
42+
43+
- Use **Maximum allowed MCP privilege level** to deny MCP access for users authenticated with a higher Matomo privilege.
44+
- `No privilege limit` (default): follows the usual Matomo access model and does not add an extra MCP privilege cap.
45+
- `View access`, `Write access`, or `Admin access`: allows only users whose highest privilege across all sites is at or below the selected level.
46+
- For stricter separation, create a separate Matomo user or token with reduced permissions for MCP use.
47+
4148
## Enabling MCP
4249

4350
MCP access is disabled by default and must be enabled in **Administration -> System -> Plugin Settings -> McpServer**.
@@ -62,5 +69,5 @@ The plugin is focused on read-oriented analytics workflows. The exact tool surfa
6269
## Troubleshooting
6370

6471
- `401 Unauthorized`: verify the Bearer token is present, active, and belongs to a user with access to the requested site data.
65-
- `403 Forbidden`: if MCP is disabled, enable MCP in **Administration -> System -> Plugin Settings -> McpServer**. If MCP is already enabled, verify the authenticated Matomo user has access to the requested site or report data.
72+
- `403 Forbidden`: if MCP is disabled, enable MCP in **Administration -> System -> Plugin Settings -> McpServer**. If MCP is already enabled, verify the authenticated Matomo user has access to the requested site or report data and does not exceed the configured maximum MCP privilege level.
6673
- `400 Bad Request`: verify the client is using the exact MCP endpoint and is not proxying requests through `API.getBulkRequest`.

lang/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@
3232
"EnableMcpHelpPurpose": "Enable the Matomo MCP Server (Model Context Protocol) to allow AI tools and assistants to access analytics context from your Matomo instance.",
3333
"EnableMcpHelpUrl": "Your MCP URL: %1$s%2$s%3$s",
3434
"EnableMcpTitle": "Enable MCP Server (Model Context Protocol)",
35+
"MaximumMcpAccessLevelAdmin": "Admin access",
36+
"MaximumMcpAccessLevelHelpPurpose": "Choose the highest Matomo privilege level allowed to use the MCP endpoint. Users authenticated with a higher privilege level will be denied.",
37+
"MaximumMcpAccessLevelHelpSeparateUser": "If you need tighter isolation, create a separate Matomo user for MCP with only the required site permissions.",
38+
"MaximumMcpAccessLevelHelpTokens": "Use this setting to limit MCP access to lower-privilege users or tokens.",
39+
"MaximumMcpAccessLevelTitle": "Maximum allowed MCP privilege level",
40+
"MaximumMcpAccessLevelUnlimited": "No privilege limit",
41+
"MaximumMcpAccessLevelView": "View access",
42+
"MaximumMcpAccessLevelWrite": "Write access",
3543
"RawApiAccessHelpDataScope": "Direct Matomo API access can expose the same data available through the Matomo user interface or direct API endpoints, including raw or personal data when features such as the Visitor Log are enabled.",
3644
"RawApiAccessHelpDestructive": "Partial API access can enable create, update, and delete methods through the selected checkboxes below. Full API access can execute any allowed state-changing or destructive API methods, including actions that modify configuration or delete data.",
3745
"RawApiAccessHelpPolicy": "Before enabling direct API access, ensure this complies with your organization's privacy and security policies and applicable regulations. You may need approval from your data protection officer (DPO) or another compliance owner.",

0 commit comments

Comments
 (0)