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' };