Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@red-hat-developer-hub/plugin-cost-management-common': minor
'@red-hat-developer-hub/plugin-cost-management-backend': minor
---

**BREAKING**: Changed permission name separator from `.` to `/` for cluster-specific and cluster-project-specific permissions.

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`.

New format:

- `ros/{clusterName}` and `ros/{clusterName}/{projectName}` (was `ros.{clusterName}` and `ros.{clusterName}.{projectName}`)
- `cost/{clusterName}` and `cost/{clusterName}/{projectName}` (was `cost.{clusterName}` and `cost.{clusterName}.{projectName}`)

Generic permissions (`ros.plugin`, `ros.apply`, `cost.plugin`) are unchanged.

See `docs/rbac.md` for a migration guide.
93 changes: 66 additions & 27 deletions workspaces/cost-management/docs/rbac.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ When a frontend request arrives at `/api/cost-management/proxy/*`, the backend:
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.
This means granting `ros/demolab` only allows seeing data for the `demolab` cluster — the user cannot modify query parameters to access other clusters.

### Apply Recommendation authorization

Expand All @@ -38,11 +38,11 @@ The Optimizations section allows users to view resource usage trends and optimiz
| Name | Resource Type | Policy | Description |
| --------------------------------- | ------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- |
| ros.plugin | - | read | Allows the user to access all optimization data in the Cost Management plugin |
| ros.[CLUSTER_NAME] | - | read | Allows the user to access optimization data for a specific Cluster in the Cost Management plugin |
| 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 |
| ros/[CLUSTER_NAME] | - | read | Allows the user to access optimization data for a specific Cluster in the Cost Management plugin |
| 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 |
| ros.apply | - | update | Allows the user to apply optimization recommendations via workflow execution |

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.
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.

## 2. OpenShift Section

Expand All @@ -53,10 +53,10 @@ The OpenShift section displays cost tracking for OpenShift clusters with flexibl
| Name | Resource Type | Policy | Description |
| ---------------------------------- | ------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- |
| cost.plugin | - | read | Allows the user to access all OpenShift cost data in the Cost Management plugin |
| cost.[CLUSTER_NAME] | - | read | Allows the user to access OpenShift cost data for a specific Cluster in the Cost Management plugin |
| 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 |
| cost/[CLUSTER_NAME] | - | read | Allows the user to access OpenShift cost data for a specific Cluster in the Cost Management plugin |
| 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 |

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.
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.

## Defining Policy File

Expand All @@ -73,14 +73,14 @@ p, role:default/rosUser, ros.plugin, read, deny
####
# Optimizations Section (ros.) - Cluster RBAC permissions
####
p, role:default/rosUser, ros.OpenShift on AWS, read, allow
p, role:default/rosUser, ros.demolab, read, allow
p, role:default/rosUser, ros/OpenShift on AWS, read, allow
p, role:default/rosUser, ros/demolab, read, allow

####
# Optimizations Section (ros.) - Cluster+Project RBAC permissions
####
p, role:default/rosUser, ros.demolab.thanos, read, allow
p, role:default/rosUser, ros.OpenShift on Azure.mobile, read, allow
p, role:default/rosUser, ros/demolab/thanos, read, allow
p, role:default/rosUser, ros/OpenShift on Azure/mobile, read, allow

####
# Optimizations Section (ros.) - Apply Recommendation permission
Expand All @@ -95,14 +95,14 @@ p, role:default/costUser, cost.plugin, read, deny
####
# OpenShift Section (cost.) - Cluster RBAC permissions
####
p, role:default/costUser, cost.OpenShift on AWS, read, allow
p, role:default/costUser, cost.demolab, read, allow
p, role:default/costUser, cost/OpenShift on AWS, read, allow
p, role:default/costUser, cost/demolab, read, allow

####
# OpenShift Section (cost.) - Cluster+Project RBAC permissions
####
p, role:default/costUser, cost.demolab.thanos, read, allow
p, role:default/costUser, cost.OpenShift on Azure.mobile, read, allow
p, role:default/costUser, cost/demolab/thanos, read, allow
p, role:default/costUser, cost/OpenShift on Azure/mobile, read, allow

g, user:default/preeti.exploring.life, role:default/rosUser
g, user:default/preeti.exploring.life, role:default/costUser
Expand All @@ -124,26 +124,26 @@ p, role:default/rosUser, ros.plugin, read, allow
g, user:default/test_user_1, role:default/rosUser
```

#### ros.[CLUSTER_NAME]
#### ros/[CLUSTER_NAME]

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

- 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.

```csv
p, role:default/rosClusterUser, ros.OpenShift on AWS, read, allow
p, role:default/rosClusterUser, ros/OpenShift on AWS, read, allow

g, user:default/test_user_2, role:default/rosClusterUser
```

#### ros.[CLUSTER_NAME].[PROJECT_NAME]
#### ros/[CLUSTER_NAME]/[PROJECT_NAME]

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

- 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.

```csv
p, role:default/rosClusterProjectUser, ros.demolab.thanos, read, allow
p, role:default/rosClusterProjectUser, ros/demolab/thanos, read, allow

g, user:default/test_user_3, role:default/rosClusterProjectUser
```
Expand Down Expand Up @@ -176,32 +176,71 @@ p, role:default/costUser, cost.plugin, read, allow
g, user:default/test_user_4, role:default/costUser
```

#### cost.[CLUSTER_NAME]
#### cost/[CLUSTER_NAME]

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

- 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.

```csv
p, role:default/costClusterUser, cost.OpenShift on AWS, read, allow
p, role:default/costClusterUser, cost/OpenShift on AWS, read, allow

g, user:default/test_user_5, role:default/costClusterUser
```

#### cost.[CLUSTER_NAME].[PROJECT_NAME]
#### cost/[CLUSTER_NAME]/[PROJECT_NAME]

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

- 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.

```csv
p, role:default/costClusterProjectUser, cost.demolab.thanos, read, allow
p, role:default/costClusterProjectUser, cost/demolab/thanos, read, allow

g, user:default/test_user_6, role:default/costClusterProjectUser
```

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

## Migration Guide: Permission Name Separator Change

> **Breaking change**: Cluster-specific and project-specific permission names now use `/` (slash) as the separator instead of `.` (dot).

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`?).

The `/` separator eliminates this ambiguity: `ros/my.cluster/project` is unambiguous.

### What to update

Replace all dot-separated cluster/project permission names in your RBAC policy CSV:

| Old format (dot separator) | New format (slash separator) |
| -------------------------------- | -------------------------------- |
| `ros.CLUSTER_NAME` | `ros/CLUSTER_NAME` |
| `ros.CLUSTER_NAME.PROJECT_NAME` | `ros/CLUSTER_NAME/PROJECT_NAME` |
| `cost.CLUSTER_NAME` | `cost/CLUSTER_NAME` |
| `cost.CLUSTER_NAME.PROJECT_NAME` | `cost/CLUSTER_NAME/PROJECT_NAME` |

> **Note**: The generic `ros.plugin` and `cost.plugin` permissions are unchanged — only cluster/project-specific permissions use the new format.

### Example migration

**Before:**

```csv
p, role:default/rosUser, ros.demolab, read, allow
p, role:default/rosUser, ros.demolab.thanos, read, allow
p, role:default/costUser, cost.OpenShift on AWS, read, allow
```

**After:**

```csv
p, role:default/rosUser, ros/demolab, read, allow
p, role:default/rosUser, ros/demolab/thanos, read, allow
p, role:default/costUser, cost/OpenShift on AWS, read, allow
```

## Enable permissions

To enable permissions, you need to add the following in the [app-config file](../app-config.yaml):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const getCostManagementAccess: (
return response.json(body);
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,32 +198,51 @@ async function resolveCostManagementAccess(
'cost',
{ clusterKey: 'cost_clusters', projectKey: 'cost_projects' },
async opts => {
const token = await getTokenFromApi(opts);
const [clustersResp, projectsResp] = await Promise.all([
opts.costManagementApi.searchOpenShiftClusters('', {
token,
limit: 1000,
}),
opts.costManagementApi.searchOpenShiftProjects('', {
token,
limit: 1000,
}),
]);

const clustersData = await clustersResp.json();
const projectsData = await projectsResp.json();
let token: string;
try {
token = await getTokenFromApi(opts);
} catch {
return null;
}

const clusters: Record<string, string> = {};
clustersData.data?.forEach(
(c: { value: string; cluster_alias: string }) => {
if (c.cluster_alias && c.value) clusters[c.cluster_alias] = c.value;
},
);
const projects = [
...new Set(projectsData.data?.map((p: { value: string }) => p.value)),
].filter((p): p is string => p !== undefined);
try {
const [clustersResp, projectsResp] = await Promise.all([
opts.costManagementApi.searchOpenShiftClusters('', {
token,
limit: 1000,
}),
opts.costManagementApi.searchOpenShiftProjects('', {
token,
limit: 1000,
}),
]);

const clustersData = await clustersResp.json();
const projectsData = await projectsResp.json();

if (
(clustersData as any).errors ||
(projectsData as any).errors ||
!clustersData.data ||
!projectsData.data
) {
return null;
}

return { clusters, projects };
const clusters: Record<string, string> = {};
clustersData.data.forEach(
(c: { value: string; cluster_alias: string }) => {
if (c.cluster_alias && c.value) clusters[c.cluster_alias] = c.value;
},
);
const projects = [
...new Set(projectsData.data.map((p: { value: string }) => p.value)),
].filter((p): p is string => p !== undefined);

return { clusters, projects };
} catch {
return null;
}
},
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ export const authorize = async (
* Uses permission hierarchy where project-level access also grants cluster access.
*
* Permission Hierarchy:
* - cost.{cluster} → grants access to cluster + ALL projects in that cluster
* - cost.{cluster}.{project} → grants access to cluster + that specific project
* - cost/{cluster} → grants access to cluster + ALL projects in that cluster
* - cost/{cluster}/{project} → grants access to cluster + that specific project
*
* @param request - The HTTP request
* @param permissionsSvc - The permissions service
Expand Down
Loading
Loading