From 2c7c2f3aaeb2ef35254b079aae7781ba57b99fe3 Mon Sep 17 00:00:00 2001 From: David Rajnoha Date: Mon, 9 Feb 2026 11:05:06 +0100 Subject: [PATCH] OU-1213: Add 403 permission denied mocking and access denied test Add cy.mockPermissionDenied() command to simulate 403 Forbidden responses from rules, silences, and Prometheus query endpoints. Add regression test verifying the Incidents page displays the access denied empty state. Co-authored-by: Cursor --- .../resources/htpasswd-secret.yaml | 19 ++++++ .../resources/limited-permissions-user.yaml | 31 +++++++++ .../resources/oauth-htpasswd.yaml | 17 +++++ .../tests/3.api_calls_data_loading_flows.md | 12 +++- .../regression/03.reg_api_calls.cy.ts | 44 +++++++++++-- .../prometheus-mocks.ts | 64 ++++++++++++++++++- 6 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 docs/incident_detection/resources/htpasswd-secret.yaml create mode 100644 docs/incident_detection/resources/limited-permissions-user.yaml create mode 100644 docs/incident_detection/resources/oauth-htpasswd.yaml diff --git a/docs/incident_detection/resources/htpasswd-secret.yaml b/docs/incident_detection/resources/htpasswd-secret.yaml new file mode 100644 index 000000000..1a435d27a --- /dev/null +++ b/docs/incident_detection/resources/htpasswd-secret.yaml @@ -0,0 +1,19 @@ +# HTPasswd Secret for creating a test user +# +# Before applying, generate the htpasswd file: +# htpasswd -c -B -b users.htpasswd testuser password123 +# +# Then base64 encode it: +# base64 -w0 users.htpasswd +# +# Replace the data.htpasswd value below with your encoded content + +apiVersion: v1 +kind: Secret +metadata: + name: htpass-secret + namespace: openshift-config +type: Opaque +data: + # base64 encoded: testuser:$2y$05$fBn5ChTgiV0A/6HEfoNKleU3CLVIWuV2816XVIsmmhwAz.fBpDObe + htpasswd: dGVzdHVzZXI6JDJ5JDA1JGZCbjVDaFRnaVYwQS82SEVmb05LbGVVM0NMVklXdVYyODE2WFZJc21taHdBei5mQnBET2JlCg== diff --git a/docs/incident_detection/resources/limited-permissions-user.yaml b/docs/incident_detection/resources/limited-permissions-user.yaml new file mode 100644 index 000000000..e5605990f --- /dev/null +++ b/docs/incident_detection/resources/limited-permissions-user.yaml @@ -0,0 +1,31 @@ +# Namespace and RoleBinding for testing limited permissions +# +# This creates a namespace with cluster-monitoring enabled and grants +# the testuser only monitoring-rules-view access (no full monitoring access) +# +# The user will receive 403 Forbidden when accessing: +# - /api/prometheus/api/v1/rules +# - /api/alertmanager/api/v2/silences +# - /api/prometheus/api/v1/query_range +# - /api/prometheus/api/v1/query + +apiVersion: v1 +kind: Namespace +metadata: + name: namespace-a + labels: + openshift.io/cluster-monitoring: "true" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: testuser-monitoring-view + namespace: namespace-a +subjects: +- kind: User + name: testuser + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: ClusterRole + name: monitoring-rules-view + apiGroup: rbac.authorization.k8s.io diff --git a/docs/incident_detection/resources/oauth-htpasswd.yaml b/docs/incident_detection/resources/oauth-htpasswd.yaml new file mode 100644 index 000000000..68fb0464f --- /dev/null +++ b/docs/incident_detection/resources/oauth-htpasswd.yaml @@ -0,0 +1,17 @@ +# OAuth configuration to use HTPasswd identity provider +# +# This configures OpenShift to authenticate users from the htpass-secret +# Apply htpasswd-secret.yaml first before applying this + +apiVersion: config.openshift.io/v1 +kind: OAuth +metadata: + name: cluster +spec: + identityProviders: + - name: my_htpasswd_provider + mappingMethod: claim + type: HTPasswd + htpasswd: + fileData: + name: htpass-secret diff --git a/docs/incident_detection/tests/3.api_calls_data_loading_flows.md b/docs/incident_detection/tests/3.api_calls_data_loading_flows.md index 1ae58d35f..fb95ab627 100644 --- a/docs/incident_detection/tests/3.api_calls_data_loading_flows.md +++ b/docs/incident_detection/tests/3.api_calls_data_loading_flows.md @@ -1,6 +1,6 @@ ## 3. CRITICAL: Data Loading – API Call Bugs -**Automation Status**: PARTIALLY AUTOMATED (Sections 3.1 and 3.2) +**Automation Status**: PARTIALLY AUTOMATED (Sections 3.1, 3.2, and 3.5) ### Prerequisites: Test Data Setup for Data Loading Tests @@ -91,4 +91,14 @@ start,end,alertname,namespace,severity,silenced,labels - [ ] Component lists combined for same group_id - [ ] Watchdog alerts filtered out +### 3.5 Permission Denied Handling +**BUG**: Page should gracefully handle 403 Forbidden responses from API endpoints. +**Automation Status**: AUTOMATED in `03.reg_api_calls.cy.ts` +- Uses mock: `cy.mockPermissionDenied({ rules: true, silences: true, prometheus: true })` +- Manual replication: Apply resources from [`docs/incident_detection/resources/`](../resources/) + +- [ ] **403 Forbidden Response**: Create user with limited permissions (testuser/password123) + - Apply: `htpasswd-secret.yaml`, `oauth-htpasswd.yaml`, `limited-permissions-user.yaml` + - Login as testuser, navigate to Observe → Incidents + - Expected: `` with "Restricted access" text diff --git a/web/cypress/e2e/incidents/regression/03.reg_api_calls.cy.ts b/web/cypress/e2e/incidents/regression/03.reg_api_calls.cy.ts index 11e23f65b..288a25f69 100644 --- a/web/cypress/e2e/incidents/regression/03.reg_api_calls.cy.ts +++ b/web/cypress/e2e/incidents/regression/03.reg_api_calls.cy.ts @@ -1,10 +1,14 @@ /* -Regression test for Silences Not Applied Correctly (Section 3.2) +Regression tests for API Calls and Data Loading (Section 3) -BUG: Silences were being matched by name only, not by name + namespace + severity. -This test verifies that silence matching uses: alertname + namespace + severity. +Tests: +1. Silences Not Applied Correctly (Section 3.2) + BUG: Silences were being matched by name only, not by name + namespace + severity. + This test verifies that silence matching uses: alertname + namespace + severity. -While targeting the bug, it verifies the basic Silences Implementation. +2. Permission Denied Handling (Section 3.5) + Tests graceful handling of 403 Forbidden responses from rules/silences endpoints. + Incidents page should still function when user lacks permissions to view rules/silences. Verifies: OU-1020, OU-706 */ @@ -125,4 +129,36 @@ describe('Regression: Silences Not Applied Correctly', { tags: ['@incidents'] }, }); }); +describe('Regression: Permission Denied Handling', { tags: ['@incidents'] }, () => { + before(() => { + cy.beforeBlockCOO(MCP, MP); + }); + + beforeEach(() => { + cy.log('Mock all API endpoints as 403 Forbidden'); + cy.mockPermissionDenied(); + cy.log('Navigate to Observe → Incidents'); + incidentsPage.goTo(); + }); + + it('Page displays access denied state when all API endpoints return 403 Forbidden', () => { + cy.log('1.1 Verify 403 requests were intercepted'); + const waitTimeout = { timeout: 120000 }; + cy.wait('@rulesPermissionDenied', waitTimeout) + .its('response').should('exist') + .its('statusCode').should('eq', 403); + cy.wait('@silencesPermissionDenied', waitTimeout) + .its('response').should('exist') + .its('statusCode').should('eq', 403); + cy.wait('@prometheusQueryRangePermissionDenied', waitTimeout) + .its('response').should('exist') + .its('statusCode').should('eq', 403); + + cy.log('1.3 Verify access denied empty state is displayed'); + cy.byTestID('access-denied').should('be.visible'); + cy.byTestID('access-denied').should('contain.text', 'You don\'t have access to this section due to cluster policy'); + + cy.log('Verified: Page displays restricted access state for permission denied'); + }); +}); \ No newline at end of file diff --git a/web/cypress/support/incidents_prometheus_query_mocks/prometheus-mocks.ts b/web/cypress/support/incidents_prometheus_query_mocks/prometheus-mocks.ts index bf35bb09d..c0a868c48 100644 --- a/web/cypress/support/incidents_prometheus_query_mocks/prometheus-mocks.ts +++ b/web/cypress/support/incidents_prometheus_query_mocks/prometheus-mocks.ts @@ -8,10 +8,17 @@ declare global { mockIncidents(incidents: IncidentDefinition[]): Chainable; mockIncidentFixture(fixturePath: string): Chainable; transformMetrics(): Chainable; + mockPermissionDenied(endpoints?: PermissionDeniedEndpoints): Chainable; } } } +export interface PermissionDeniedEndpoints { + rules?: boolean; + silences?: boolean; + prometheus?: boolean; +} + export const NEW_METRIC_NAME = 'cluster_health_components_map'; export const OLD_METRIC_NAME = 'cluster:health:components:map'; const MOCK_QUERY = '/api/prometheus/api/v1/query_range*'; @@ -147,4 +154,59 @@ Cypress.Commands.add('transformMetrics', () => { req.continue(); } }); -}); \ No newline at end of file +}); + +/** + * Mocks API endpoints to return 403 Forbidden responses. + * Useful for testing permission error handling in the Incidents page. + * + * @param endpoints - Configuration for which endpoints to mock as forbidden + * - rules: Mock /api/prometheus/api/v1/rules as 403 + * - silences: Mock /api/alertmanager/api/v2/silences as 403 + * - prometheus: Mock all Prometheus query endpoints as 403 + */ +export function mockPermissionDeniedResponses(endpoints: PermissionDeniedEndpoints = {}): void { + const { rules = true, silences = true, prometheus = true } = endpoints; + + const forbiddenResponse = { + statusCode: 403, + body: 'Forbidden', + headers: { + 'content-type': 'text/plain' + } + }; + + if (rules) { + cy.intercept('GET', '/api/prometheus/api/v1/rules*', (req) => { + Cypress.log({ name: '403', message: `${req.method} ${req.url}` }); + req.reply(forbiddenResponse); + }).as('rulesPermissionDenied'); + cy.log('Mocking /api/prometheus/api/v1/rules as 403 Forbidden'); + } + + if (silences) { + cy.intercept('GET', '/api/alertmanager/api/v2/silences*', (req) => { + Cypress.log({ name: '403', message: `${req.method} ${req.url}` }); + req.reply(forbiddenResponse); + }).as('silencesPermissionDenied'); + cy.log('Mocking /api/alertmanager/api/v2/silences as 403 Forbidden'); + } + + if (prometheus) { + cy.intercept('GET', MOCK_QUERY, (req) => { + Cypress.log({ name: '403', message: `${req.method} ${req.url}` }); + req.reply(forbiddenResponse); + }).as('prometheusQueryRangePermissionDenied'); + + cy.intercept('GET', /\/api\/prometheus\/api\/v1\/query\?.*/, (req) => { + Cypress.log({ name: '403', message: `${req.method} ${req.url}` }); + req.reply(forbiddenResponse); + }).as('prometheusQueryInstantPermissionDenied'); + cy.log('Mocking all Prometheus query endpoints as 403 Forbidden'); + } +} + +Cypress.Commands.add('mockPermissionDenied', (endpoints: PermissionDeniedEndpoints = {}) => { + cy.log('=== SETTING UP PERMISSION DENIED MOCKS ==='); + mockPermissionDeniedResponses(endpoints); +});