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); +});