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
19 changes: 19 additions & 0 deletions docs/incident_detection/resources/htpasswd-secret.yaml
Original file line number Diff line number Diff line change
@@ -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==
31 changes: 31 additions & 0 deletions docs/incident_detection/resources/limited-permissions-user.yaml
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions docs/incident_detection/resources/oauth-htpasswd.yaml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 11 additions & 1 deletion docs/incident_detection/tests/3.api_calls_data_loading_flows.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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: `<EmptyState data-test="access-denied">` with "Restricted access" text

44 changes: 40 additions & 4 deletions web/cypress/e2e/incidents/regression/03.reg_api_calls.cy.ts
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -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');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,17 @@ declare global {
mockIncidents(incidents: IncidentDefinition[]): Chainable<Element>;
mockIncidentFixture(fixturePath: string): Chainable<Element>;
transformMetrics(): Chainable<Element>;
mockPermissionDenied(endpoints?: PermissionDeniedEndpoints): Chainable<void>;
}
}
}

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*';
Expand Down Expand Up @@ -147,4 +154,59 @@ Cypress.Commands.add('transformMetrics', () => {
req.continue();
}
});
});
});

/**
* 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);
});