Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/fb-e2e-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -398,3 +413,4 @@ jobs:
setup_services: '--database psmdb'
pmm_test_flag: '@rta'
workers: 1

2 changes: 1 addition & 1 deletion codeceptjs-e2e/tests/configuration/permissions_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ Data(settingsReadOnly).Scenario(
Cookie: `pmm_session=${cookie.value}`,
});

assert.ok(r.status === 401);
assert.ok(r.status === 403);
},
);

Expand Down
1 change: 0 additions & 1 deletion codeceptjs-e2e/tests/pages/pmmDemoPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.")]',

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Unused.

title: '//span[contains(text(), "Percona Monitoring and Management")]',
failedSecurityChecks: '//span[contains(text(), "Failed security check")]',
noAccess: '$unauthorized',
Expand Down
4 changes: 4 additions & 0 deletions e2e_tests/helpers/apiEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
103 changes: 103 additions & 0 deletions e2e_tests/tests/api/alerting/alertingPermissions.test.ts
Original file line number Diff line number Diff line change
@@ -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-api',
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<string, string>): { label: string; send: () => Promise<APIResponse> }[] => [
{
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);
}
});
},
);
12 changes: 6 additions & 6 deletions e2e_tests/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading