Skip to content

Commit 2c7c2f3

Browse files
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 <cursoragent@cursor.com>
1 parent edce6c4 commit 2c7c2f3

6 files changed

Lines changed: 181 additions & 6 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# HTPasswd Secret for creating a test user
2+
#
3+
# Before applying, generate the htpasswd file:
4+
# htpasswd -c -B -b users.htpasswd testuser password123
5+
#
6+
# Then base64 encode it:
7+
# base64 -w0 users.htpasswd
8+
#
9+
# Replace the data.htpasswd value below with your encoded content
10+
11+
apiVersion: v1
12+
kind: Secret
13+
metadata:
14+
name: htpass-secret
15+
namespace: openshift-config
16+
type: Opaque
17+
data:
18+
# base64 encoded: testuser:$2y$05$fBn5ChTgiV0A/6HEfoNKleU3CLVIWuV2816XVIsmmhwAz.fBpDObe
19+
htpasswd: dGVzdHVzZXI6JDJ5JDA1JGZCbjVDaFRnaVYwQS82SEVmb05LbGVVM0NMVklXdVYyODE2WFZJc21taHdBei5mQnBET2JlCg==
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Namespace and RoleBinding for testing limited permissions
2+
#
3+
# This creates a namespace with cluster-monitoring enabled and grants
4+
# the testuser only monitoring-rules-view access (no full monitoring access)
5+
#
6+
# The user will receive 403 Forbidden when accessing:
7+
# - /api/prometheus/api/v1/rules
8+
# - /api/alertmanager/api/v2/silences
9+
# - /api/prometheus/api/v1/query_range
10+
# - /api/prometheus/api/v1/query
11+
12+
apiVersion: v1
13+
kind: Namespace
14+
metadata:
15+
name: namespace-a
16+
labels:
17+
openshift.io/cluster-monitoring: "true"
18+
---
19+
apiVersion: rbac.authorization.k8s.io/v1
20+
kind: RoleBinding
21+
metadata:
22+
name: testuser-monitoring-view
23+
namespace: namespace-a
24+
subjects:
25+
- kind: User
26+
name: testuser
27+
apiGroup: rbac.authorization.k8s.io
28+
roleRef:
29+
kind: ClusterRole
30+
name: monitoring-rules-view
31+
apiGroup: rbac.authorization.k8s.io
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# OAuth configuration to use HTPasswd identity provider
2+
#
3+
# This configures OpenShift to authenticate users from the htpass-secret
4+
# Apply htpasswd-secret.yaml first before applying this
5+
6+
apiVersion: config.openshift.io/v1
7+
kind: OAuth
8+
metadata:
9+
name: cluster
10+
spec:
11+
identityProviders:
12+
- name: my_htpasswd_provider
13+
mappingMethod: claim
14+
type: HTPasswd
15+
htpasswd:
16+
fileData:
17+
name: htpass-secret

docs/incident_detection/tests/3.api_calls_data_loading_flows.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## 3. CRITICAL: Data Loading – API Call Bugs
22

3-
**Automation Status**: PARTIALLY AUTOMATED (Sections 3.1 and 3.2)
3+
**Automation Status**: PARTIALLY AUTOMATED (Sections 3.1, 3.2, and 3.5)
44

55
### Prerequisites: Test Data Setup for Data Loading Tests
66

@@ -91,4 +91,14 @@ start,end,alertname,namespace,severity,silenced,labels
9191
- [ ] Component lists combined for same group_id
9292
- [ ] Watchdog alerts filtered out
9393

94+
### 3.5 Permission Denied Handling
95+
**BUG**: Page should gracefully handle 403 Forbidden responses from API endpoints.
96+
**Automation Status**: AUTOMATED in `03.reg_api_calls.cy.ts`
97+
- Uses mock: `cy.mockPermissionDenied({ rules: true, silences: true, prometheus: true })`
98+
- Manual replication: Apply resources from [`docs/incident_detection/resources/`](../resources/)
99+
100+
- [ ] **403 Forbidden Response**: Create user with limited permissions (testuser/password123)
101+
- Apply: `htpasswd-secret.yaml`, `oauth-htpasswd.yaml`, `limited-permissions-user.yaml`
102+
- Login as testuser, navigate to Observe → Incidents
103+
- Expected: `<EmptyState data-test="access-denied">` with "Restricted access" text
94104

web/cypress/e2e/incidents/regression/03.reg_api_calls.cy.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
/*
2-
Regression test for Silences Not Applied Correctly (Section 3.2)
2+
Regression tests for API Calls and Data Loading (Section 3)
33
4-
BUG: Silences were being matched by name only, not by name + namespace + severity.
5-
This test verifies that silence matching uses: alertname + namespace + severity.
4+
Tests:
5+
1. Silences Not Applied Correctly (Section 3.2)
6+
BUG: Silences were being matched by name only, not by name + namespace + severity.
7+
This test verifies that silence matching uses: alertname + namespace + severity.
68
7-
While targeting the bug, it verifies the basic Silences Implementation.
9+
2. Permission Denied Handling (Section 3.5)
10+
Tests graceful handling of 403 Forbidden responses from rules/silences endpoints.
11+
Incidents page should still function when user lacks permissions to view rules/silences.
812
913
Verifies: OU-1020, OU-706
1014
*/
@@ -125,4 +129,36 @@ describe('Regression: Silences Not Applied Correctly', { tags: ['@incidents'] },
125129
});
126130
});
127131

132+
describe('Regression: Permission Denied Handling', { tags: ['@incidents'] }, () => {
128133

134+
before(() => {
135+
cy.beforeBlockCOO(MCP, MP);
136+
});
137+
138+
beforeEach(() => {
139+
cy.log('Mock all API endpoints as 403 Forbidden');
140+
cy.mockPermissionDenied();
141+
cy.log('Navigate to Observe → Incidents');
142+
incidentsPage.goTo();
143+
});
144+
145+
it('Page displays access denied state when all API endpoints return 403 Forbidden', () => {
146+
cy.log('1.1 Verify 403 requests were intercepted');
147+
const waitTimeout = { timeout: 120000 };
148+
cy.wait('@rulesPermissionDenied', waitTimeout)
149+
.its('response').should('exist')
150+
.its('statusCode').should('eq', 403);
151+
cy.wait('@silencesPermissionDenied', waitTimeout)
152+
.its('response').should('exist')
153+
.its('statusCode').should('eq', 403);
154+
cy.wait('@prometheusQueryRangePermissionDenied', waitTimeout)
155+
.its('response').should('exist')
156+
.its('statusCode').should('eq', 403);
157+
158+
cy.log('1.3 Verify access denied empty state is displayed');
159+
cy.byTestID('access-denied').should('be.visible');
160+
cy.byTestID('access-denied').should('contain.text', 'You don\'t have access to this section due to cluster policy');
161+
162+
cy.log('Verified: Page displays restricted access state for permission denied');
163+
});
164+
});

web/cypress/support/incidents_prometheus_query_mocks/prometheus-mocks.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@ declare global {
88
mockIncidents(incidents: IncidentDefinition[]): Chainable<Element>;
99
mockIncidentFixture(fixturePath: string): Chainable<Element>;
1010
transformMetrics(): Chainable<Element>;
11+
mockPermissionDenied(endpoints?: PermissionDeniedEndpoints): Chainable<void>;
1112
}
1213
}
1314
}
1415

16+
export interface PermissionDeniedEndpoints {
17+
rules?: boolean;
18+
silences?: boolean;
19+
prometheus?: boolean;
20+
}
21+
1522
export const NEW_METRIC_NAME = 'cluster_health_components_map';
1623
export const OLD_METRIC_NAME = 'cluster:health:components:map';
1724
const MOCK_QUERY = '/api/prometheus/api/v1/query_range*';
@@ -147,4 +154,59 @@ Cypress.Commands.add('transformMetrics', () => {
147154
req.continue();
148155
}
149156
});
150-
});
157+
});
158+
159+
/**
160+
* Mocks API endpoints to return 403 Forbidden responses.
161+
* Useful for testing permission error handling in the Incidents page.
162+
*
163+
* @param endpoints - Configuration for which endpoints to mock as forbidden
164+
* - rules: Mock /api/prometheus/api/v1/rules as 403
165+
* - silences: Mock /api/alertmanager/api/v2/silences as 403
166+
* - prometheus: Mock all Prometheus query endpoints as 403
167+
*/
168+
export function mockPermissionDeniedResponses(endpoints: PermissionDeniedEndpoints = {}): void {
169+
const { rules = true, silences = true, prometheus = true } = endpoints;
170+
171+
const forbiddenResponse = {
172+
statusCode: 403,
173+
body: 'Forbidden',
174+
headers: {
175+
'content-type': 'text/plain'
176+
}
177+
};
178+
179+
if (rules) {
180+
cy.intercept('GET', '/api/prometheus/api/v1/rules*', (req) => {
181+
Cypress.log({ name: '403', message: `${req.method} ${req.url}` });
182+
req.reply(forbiddenResponse);
183+
}).as('rulesPermissionDenied');
184+
cy.log('Mocking /api/prometheus/api/v1/rules as 403 Forbidden');
185+
}
186+
187+
if (silences) {
188+
cy.intercept('GET', '/api/alertmanager/api/v2/silences*', (req) => {
189+
Cypress.log({ name: '403', message: `${req.method} ${req.url}` });
190+
req.reply(forbiddenResponse);
191+
}).as('silencesPermissionDenied');
192+
cy.log('Mocking /api/alertmanager/api/v2/silences as 403 Forbidden');
193+
}
194+
195+
if (prometheus) {
196+
cy.intercept('GET', MOCK_QUERY, (req) => {
197+
Cypress.log({ name: '403', message: `${req.method} ${req.url}` });
198+
req.reply(forbiddenResponse);
199+
}).as('prometheusQueryRangePermissionDenied');
200+
201+
cy.intercept('GET', /\/api\/prometheus\/api\/v1\/query\?.*/, (req) => {
202+
Cypress.log({ name: '403', message: `${req.method} ${req.url}` });
203+
req.reply(forbiddenResponse);
204+
}).as('prometheusQueryInstantPermissionDenied');
205+
cy.log('Mocking all Prometheus query endpoints as 403 Forbidden');
206+
}
207+
}
208+
209+
Cypress.Commands.add('mockPermissionDenied', (endpoints: PermissionDeniedEndpoints = {}) => {
210+
cy.log('=== SETTING UP PERMISSION DENIED MOCKS ===');
211+
mockPermissionDeniedResponses(endpoints);
212+
});

0 commit comments

Comments
 (0)