Skip to content

Commit 7b7bab9

Browse files
hardenglclaude
andauthored
fix(cost-management): change permission name separator from dot to slash (#2620)
* fix(cost-management): change permission name separator from dot to slash Cluster-specific and project-specific permissions now use / instead of . as the separator (e.g. ros/my.cluster/project instead of ros.my.cluster.project), resolving ambiguity when cluster names contain dots. Generic permissions (ros.plugin, ros.apply, cost.plugin) are unchanged. Includes migration guide in docs/rbac.md. FLPATH-3489 Made-with: Cursor * fix: return 403 instead of 500 for OpenShift tab when user lacks cost.plugin permission resolveCostManagementAccess data fetcher did not handle errors from the upstream cost-management API, causing unhandled exceptions to propagate as 500 Internal Server Error. The ROS (optimizations) fetcher already returned null on error, which resolveAccessForSection maps to DENY/403. Apply the same pattern: wrap token fetch and API calls in try/catch, check for error responses, and return null so the RBAC flow correctly returns 403 Forbidden. Made-with: Cursor * fix: disable Apply Recommendation button when user lacks ros.apply permission The button was only disabled when the workflow was unavailable, not when the user lacked the ros.apply RBAC permission. Added usePermission hook to check ros.apply on the frontend and disable the button with a tooltip explaining the lack of permission. Also surface backend 403 errors via ResponseErrorPanel instead of silently swallowing them in the catch handler. Made-with: Cursor * fix(cost-management): update stale comments to use slash separator format Update comments in costManagementAccess.ts and checkPermissions.ts that still referenced the old dot-separator format (cost.{cluster}) to use the new slash format (cost/{cluster}) matching the actual permission implementation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(cost-management): update yarn.lock for permission-react dependency Made-with: Cursor --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b2fe9d7 commit 7b7bab9

11 files changed

Lines changed: 288 additions & 67 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@red-hat-developer-hub/plugin-cost-management-common': minor
3+
'@red-hat-developer-hub/plugin-cost-management-backend': minor
4+
---
5+
6+
**BREAKING**: Changed permission name separator from `.` to `/` for cluster-specific and cluster-project-specific permissions.
7+
8+
This resolves an ambiguity where dotted cluster names (e.g., `my.cluster`) could not be distinguished from the separator in permission names like `ros.my.cluster.project`.
9+
10+
New format:
11+
12+
- `ros/{clusterName}` and `ros/{clusterName}/{projectName}` (was `ros.{clusterName}` and `ros.{clusterName}.{projectName}`)
13+
- `cost/{clusterName}` and `cost/{clusterName}/{projectName}` (was `cost.{clusterName}` and `cost.{clusterName}.{projectName}`)
14+
15+
Generic permissions (`ros.plugin`, `ros.apply`, `cost.plugin`) are unchanged.
16+
17+
See `docs/rbac.md` for a migration guide.

workspaces/cost-management/docs/rbac.md

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ When a frontend request arrives at `/api/cost-management/proxy/*`, the backend:
1616
5. Injects the server-authorized filters before forwarding to the upstream API
1717
6. Returns only the data the user is permitted to see
1818

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

2121
### Apply Recommendation authorization
2222

@@ -38,11 +38,11 @@ The Optimizations section allows users to view resource usage trends and optimiz
3838
| Name | Resource Type | Policy | Description |
3939
| --------------------------------- | ------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- |
4040
| ros.plugin | - | read | Allows the user to access all optimization data in the Cost Management plugin |
41-
| ros.[CLUSTER_NAME] | - | read | Allows the user to access optimization data for a specific Cluster in the Cost Management plugin |
42-
| ros.[CLUSTER_NAME].[PROJECT_NAME] | - | read | Allows the user to access optimization data for a specific Project within a specific Cluster in the Cost Management plugin |
41+
| ros/[CLUSTER_NAME] | - | read | Allows the user to access optimization data for a specific Cluster in the Cost Management plugin |
42+
| ros/[CLUSTER_NAME]/[PROJECT_NAME] | - | read | Allows the user to access optimization data for a specific Project within a specific Cluster in the Cost Management plugin |
4343
| ros.apply | - | update | Allows the user to apply optimization recommendations via workflow execution |
4444

45-
The user is permitted to do an action if either the generic permission or the specific one allows it. In other words, it is not possible to grant generic ros.plugin and then selectively disable it for a specific cluster via ros.[CLUSTER_NAME] with deny.
45+
The user is permitted to do an action if either the generic permission or the specific one allows it. In other words, it is not possible to grant generic ros.plugin and then selectively disable it for a specific cluster via ros/[CLUSTER_NAME] with deny.
4646

4747
## 2. OpenShift Section
4848

@@ -53,10 +53,10 @@ The OpenShift section displays cost tracking for OpenShift clusters with flexibl
5353
| Name | Resource Type | Policy | Description |
5454
| ---------------------------------- | ------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- |
5555
| cost.plugin | - | read | Allows the user to access all OpenShift cost data in the Cost Management plugin |
56-
| cost.[CLUSTER_NAME] | - | read | Allows the user to access OpenShift cost data for a specific Cluster in the Cost Management plugin |
57-
| cost.[CLUSTER_NAME].[PROJECT_NAME] | - | read | Allows the user to access OpenShift cost data for a specific Project within a specific Cluster in the Cost Management plugin |
56+
| cost/[CLUSTER_NAME] | - | read | Allows the user to access OpenShift cost data for a specific Cluster in the Cost Management plugin |
57+
| cost/[CLUSTER_NAME]/[PROJECT_NAME] | - | read | Allows the user to access OpenShift cost data for a specific Project within a specific Cluster in the Cost Management plugin |
5858

59-
The user is permitted to do an action if either the generic permission or the specific one allows it. In other words, it is not possible to grant generic cost.plugin and then selectively disable it for a specific cluster via cost.[CLUSTER_NAME] with deny.
59+
The user is permitted to do an action if either the generic permission or the specific one allows it. In other words, it is not possible to grant generic cost.plugin and then selectively disable it for a specific cluster via cost/[CLUSTER_NAME] with deny.
6060

6161
## Defining Policy File
6262

@@ -73,14 +73,14 @@ p, role:default/rosUser, ros.plugin, read, deny
7373
####
7474
# Optimizations Section (ros.) - Cluster RBAC permissions
7575
####
76-
p, role:default/rosUser, ros.OpenShift on AWS, read, allow
77-
p, role:default/rosUser, ros.demolab, read, allow
76+
p, role:default/rosUser, ros/OpenShift on AWS, read, allow
77+
p, role:default/rosUser, ros/demolab, read, allow
7878
7979
####
8080
# Optimizations Section (ros.) - Cluster+Project RBAC permissions
8181
####
82-
p, role:default/rosUser, ros.demolab.thanos, read, allow
83-
p, role:default/rosUser, ros.OpenShift on Azure.mobile, read, allow
82+
p, role:default/rosUser, ros/demolab/thanos, read, allow
83+
p, role:default/rosUser, ros/OpenShift on Azure/mobile, read, allow
8484
8585
####
8686
# Optimizations Section (ros.) - Apply Recommendation permission
@@ -95,14 +95,14 @@ p, role:default/costUser, cost.plugin, read, deny
9595
####
9696
# OpenShift Section (cost.) - Cluster RBAC permissions
9797
####
98-
p, role:default/costUser, cost.OpenShift on AWS, read, allow
99-
p, role:default/costUser, cost.demolab, read, allow
98+
p, role:default/costUser, cost/OpenShift on AWS, read, allow
99+
p, role:default/costUser, cost/demolab, read, allow
100100
101101
####
102102
# OpenShift Section (cost.) - Cluster+Project RBAC permissions
103103
####
104-
p, role:default/costUser, cost.demolab.thanos, read, allow
105-
p, role:default/costUser, cost.OpenShift on Azure.mobile, read, allow
104+
p, role:default/costUser, cost/demolab/thanos, read, allow
105+
p, role:default/costUser, cost/OpenShift on Azure/mobile, read, allow
106106
107107
g, user:default/preeti.exploring.life, role:default/rosUser
108108
g, user:default/preeti.exploring.life, role:default/costUser
@@ -124,26 +124,26 @@ p, role:default/rosUser, ros.plugin, read, allow
124124
g, user:default/test_user_1, role:default/rosUser
125125
```
126126

127-
#### ros.[CLUSTER_NAME]
127+
#### ros/[CLUSTER_NAME]
128128

129-
Since the `test_user_2` user has the `default/rosClusterUser` role, which has `ros.OpenShift on AWS` permission, it can:
129+
Since the `test_user_2` user has the `default/rosClusterUser` role, which has `ros/OpenShift on AWS` permission, it can:
130130

131131
- See the list of records for `OpenShift on AWS` cluster for the service account which is specified in the [app-config file](../app-config.yaml) file using `clientId` and `clientSecret` and optimization recommendations for the same.
132132

133133
```csv
134-
p, role:default/rosClusterUser, ros.OpenShift on AWS, read, allow
134+
p, role:default/rosClusterUser, ros/OpenShift on AWS, read, allow
135135
136136
g, user:default/test_user_2, role:default/rosClusterUser
137137
```
138138

139-
#### ros.[CLUSTER_NAME].[PROJECT_NAME]
139+
#### ros/[CLUSTER_NAME]/[PROJECT_NAME]
140140

141-
Since the `test_user_3` user has the `default/rosClusterProjectUser` role, which has `ros.demolab.thanos` permission, it can:
141+
Since the `test_user_3` user has the `default/rosClusterProjectUser` role, which has `ros/demolab/thanos` permission, it can:
142142

143143
- See the list of records for `thanos` project under `demolab` cluster for the service account which is specified in the [app-config file](../app-config.yaml) file using `clientId` and `clientSecret` and optimization recommendations for the same.
144144

145145
```csv
146-
p, role:default/rosClusterProjectUser, ros.demolab.thanos, read, allow
146+
p, role:default/rosClusterProjectUser, ros/demolab/thanos, read, allow
147147
148148
g, user:default/test_user_3, role:default/rosClusterProjectUser
149149
```
@@ -176,32 +176,71 @@ p, role:default/costUser, cost.plugin, read, allow
176176
g, user:default/test_user_4, role:default/costUser
177177
```
178178

179-
#### cost.[CLUSTER_NAME]
179+
#### cost/[CLUSTER_NAME]
180180

181-
Since the `test_user_5` user has the `default/costClusterUser` role, which has `cost.OpenShift on AWS` permission, it can:
181+
Since the `test_user_5` user has the `default/costClusterUser` role, which has `cost/OpenShift on AWS` permission, it can:
182182

183183
- See the list of records for `OpenShift on AWS` cluster for the service account which is specified in the [app-config file](../app-config.yaml) file using `clientId` and `clientSecret` and cost data for the same.
184184

185185
```csv
186-
p, role:default/costClusterUser, cost.OpenShift on AWS, read, allow
186+
p, role:default/costClusterUser, cost/OpenShift on AWS, read, allow
187187
188188
g, user:default/test_user_5, role:default/costClusterUser
189189
```
190190

191-
#### cost.[CLUSTER_NAME].[PROJECT_NAME]
191+
#### cost/[CLUSTER_NAME]/[PROJECT_NAME]
192192

193-
Since the `test_user_6` user has the `default/costClusterProjectUser` role, which has `cost.demolab.thanos` permission, it can:
193+
Since the `test_user_6` user has the `default/costClusterProjectUser` role, which has `cost/demolab/thanos` permission, it can:
194194

195195
- See the list of records for `thanos` project under `demolab` cluster for the service account which is specified in the [app-config file](../app-config.yaml) file using `clientId` and `clientSecret` and cost data for the same.
196196

197197
```csv
198-
p, role:default/costClusterProjectUser, cost.demolab.thanos, read, allow
198+
p, role:default/costClusterProjectUser, cost/demolab/thanos, read, allow
199199
200200
g, user:default/test_user_6, role:default/costClusterProjectUser
201201
```
202202

203203
See https://casbin.org/docs/rbac for more information about casbin rules.
204204

205+
## Migration Guide: Permission Name Separator Change
206+
207+
> **Breaking change**: Cluster-specific and project-specific permission names now use `/` (slash) as the separator instead of `.` (dot).
208+
209+
Previous versions used a dot separator (e.g., `ros.demolab.thanos`), which caused ambiguity when cluster names themselves contained dots (e.g., `ros.my.cluster.project` — is the cluster `my.cluster` and the project `project`, or the cluster `my` and the project `cluster.project`?).
210+
211+
The `/` separator eliminates this ambiguity: `ros/my.cluster/project` is unambiguous.
212+
213+
### What to update
214+
215+
Replace all dot-separated cluster/project permission names in your RBAC policy CSV:
216+
217+
| Old format (dot separator) | New format (slash separator) |
218+
| -------------------------------- | -------------------------------- |
219+
| `ros.CLUSTER_NAME` | `ros/CLUSTER_NAME` |
220+
| `ros.CLUSTER_NAME.PROJECT_NAME` | `ros/CLUSTER_NAME/PROJECT_NAME` |
221+
| `cost.CLUSTER_NAME` | `cost/CLUSTER_NAME` |
222+
| `cost.CLUSTER_NAME.PROJECT_NAME` | `cost/CLUSTER_NAME/PROJECT_NAME` |
223+
224+
> **Note**: The generic `ros.plugin` and `cost.plugin` permissions are unchanged — only cluster/project-specific permissions use the new format.
225+
226+
### Example migration
227+
228+
**Before:**
229+
230+
```csv
231+
p, role:default/rosUser, ros.demolab, read, allow
232+
p, role:default/rosUser, ros.demolab.thanos, read, allow
233+
p, role:default/costUser, cost.OpenShift on AWS, read, allow
234+
```
235+
236+
**After:**
237+
238+
```csv
239+
p, role:default/rosUser, ros/demolab, read, allow
240+
p, role:default/rosUser, ros/demolab/thanos, read, allow
241+
p, role:default/costUser, cost/OpenShift on AWS, read, allow
242+
```
243+
205244
## Enable permissions
206245

207246
To enable permissions, you need to add the following in the [app-config file](../app-config.yaml):

workspaces/cost-management/plugins/cost-management-backend/src/routes/costManagementAccess.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export const getCostManagementAccess: (
6666
return response.json(body);
6767
}
6868

69-
// RBAC Filtering logic for Cluster & Project using cost.{clusterName} and cost.{clusterName}.{projectName} permissions
69+
// RBAC Filtering logic for Cluster & Project using cost/{clusterName} and cost/{clusterName}/{projectName} permissions
7070
let clusterDataMap: Record<string, string> = {};
7171
let allProjects: string[] = [];
7272

workspaces/cost-management/plugins/cost-management-backend/src/routes/secureProxy.ts

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -199,32 +199,51 @@ async function resolveCostManagementAccess(
199199
'cost',
200200
{ clusterKey: 'cost_clusters', projectKey: 'cost_projects' },
201201
async opts => {
202-
const token = await getTokenFromApi(opts);
203-
const [clustersResp, projectsResp] = await Promise.all([
204-
opts.costManagementApi.searchOpenShiftClusters('', {
205-
token,
206-
limit: 1000,
207-
}),
208-
opts.costManagementApi.searchOpenShiftProjects('', {
209-
token,
210-
limit: 1000,
211-
}),
212-
]);
213-
214-
const clustersData = await clustersResp.json();
215-
const projectsData = await projectsResp.json();
202+
let token: string;
203+
try {
204+
token = await getTokenFromApi(opts);
205+
} catch {
206+
return null;
207+
}
216208

217-
const clusters: Record<string, string> = {};
218-
clustersData.data?.forEach(
219-
(c: { value: string; cluster_alias: string }) => {
220-
if (c.cluster_alias && c.value) clusters[c.cluster_alias] = c.value;
221-
},
222-
);
223-
const projects = [
224-
...new Set(projectsData.data?.map((p: { value: string }) => p.value)),
225-
].filter((p): p is string => p !== undefined);
209+
try {
210+
const [clustersResp, projectsResp] = await Promise.all([
211+
opts.costManagementApi.searchOpenShiftClusters('', {
212+
token,
213+
limit: 1000,
214+
}),
215+
opts.costManagementApi.searchOpenShiftProjects('', {
216+
token,
217+
limit: 1000,
218+
}),
219+
]);
220+
221+
const clustersData = await clustersResp.json();
222+
const projectsData = await projectsResp.json();
223+
224+
if (
225+
(clustersData as any).errors ||
226+
(projectsData as any).errors ||
227+
!clustersData.data ||
228+
!projectsData.data
229+
) {
230+
return null;
231+
}
226232

227-
return { clusters, projects };
233+
const clusters: Record<string, string> = {};
234+
clustersData.data.forEach(
235+
(c: { value: string; cluster_alias: string }) => {
236+
if (c.cluster_alias && c.value) clusters[c.cluster_alias] = c.value;
237+
},
238+
);
239+
const projects = [
240+
...new Set(projectsData.data.map((p: { value: string }) => p.value)),
241+
].filter((p): p is string => p !== undefined);
242+
243+
return { clusters, projects };
244+
} catch {
245+
return null;
246+
}
228247
},
229248
);
230249
}

workspaces/cost-management/plugins/cost-management-backend/src/util/checkPermissions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ export const authorize = async (
8787
* Uses permission hierarchy where project-level access also grants cluster access.
8888
*
8989
* Permission Hierarchy:
90-
* - cost.{cluster} → grants access to cluster + ALL projects in that cluster
91-
* - cost.{cluster}.{project} → grants access to cluster + that specific project
90+
* - cost/{cluster} → grants access to cluster + ALL projects in that cluster
91+
* - cost/{cluster}/{project} → grants access to cluster + that specific project
9292
*
9393
* @param request - The HTTP request
9494
* @param permissionsSvc - The permissions service

0 commit comments

Comments
 (0)