Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@red-hat-developer-hub/plugin-cost-management-backend': minor
'@red-hat-developer-hub/plugin-cost-management-common': minor
'@red-hat-developer-hub/plugin-cost-management': minor
---

Move Cost Management data fetching server-side to eliminate token exposure and RBAC bypass

- 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
- Removed `/token` endpoint that exposed SSO service account credentials to the browser
- Removed `dangerously-allow-unauthenticated` proxy configuration from `app-config.dynamic.yaml`
- Updated `OptimizationsClient` and `CostManagementSlimClient` to route through the new secure backend proxy instead of the old Backstage proxy
- Eliminated client-side RBAC filter injection that could be bypassed by calling the proxy directly
74 changes: 46 additions & 28 deletions workspaces/cost-management/docs/dynamic-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,32 +33,50 @@ The procedure involves the following steps:
```yaml
# Add to dynamic-plugins-rhdh ConfigMap

- package: oci://quay.io/redhat-resource-optimization/dynamic-plugins:1.2.0!red-hat-developer-hub-plugin-cost-management
disabled: false
pluginConfig:
dynamicPlugins:
frontend:
backstage-community.plugin-cost-management:
appIcons:
- name: costManagementIconOutlined
importName: CostManagementIconOutlined
dynamicRoutes:
- path: /cost-management/optimizations
importName: ResourceOptimizationPage
menuItem:
icon: costManagementIconOutlined
text: Optimizations
- package: oci://quay.io/redhat-resource-optimization/dynamic-plugins:1.2.0!red-hat-developer-hub-plugin-cost-management-backend
disabled: false
pluginConfig:
proxy:
endpoints:
'/cost-management/v1':
target: https://console.redhat.com/api/cost-management/v1
allowedHeaders: ['Authorization']
credentials: dangerously-allow-unauthenticated
costManagement:
clientId: ${CM_CLIENT_ID}
clientSecret: ${CM_CLIENT_SECRET}
optimizationWorkflowId: 'patch-k8s-resource'
- package: oci://quay.io/redhat-resource-optimization/dynamic-plugins:latest!red-hat-developer-hub-plugin-cost-management
disabled: false
pluginConfig:
dynamicPlugins:
frontend:
red-hat-developer-hub.plugin-cost-management:
appIcons:
- name: costManagementIcon
importName: CostManagementIconOutlined
dynamicRoutes:
- path: /cost-management/optimizations
importName: ResourceOptimizationPage
menuItem:
icon: costManagementIcon
text: Optimizations
- path: /cost-management/openshift
importName: OpenShiftPage
menuItem:
icon: costManagementIcon
text: OpenShift
menuItems:
cost-management:
icon: costManagementIcon
title: Cost management
priority: 100
cost-management.optimizations:
parent: cost-management
priority: 10
cost-management.openshift:
parent: cost-management
priority: 20
- package: oci://quay.io/redhat-resource-optimization/dynamic-plugins:latest!red-hat-developer-hub-plugin-cost-management-backend
disabled: false
pluginConfig:
costManagement:
clientId: ${CM_CLIENT_ID}
clientSecret: ${CM_CLIENT_SECRET}
optimizationWorkflowId: 'patch-k8s-resource'
```

> **Note:** No `proxy` configuration is required. Previous versions required a
> `proxy.endpoints['/cost-management/v1']` entry that forwarded requests to
> `console.redhat.com` — this has been removed. The backend plugin now
> communicates with the Red Hat Cost Management API server-side via a secure
> proxy. SSO tokens are obtained internally via OAuth2 `client_credentials`
> grant and never exposed to the browser. RBAC filtering is enforced
> server-side before data is returned. See [rbac.md](./rbac.md) for details.
15 changes: 14 additions & 1 deletion workspaces/cost-management/docs/rbac.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
The Cost Management plugin protects its backend endpoints with the builtin permission mechanism and combines it with the RBAC plugin.
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.

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

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

### How it works

When a frontend request arrives at `/api/cost-management/proxy/*`, the backend:

1. Authenticates the user via Backstage `httpAuth`
2. Evaluates the user's permissions against the `ros.*` or `cost.*` policy
3. Determines the authorized list of clusters and projects
4. Strips any client-supplied cluster/project filter parameters
5. Injects the server-authorized filters before forwarding to the upstream API
6. Returns only the data the user is permitted to see

This means granting `ros.demolab` only allows seeing data for the `demolab` cluster — the user cannot modify query parameters to access other clusters.

## 1. Optimizations Section

The Optimizations section allows users to view resource usage trends and optimization recommendations for workloads running on OpenShift clusters.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,46 @@
# Resource Optimization back-end plugin

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

_This plugin was created through the Backstage CLI_
## Architecture

The plugin exposes a single catch-all route at `/api/cost-management/proxy/*`.
When a request arrives the handler:

1. **Authenticates** the caller via Backstage `httpAuth` (requires a valid user session).
2. **Checks permissions** through the Backstage permission framework using the
`ros.*` and `cost.*` permission sets (see [docs/rbac.md](../../docs/rbac.md)).
3. **Obtains an SSO token** internally via the OAuth2 `client_credentials` grant
using the `costManagement.clientId` / `costManagement.clientSecret` from
`app-config`.
4. **Strips** any client-supplied RBAC-controlled query parameters (`cluster`,
`project`, `filter[exact:cluster]`, `filter[exact:project]`).
5. **Injects** server-authorised cluster/project filters from the permission
decision.
6. **Forwards** the request to the Red Hat Cost Management API and streams the
response back.

### Endpoints

| Path | Auth | Description |
| ---------------------------------- | ------------- | ----------------------------------- |
| `GET /api/cost-management/health` | None | Health check |
| `ALL /api/cost-management/proxy/*` | `user-cookie` | Secure proxy to Cost Management API |

### Configuration

```yaml
# app-config.yaml
costManagement:
clientId: ${RHHCC_SA_CLIENT_ID}
clientSecret: ${RHHCC_SA_CLIENT_SECRET}
```

No `proxy` block with `dangerously-allow-unauthenticated` is needed — the
backend plugin handles upstream communication directly.

## Getting started

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
proxy:
endpoints:
'/cost-management/v1':
target: https://console.redhat.com/api/cost-management/v1
allowedHeaders: ['Authorization']
credentials: dangerously-allow-unauthenticated
costManagement:
clientId: ${CM_CLIENT_ID}
clientSecret: ${CM_CLIENT_SECRET}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,13 @@ export interface Config {
/** @visibility secret */
clientSecret: string;
};

/**
* Base URL for the Cost Management API proxy target.
*
* @default "https://console.redhat.com/api"
*
* @visibility backend
*/
costManagementProxyBaseUrl?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,15 @@ export const costManagementPlugin = createBackendPlugin({
allow: 'unauthenticated',
});
httpRouter.addAuthPolicy({
path: '/token',
path: '/access',
allow: 'user-cookie',
});
httpRouter.addAuthPolicy({
path: '/access',
path: '/access/cost-management',
allow: 'user-cookie',
});
httpRouter.addAuthPolicy({
path: '/access/cost-management',
path: '/proxy',
allow: 'user-cookie',
});
},
Expand Down
Loading
Loading