From 71314be570bc3c694e2a780311d2f25dbcc5c96f Mon Sep 17 00:00:00 2001
From: Alex Demidoff
Date: Sat, 13 Jun 2026 00:54:22 +0300
Subject: [PATCH 1/4] PMM-15138 Expect 403 (not 401) for viewer/editor settings
update
The PMM auth layer now returns 403 for authorization denials instead of 401.
---
codeceptjs-e2e/tests/configuration/permissions_test.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/codeceptjs-e2e/tests/configuration/permissions_test.js b/codeceptjs-e2e/tests/configuration/permissions_test.js
index f081e846..c480ee01 100644
--- a/codeceptjs-e2e/tests/configuration/permissions_test.js
+++ b/codeceptjs-e2e/tests/configuration/permissions_test.js
@@ -244,7 +244,7 @@ Data(settingsReadOnly).Scenario(
Cookie: `pmm_session=${cookie.value}`,
});
- assert.ok(r.status === 401);
+ assert.ok(r.status === 403);
},
);
From 13b71b41b1ea21eafab13013648576daab59ab04 Mon Sep 17 00:00:00 2001
From: Alex Demidoff
Date: Sat, 13 Jun 2026 11:57:41 +0300
Subject: [PATCH 2/4] PMM-15138 Removed unused field
---
codeceptjs-e2e/tests/pages/pmmDemoPage.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/codeceptjs-e2e/tests/pages/pmmDemoPage.js b/codeceptjs-e2e/tests/pages/pmmDemoPage.js
index c9c88570..103ec735 100644
--- a/codeceptjs-e2e/tests/pages/pmmDemoPage.js
+++ b/codeceptjs-e2e/tests/pages/pmmDemoPage.js
@@ -36,7 +36,6 @@ module.exports = {
privacy: '//a[contains(text(), "Privacy")]',
copyright: '//a[contains(text(), "Copyright")]',
legal: '//a[contains(text(), "Legal")]',
- accessDenied: '//div[contains(@class, "alert-title") and contains(text(), "Access denied.")]',
title: '//span[contains(text(), "Percona Monitoring and Management")]',
failedSecurityChecks: '//span[contains(text(), "Failed security check")]',
noAccess: '$unauthorized',
From cac009470c3faa9471e13a50b768a1a64b6abb6e Mon Sep 17 00:00:00 2001
From: Alex Demidoff
Date: Sat, 13 Jun 2026 12:56:54 +0300
Subject: [PATCH 3/4] PMM-15138 Add alerting authz tests
---
e2e_tests/helpers/apiEndpoints.ts | 4 +
.../api/alerting/alertingPermissions.test.ts | 103 ++++++++++++++++++
e2e_tests/tsconfig.json | 12 +-
3 files changed, 113 insertions(+), 6 deletions(-)
create mode 100644 e2e_tests/tests/api/alerting/alertingPermissions.test.ts
diff --git a/e2e_tests/helpers/apiEndpoints.ts b/e2e_tests/helpers/apiEndpoints.ts
index 7ffa35ed..877f8fac 100644
--- a/e2e_tests/helpers/apiEndpoints.ts
+++ b/e2e_tests/helpers/apiEndpoints.ts
@@ -3,6 +3,10 @@ const apiEndpoints = {
roles: '/v1/accesscontrol/roles',
rolesAssign: '/v1/accesscontrol/roles:assign',
},
+ alerting: {
+ rules: '/v1/alerting/rules',
+ templates: '/v1/alerting/templates',
+ },
backups: {
artifacts: '/v1/backups/artifacts',
locations: '/v1/backups/locations',
diff --git a/e2e_tests/tests/api/alerting/alertingPermissions.test.ts b/e2e_tests/tests/api/alerting/alertingPermissions.test.ts
new file mode 100644
index 00000000..2e4786ef
--- /dev/null
+++ b/e2e_tests/tests/api/alerting/alertingPermissions.test.ts
@@ -0,0 +1,103 @@
+import { APIResponse, expect } from '@playwright/test';
+import pmmTest from '@fixtures/pmmTest';
+import apiEndpoints from '@helpers/apiEndpoints';
+import GrafanaHelper from '@helpers/grafana.helper';
+
+// Set by the test, deleted by the afterEach hook so cleanup runs even on failure.
+let viewerId: number | undefined;
+let editorId: number | undefined;
+
+pmmTest.afterEach(async ({ request }) => {
+ const admin = GrafanaHelper.getAuthHeader();
+
+ if (viewerId) await request.delete(`graph/api/admin/users/${viewerId}`, { headers: admin });
+ if (editorId) await request.delete(`graph/api/admin/users/${editorId}`, { headers: admin });
+});
+
+// PMM-15138: a Viewer may list alert templates but must not create, update or delete
+// templates, nor create rules. An Editor must be allowed to manage them. The auth layer
+// denies a forbidden write with HTTP 403 / code 7 (PermissionDenied) before the request
+// reaches the backend, so the request body is irrelevant for the Viewer assertions.
+pmmTest(
+ 'PMM-15138 - Viewer cannot create/update/delete alert templates or rules; Editor can @alerting',
+ async ({ request }) => {
+ const suffix = Date.now();
+ const viewer = { login: `viewer-${suffix}`, password: 'Viewer-pw-12345' };
+ const editor = { login: `editor-${suffix}`, password: 'Editor-pw-12345' };
+ const admin = GrafanaHelper.getAuthHeader();
+
+ // Created users default to the Viewer org role; the editor is promoted below.
+ const createUser = async ({ login, password }: { login: string; password: string }) => {
+ const res = await request.post('graph/api/admin/users', {
+ data: { login, name: login, OrgId: 1, password },
+ headers: admin,
+ });
+
+ expect(res.status(), `create user ${login}`).toBe(200);
+
+ return (await res.json()).id as number;
+ };
+
+ const templateName = `pmm15138_${suffix}`;
+ const templatePath = `${apiEndpoints.alerting.templates}/${templateName}`;
+ // Deliberately invalid: the Viewer is rejected by auth first, the Editor by the backend.
+ const yamlBody = { yaml: 'placeholder' };
+
+ // Every write the ticket forbids for a Viewer, parameterised by the caller's auth header.
+ const writes = (headers: Record): { label: string; send: () => Promise }[] => [
+ {
+ label: 'POST templates',
+ send: () => request.post(apiEndpoints.alerting.templates, { data: yamlBody, headers }),
+ },
+ {
+ label: 'PUT template',
+ send: () => request.put(templatePath, { data: { name: templateName, ...yamlBody }, headers }),
+ },
+ {
+ label: 'DELETE template',
+ send: () => request.delete(templatePath, { headers }),
+ },
+ {
+ label: 'POST rule',
+ send: () => request.post(apiEndpoints.alerting.rules, { data: { template_name: templateName }, headers }),
+ },
+ ];
+
+ viewerId = await createUser(viewer);
+ editorId = await createUser(editor);
+
+ const roleRes = await request.patch(`graph/api/org/users/${editorId}`, {
+ data: { role: 'Editor' },
+ headers: admin,
+ });
+
+ expect(roleRes.status(), 'promote editor').toBe(200);
+
+ const asViewer = GrafanaHelper.getAuthHeader(viewer.login, viewer.password);
+ const asEditor = GrafanaHelper.getAuthHeader(editor.login, editor.password);
+
+ await pmmTest.step('Viewer can list alert templates', async () => {
+ const res = await request.get(apiEndpoints.alerting.templates, { headers: asViewer });
+
+ expect(res.status()).toBe(200);
+ });
+
+ await pmmTest.step('Viewer is denied every alerting write with 403', async () => {
+ for (const { label, send } of writes(asViewer)) {
+ const res = await send();
+
+ expect(res.status(), `viewer ${label}`).toBe(403);
+ // 7 == codes.PermissionDenied
+ expect((await res.json()).code, `viewer ${label} code`).toBe(7);
+ }
+ });
+
+ await pmmTest.step('Editor is authorized for every alerting write (not 403)', async () => {
+ for (const { label, send } of writes(asEditor)) {
+ const res = await send();
+
+ expect(res.status(), `editor ${label}`).not.toBe(403);
+ }
+ });
+ },
+);
diff --git a/e2e_tests/tsconfig.json b/e2e_tests/tsconfig.json
index f51cf29d..c9e16b28 100644
--- a/e2e_tests/tsconfig.json
+++ b/e2e_tests/tsconfig.json
@@ -17,22 +17,22 @@
"isolatedModules": true,
"paths": {
"@fixtures/*": [
- "fixtures/*"
+ "./fixtures/*"
],
"@interfaces/*": [
- "interfaces/*"
+ "./interfaces/*"
],
"@helpers/*": [
- "helpers/*"
+ "./helpers/*"
],
"@components/*": [
- "components/*"
+ "./components/*"
],
"@pages/*": [
- "pages/*"
+ "./pages/*"
],
"@api/*": [
- "api/*"
+ "./api/*"
],
"@valkey": [
"./pages/dashboards/valkey/index.ts"
From ced6519080bc8977cf14555f73b7065cd2b508ca Mon Sep 17 00:00:00 2001
From: Alex Demidoff
Date: Sat, 13 Jun 2026 19:41:01 +0300
Subject: [PATCH 4/4] PMM-15138 Add the test to the workflow
---
.github/workflows/fb-e2e-suite.yml | 16 ++++++++++++++++
.../api/alerting/alertingPermissions.test.ts | 2 +-
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/fb-e2e-suite.yml b/.github/workflows/fb-e2e-suite.yml
index 12f1391e..a60f88cd 100644
--- a/.github/workflows/fb-e2e-suite.yml
+++ b/.github/workflows/fb-e2e-suite.yml
@@ -165,6 +165,20 @@ jobs:
sha: ${{ inputs.sha || github.event.pull_request.head.sha || 'null' }}
setup_services: '--database mysql'
tags_for_tests: '@fb-alerting|@fb-settings'
+
+ # TODO: this section can accomodate the tests migrated from CodeceptJS to Playwright.
+ alerting_api:
+ name: Alerting permissions API tests
+ uses: ./.github/workflows/runner-e2e-tests-playwright.yml
+ secrets:
+ LAUNCHABLE_TOKEN: ${{ secrets.LAUNCHABLE_TOKEN }}
+ with:
+ pmm_server_version: ${{ inputs.pmm_server_image || 'perconalab/pmm-server:3-dev-latest' }}
+ pmm_client_version: ${{ inputs.pmm_client_version || 'latest-tarball' }}
+ launchable_confidence: ${{ inputs.launchable_confidence || '100%' }}
+ pmm_qa_branch: ${{ github.event_name == 'pull_request' && github.head_ref || (inputs.pmm_qa_branch || 'main') }}
+ setup_services: '-h'
+ pmm_test_flag: '@alerting-api'
user_and_password:
name: User with changed password UI tests
@@ -369,6 +383,7 @@ jobs:
pmm_qa_branch: ${{ github.event_name == 'pull_request' && github.head_ref || (inputs.pmm_qa_branch || github.event.inputs.pmm_qa_branch || 'main') }}
setup_services: '--database pdpgsql'
pmm_test_flag: '@docker-configuration'
+
nomad:
name: Nomad tests
uses: ./.github/workflows/runner-e2e-tests-codeceptjs.yml
@@ -398,3 +413,4 @@ jobs:
setup_services: '--database psmdb'
pmm_test_flag: '@rta'
workers: 1
+
diff --git a/e2e_tests/tests/api/alerting/alertingPermissions.test.ts b/e2e_tests/tests/api/alerting/alertingPermissions.test.ts
index 2e4786ef..2470cbd2 100644
--- a/e2e_tests/tests/api/alerting/alertingPermissions.test.ts
+++ b/e2e_tests/tests/api/alerting/alertingPermissions.test.ts
@@ -19,7 +19,7 @@ pmmTest.afterEach(async ({ request }) => {
// denies a forbidden write with HTTP 403 / code 7 (PermissionDenied) before the request
// reaches the backend, so the request body is irrelevant for the Viewer assertions.
pmmTest(
- 'PMM-15138 - Viewer cannot create/update/delete alert templates or rules; Editor can @alerting',
+ 'PMM-15138 - Viewer cannot create/update/delete alert templates or rules; Editor can @alerting-api',
async ({ request }) => {
const suffix = Date.now();
const viewer = { login: `viewer-${suffix}`, password: 'Viewer-pw-12345' };