Skip to content

Commit ab26a80

Browse files
hardenglclaude
andauthored
fix(cost-management): move data fetching server-side to eliminate token exposure and RBAC bypass (#2616)
* fix(cost-management): move data fetching server-side to eliminate token exposure and RBAC bypass Addresses security findings from the RHDH Cost Plugin threat model: - Removed /token endpoint that exposed SSO credentials to the browser - Added secure backend proxy (/api/cost-management/proxy/*) with server-side RBAC enforcement and internal SSO token management - Removed dangerously-allow-unauthenticated proxy configuration - Updated frontend clients to route through the secure backend proxy FLPATH-3487 Made-with: Cursor * docs(cost-management): update documentation for server-side proxy architecture - Remove all references to dangerously-allow-unauthenticated proxy configuration - Document new secure backend proxy architecture and endpoints - Update static and dynamic plugin setup instructions - Add server-side RBAC enforcement explanation to rbac.md Made-with: Cursor * fix(cost-management): address review - path traversal and encoded RBAC bypass - Reject proxyPath containing dot-segments (../) or leading slashes to prevent path traversal beyond /cost-management/v1/ - Post-construction check ensures resolved pathname stays under the base path - Decode query parameter keys before RBAC matching so percent-encoded variants (e.g. filter%5Bexact%3Acluster%5D) are correctly stripped - Replace regex-based stripping with Set-based decoded key lookup Made-with: Cursor * refactor(cost-management): extract common RBAC resolution to reduce duplication Extract resolveAccessForSection() to eliminate structural duplication between resolveOptimizationsAccess and resolveCostManagementAccess. Each section now provides only a data-fetching callback while the common authorize-cache-filter logic lives in one place. Made-with: Cursor * fix(cost-management): harden secure proxy — hardcode GET method and declare config schema Defense-in-depth: hardcode fetch method to 'GET' instead of passing req.method, preventing SSRF amplification if the route registration ever changes from router.get to router.all. Add costManagementProxyBaseUrl to config.d.ts with @visibility backend so the config key is schema-validated and documented. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(cost-management): reduce secureProxy cognitive complexity Extract three helper functions from the secureProxy handler to bring cognitive complexity from 24 down below the SonarQube threshold of 15: - isPathTraversal: path validation guard - parseClientQueryParams: query string parsing with RBAC key stripping - injectRbacFilters: server-side RBAC filter injection Made-with: Cursor * docs(cost-management): update dynamic plugin config with full route bindings Add missing OpenShift route, menu hierarchy, and explain why the proxy.endpoints config was removed (replaced by server-side secure proxy). Made-with: Cursor * docs(cost-management): align dynamic-plugin.md with verified deployment config Update the ConfigMap example to exactly match the working configuration deployed on ocp-edge73: icon reference name costManagementIcon and dot-separated menuItems keys (cost-management.optimizations). Made-with: Cursor --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3f740e4 commit ab26a80

15 files changed

Lines changed: 604 additions & 444 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@red-hat-developer-hub/plugin-cost-management-backend': minor
3+
'@red-hat-developer-hub/plugin-cost-management-common': minor
4+
'@red-hat-developer-hub/plugin-cost-management': minor
5+
---
6+
7+
Move Cost Management data fetching server-side to eliminate token exposure and RBAC bypass
8+
9+
- Added secure backend proxy (`/api/cost-management/proxy/*`) that authenticates requests via Backstage httpAuth, checks RBAC permissions, retrieves SSO tokens internally, and injects server-side cluster/project filters before forwarding to the Cost Management API
10+
- Removed `/token` endpoint that exposed SSO service account credentials to the browser
11+
- Removed `dangerously-allow-unauthenticated` proxy configuration from `app-config.dynamic.yaml`
12+
- Updated `OptimizationsClient` and `CostManagementSlimClient` to route through the new secure backend proxy instead of the old Backstage proxy
13+
- Eliminated client-side RBAC filter injection that could be bypassed by calling the proxy directly

workspaces/cost-management/docs/dynamic-plugin.md

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,32 +33,50 @@ The procedure involves the following steps:
3333
```yaml
3434
# Add to dynamic-plugins-rhdh ConfigMap
3535

36-
- package: oci://quay.io/redhat-resource-optimization/dynamic-plugins:1.2.0!red-hat-developer-hub-plugin-cost-management
37-
disabled: false
38-
pluginConfig:
39-
dynamicPlugins:
40-
frontend:
41-
backstage-community.plugin-cost-management:
42-
appIcons:
43-
- name: costManagementIconOutlined
44-
importName: CostManagementIconOutlined
45-
dynamicRoutes:
46-
- path: /cost-management/optimizations
47-
importName: ResourceOptimizationPage
48-
menuItem:
49-
icon: costManagementIconOutlined
50-
text: Optimizations
51-
- package: oci://quay.io/redhat-resource-optimization/dynamic-plugins:1.2.0!red-hat-developer-hub-plugin-cost-management-backend
52-
disabled: false
53-
pluginConfig:
54-
proxy:
55-
endpoints:
56-
'/cost-management/v1':
57-
target: https://console.redhat.com/api/cost-management/v1
58-
allowedHeaders: ['Authorization']
59-
credentials: dangerously-allow-unauthenticated
60-
costManagement:
61-
clientId: ${CM_CLIENT_ID}
62-
clientSecret: ${CM_CLIENT_SECRET}
63-
optimizationWorkflowId: 'patch-k8s-resource'
36+
- package: oci://quay.io/redhat-resource-optimization/dynamic-plugins:latest!red-hat-developer-hub-plugin-cost-management
37+
disabled: false
38+
pluginConfig:
39+
dynamicPlugins:
40+
frontend:
41+
red-hat-developer-hub.plugin-cost-management:
42+
appIcons:
43+
- name: costManagementIcon
44+
importName: CostManagementIconOutlined
45+
dynamicRoutes:
46+
- path: /cost-management/optimizations
47+
importName: ResourceOptimizationPage
48+
menuItem:
49+
icon: costManagementIcon
50+
text: Optimizations
51+
- path: /cost-management/openshift
52+
importName: OpenShiftPage
53+
menuItem:
54+
icon: costManagementIcon
55+
text: OpenShift
56+
menuItems:
57+
cost-management:
58+
icon: costManagementIcon
59+
title: Cost management
60+
priority: 100
61+
cost-management.optimizations:
62+
parent: cost-management
63+
priority: 10
64+
cost-management.openshift:
65+
parent: cost-management
66+
priority: 20
67+
- package: oci://quay.io/redhat-resource-optimization/dynamic-plugins:latest!red-hat-developer-hub-plugin-cost-management-backend
68+
disabled: false
69+
pluginConfig:
70+
costManagement:
71+
clientId: ${CM_CLIENT_ID}
72+
clientSecret: ${CM_CLIENT_SECRET}
73+
optimizationWorkflowId: 'patch-k8s-resource'
6474
```
75+
76+
> **Note:** No `proxy` configuration is required. Previous versions required a
77+
> `proxy.endpoints['/cost-management/v1']` entry that forwarded requests to
78+
> `console.redhat.com` — this has been removed. The backend plugin now
79+
> communicates with the Red Hat Cost Management API server-side via a secure
80+
> proxy. SSO tokens are obtained internally via OAuth2 `client_credentials`
81+
> grant and never exposed to the browser. RBAC filtering is enforced
82+
> server-side before data is returned. See [rbac.md](./rbac.md) for details.

workspaces/cost-management/docs/rbac.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
1-
The Cost Management plugin protects its backend endpoints with the builtin permission mechanism and combines it with the RBAC plugin.
1+
The Cost Management plugin protects its backend endpoints with the builtin permission mechanism and combines it with the RBAC plugin. All permission checks are enforced **server-side** within the backend plugin's secure proxy — the frontend never receives data the user is not authorized to see, and RBAC filters cannot be bypassed by modifying client requests.
22

33
The Cost Management plugin consists of two main sections, each with its own set of permissions:
44

55
- **Optimizations**: Uses permissions starting with `ros.`
66
- **OpenShift**: Uses permissions starting with `cost.`
77

8+
### How it works
9+
10+
When a frontend request arrives at `/api/cost-management/proxy/*`, the backend:
11+
12+
1. Authenticates the user via Backstage `httpAuth`
13+
2. Evaluates the user's permissions against the `ros.*` or `cost.*` policy
14+
3. Determines the authorized list of clusters and projects
15+
4. Strips any client-supplied cluster/project filter parameters
16+
5. Injects the server-authorized filters before forwarding to the upstream API
17+
6. Returns only the data the user is permitted to see
18+
19+
This means granting `ros.demolab` only allows seeing data for the `demolab` cluster — the user cannot modify query parameters to access other clusters.
20+
821
## 1. Optimizations Section
922

1023
The Optimizations section allows users to view resource usage trends and optimization recommendations for workloads running on OpenShift clusters.

workspaces/cost-management/plugins/cost-management-backend/README.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,46 @@
11
# Resource Optimization back-end plugin
22

3-
Welcome to the cost-management backend plugin!
3+
The cost-management backend plugin provides a secure server-side proxy for the
4+
Red Hat Cost Management API. All communication with the upstream API happens
5+
within the backend — SSO tokens never leave the server and RBAC filtering is
6+
enforced before data is returned to the browser.
47

5-
_This plugin was created through the Backstage CLI_
8+
## Architecture
9+
10+
The plugin exposes a single catch-all route at `/api/cost-management/proxy/*`.
11+
When a request arrives the handler:
12+
13+
1. **Authenticates** the caller via Backstage `httpAuth` (requires a valid user session).
14+
2. **Checks permissions** through the Backstage permission framework using the
15+
`ros.*` and `cost.*` permission sets (see [docs/rbac.md](../../docs/rbac.md)).
16+
3. **Obtains an SSO token** internally via the OAuth2 `client_credentials` grant
17+
using the `costManagement.clientId` / `costManagement.clientSecret` from
18+
`app-config`.
19+
4. **Strips** any client-supplied RBAC-controlled query parameters (`cluster`,
20+
`project`, `filter[exact:cluster]`, `filter[exact:project]`).
21+
5. **Injects** server-authorised cluster/project filters from the permission
22+
decision.
23+
6. **Forwards** the request to the Red Hat Cost Management API and streams the
24+
response back.
25+
26+
### Endpoints
27+
28+
| Path | Auth | Description |
29+
| ---------------------------------- | ------------- | ----------------------------------- |
30+
| `GET /api/cost-management/health` | None | Health check |
31+
| `ALL /api/cost-management/proxy/*` | `user-cookie` | Secure proxy to Cost Management API |
32+
33+
### Configuration
34+
35+
```yaml
36+
# app-config.yaml
37+
costManagement:
38+
clientId: ${RHHCC_SA_CLIENT_ID}
39+
clientSecret: ${RHHCC_SA_CLIENT_SECRET}
40+
```
41+
42+
No `proxy` block with `dangerously-allow-unauthenticated` is needed — the
43+
backend plugin handles upstream communication directly.
644

745
## Getting started
846

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
proxy:
2-
endpoints:
3-
'/cost-management/v1':
4-
target: https://console.redhat.com/api/cost-management/v1
5-
allowedHeaders: ['Authorization']
6-
credentials: dangerously-allow-unauthenticated
71
costManagement:
82
clientId: ${CM_CLIENT_ID}
93
clientSecret: ${CM_CLIENT_SECRET}

workspaces/cost-management/plugins/cost-management-backend/config.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,13 @@ export interface Config {
2929
/** @visibility secret */
3030
clientSecret: string;
3131
};
32+
33+
/**
34+
* Base URL for the Cost Management API proxy target.
35+
*
36+
* @default "https://console.redhat.com/api"
37+
*
38+
* @visibility backend
39+
*/
40+
costManagementProxyBaseUrl?: string;
3241
}

workspaces/cost-management/plugins/cost-management-backend/src/plugin.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,15 @@ export const costManagementPlugin = createBackendPlugin({
6767
allow: 'unauthenticated',
6868
});
6969
httpRouter.addAuthPolicy({
70-
path: '/token',
70+
path: '/access',
7171
allow: 'user-cookie',
7272
});
7373
httpRouter.addAuthPolicy({
74-
path: '/access',
74+
path: '/access/cost-management',
7575
allow: 'user-cookie',
7676
});
7777
httpRouter.addAuthPolicy({
78-
path: '/access/cost-management',
78+
path: '/proxy',
7979
allow: 'user-cookie',
8080
});
8181
},

0 commit comments

Comments
 (0)