@red-hat-developer-hub/plugin-cost-management-backend 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ When a request arrives the handler:
12
12
 
13
13
  1. **Authenticates** the caller via Backstage `httpAuth` (requires a valid user session).
14
14
  2. **Checks permissions** through the Backstage permission framework using the
15
- `ros.*` and `cost.*` permission sets (see [docs/rbac.md](../../docs/rbac.md)).
15
+ `ros.*` / `ros/…` and `cost.*` / `cost/…` permission sets (see [docs/rbac.md](../../docs/rbac.md)).
16
16
  3. **Obtains an SSO token** internally via the OAuth2 `client_credentials` grant
17
17
  using the `costManagement.clientId` / `costManagement.clientSecret` from
18
18
  `app-config`.
@@ -25,10 +25,13 @@ When a request arrives the handler:
25
25
 
26
26
  ### Endpoints
27
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 |
28
+ | Path | Auth | Description |
29
+ | ------------------------------------------------- | ------------- | ---------------------------------------------------------- |
30
+ | `GET /api/cost-management/health` | None | Health check |
31
+ | `GET /api/cost-management/proxy/*` | `user-cookie` | Secure proxy to Cost Management API |
32
+ | `GET /api/cost-management/access` | `user-cookie` | Check user's Optimizations RBAC access |
33
+ | `GET /api/cost-management/access/cost-management` | `user-cookie` | Check user's OpenShift cost RBAC access |
34
+ | `POST /api/cost-management/apply-recommendation` | `user-cookie` | Apply optimization via Orchestrator (requires `ros.apply`) |
32
35
 
33
36
  ### Configuration
34
37
 
@@ -42,6 +45,29 @@ costManagement:
42
45
  No `proxy` block with `dangerously-allow-unauthenticated` is needed — the
43
46
  backend plugin handles upstream communication directly.
44
47
 
48
+ ### Audit Logging
49
+
50
+ All backend endpoints emit structured audit log entries with user identity via Backstage's logger. Each entry includes:
51
+
52
+ - **`actor`** — the authenticated user's entity ref (e.g., `user:default/admin`)
53
+ - **`action`** — the operation performed (`data_access`, `apply_recommendation`, `access_check`)
54
+ - **`decision`** — the RBAC outcome (`ALLOW` or `DENY`)
55
+ - **`resource`** — the upstream API path accessed
56
+ - **`filters`** — the server-injected cluster/project filters (for proxy requests)
57
+
58
+ Example log entry:
59
+
60
+ ```json
61
+ {
62
+ "audit": true,
63
+ "actor": "user:default/admin",
64
+ "action": "data_access",
65
+ "resource": "recommendations/openshift",
66
+ "decision": "ALLOW",
67
+ "filters": { "clusters": ["cluster73"], "projects": ["rhdh"] }
68
+ }
69
+ ```
70
+
45
71
  ## Getting started
46
72
 
47
73
  The plugin has been added to the example app in this workspace, meaning you'll be able to access it by running `yarn
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './service/router';\nimport { optimizationServiceRef } from './service/optimizationsService';\nimport { costManagementServiceRef } from './service/costManagementService';\n\n/**\n * costManagementPlugin backend plugin\n *\n * @public\n */\nexport const costManagementPlugin = createBackendPlugin({\n pluginId: 'cost-management',\n register(env) {\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n httpAuth: coreServices.httpAuth,\n permissions: coreServices.permissions,\n cache: coreServices.cache,\n discovery: coreServices.discovery,\n auth: coreServices.auth,\n userInfo: coreServices.userInfo,\n optimizationApi: optimizationServiceRef,\n costManagementApi: costManagementServiceRef,\n },\n async init({\n httpRouter,\n logger,\n config,\n httpAuth,\n permissions,\n cache,\n discovery,\n auth,\n userInfo,\n optimizationApi,\n costManagementApi,\n }) {\n const router = await createRouter({\n logger,\n config,\n httpAuth,\n permissions,\n cache,\n discovery,\n auth,\n userInfo,\n optimizationApi,\n costManagementApi,\n });\n // @ts-ignore\n httpRouter.use(router);\n httpRouter.addAuthPolicy({\n path: '/health',\n allow: 'unauthenticated',\n });\n httpRouter.addAuthPolicy({\n path: '/access',\n allow: 'user-cookie',\n });\n httpRouter.addAuthPolicy({\n path: '/access/cost-management',\n allow: 'user-cookie',\n });\n httpRouter.addAuthPolicy({\n path: '/proxy',\n allow: 'user-cookie',\n });\n httpRouter.addAuthPolicy({\n path: '/apply-recommendation',\n allow: 'user-cookie',\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","optimizationServiceRef","costManagementServiceRef","router","createRouter"],"mappings":";;;;;;;AA6BO,MAAM,uBAAuBA,oCAAoB,CAAA;AAAA,EACtD,QAAU,EAAA,iBAAA;AAAA,EACV,SAAS,GAAK,EAAA;AACZ,IAAA,GAAA,CAAI,YAAa,CAAA;AAAA,MACf,IAAM,EAAA;AAAA,QACJ,YAAYC,6BAAa,CAAA,UAAA;AAAA,QACzB,QAAQA,6BAAa,CAAA,MAAA;AAAA,QACrB,QAAQA,6BAAa,CAAA,UAAA;AAAA,QACrB,UAAUA,6BAAa,CAAA,QAAA;AAAA,QACvB,aAAaA,6BAAa,CAAA,WAAA;AAAA,QAC1B,OAAOA,6BAAa,CAAA,KAAA;AAAA,QACpB,WAAWA,6BAAa,CAAA,SAAA;AAAA,QACxB,MAAMA,6BAAa,CAAA,IAAA;AAAA,QACnB,UAAUA,6BAAa,CAAA,QAAA;AAAA,QACvB,eAAiB,EAAAC,2CAAA;AAAA,QACjB,iBAAmB,EAAAC;AAAA,OACrB;AAAA,MACA,MAAM,IAAK,CAAA;AAAA,QACT,UAAA;AAAA,QACA,MAAA;AAAA,QACA,MAAA;AAAA,QACA,QAAA;AAAA,QACA,WAAA;AAAA,QACA,KAAA;AAAA,QACA,SAAA;AAAA,QACA,IAAA;AAAA,QACA,QAAA;AAAA,QACA,eAAA;AAAA,QACA;AAAA,OACC,EAAA;AACD,QAAM,MAAAC,QAAA,GAAS,MAAMC,mBAAa,CAAA;AAAA,UAChC,MAAA;AAAA,UACA,MAAA;AAAA,UACA,QAAA;AAAA,UACA,WAAA;AAAA,UACA,KAAA;AAAA,UACA,SAAA;AAAA,UACA,IAAA;AAAA,UACA,QAAA;AAAA,UACA,eAAA;AAAA,UACA;AAAA,SACD,CAAA;AAED,QAAA,UAAA,CAAW,IAAID,QAAM,CAAA;AACrB,QAAA,UAAA,CAAW,aAAc,CAAA;AAAA,UACvB,IAAM,EAAA,SAAA;AAAA,UACN,KAAO,EAAA;AAAA,SACR,CAAA;AACD,QAAA,UAAA,CAAW,aAAc,CAAA;AAAA,UACvB,IAAM,EAAA,SAAA;AAAA,UACN,KAAO,EAAA;AAAA,SACR,CAAA;AACD,QAAA,UAAA,CAAW,aAAc,CAAA;AAAA,UACvB,IAAM,EAAA,yBAAA;AAAA,UACN,KAAO,EAAA;AAAA,SACR,CAAA;AACD,QAAA,UAAA,CAAW,aAAc,CAAA;AAAA,UACvB,IAAM,EAAA,QAAA;AAAA,UACN,KAAO,EAAA;AAAA,SACR,CAAA;AACD,QAAA,UAAA,CAAW,aAAc,CAAA;AAAA,UACvB,IAAM,EAAA,uBAAA;AAAA,UACN,KAAO,EAAA;AAAA,SACR,CAAA;AAAA;AACH,KACD,CAAA;AAAA;AAEL,CAAC;;;;"}
1
+ {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './service/router';\nimport { optimizationServiceRef } from './service/optimizationsService';\nimport { costManagementServiceRef } from './service/costManagementService';\n\n/**\n * costManagementPlugin backend plugin\n *\n * @public\n */\nexport const costManagementPlugin = createBackendPlugin({\n pluginId: 'cost-management',\n register(env) {\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n httpAuth: coreServices.httpAuth,\n permissions: coreServices.permissions,\n cache: coreServices.cache,\n discovery: coreServices.discovery,\n auth: coreServices.auth,\n userInfo: coreServices.userInfo,\n optimizationApi: optimizationServiceRef,\n costManagementApi: costManagementServiceRef,\n },\n async init({\n httpRouter,\n logger,\n config,\n httpAuth,\n permissions,\n cache,\n discovery,\n auth,\n userInfo,\n optimizationApi,\n costManagementApi,\n }) {\n const router = await createRouter({\n logger,\n config,\n httpAuth,\n permissions,\n cache,\n discovery,\n auth,\n userInfo,\n optimizationApi,\n costManagementApi,\n });\n // @ts-ignore\n httpRouter.use(router);\n httpRouter.addAuthPolicy({\n path: '/health',\n allow: 'unauthenticated',\n });\n httpRouter.addAuthPolicy({\n path: '/access',\n allow: 'user-cookie',\n });\n httpRouter.addAuthPolicy({\n path: '/access/cost-management',\n allow: 'user-cookie',\n });\n httpRouter.addAuthPolicy({\n path: '/proxy',\n allow: 'user-cookie',\n });\n httpRouter.addAuthPolicy({\n path: '/apply-recommendation',\n allow: 'user-cookie',\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","optimizationServiceRef","costManagementServiceRef","router","createRouter"],"mappings":";;;;;;;AA6BO,MAAM,uBAAuBA,oCAAA,CAAoB;AAAA,EACtD,QAAA,EAAU,iBAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,YAAYC,6BAAA,CAAa,UAAA;AAAA,QACzB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,aAAaA,6BAAA,CAAa,WAAA;AAAA,QAC1B,OAAOA,6BAAA,CAAa,KAAA;AAAA,QACpB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,MAAMA,6BAAA,CAAa,IAAA;AAAA,QACnB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,eAAA,EAAiBC,2CAAA;AAAA,QACjB,iBAAA,EAAmBC;AAAA,OACrB;AAAA,MACA,MAAM,IAAA,CAAK;AAAA,QACT,UAAA;AAAA,QACA,MAAA;AAAA,QACA,MAAA;AAAA,QACA,QAAA;AAAA,QACA,WAAA;AAAA,QACA,KAAA;AAAA,QACA,SAAA;AAAA,QACA,IAAA;AAAA,QACA,QAAA;AAAA,QACA,eAAA;AAAA,QACA;AAAA,OACF,EAAG;AACD,QAAA,MAAMC,QAAA,GAAS,MAAMC,mBAAA,CAAa;AAAA,UAChC,MAAA;AAAA,UACA,MAAA;AAAA,UACA,QAAA;AAAA,UACA,WAAA;AAAA,UACA,KAAA;AAAA,UACA,SAAA;AAAA,UACA,IAAA;AAAA,UACA,QAAA;AAAA,UACA,eAAA;AAAA,UACA;AAAA,SACD,CAAA;AAED,QAAA,UAAA,CAAW,IAAID,QAAM,CAAA;AACrB,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,SAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AACD,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,SAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AACD,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,yBAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AACD,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,QAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AACD,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,uBAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AAAA,MACH;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"access.cjs.js","sources":["../../src/routes/access.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { RequestHandler } from 'express';\nimport type { RouterOptions } from '../models/RouterOptions';\nimport {\n authorize,\n filterAuthorizedClustersAndProjects,\n} from '../util/checkPermissions';\nimport { rosPluginPermissions } from '@red-hat-developer-hub/plugin-cost-management-common/permissions';\nimport { getTokenFromApi } from '../util/tokenUtil';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport { resolveActor, emitAuditLog } from '../util/auditLog';\n\nexport const getAccess: (options: RouterOptions) => RequestHandler =\n options => async (_, response) => {\n const { logger, permissions, httpAuth, cache, optimizationApi } = options;\n let finalDecision = AuthorizeResult.DENY;\n\n // Check for ros.plugin permisssion\n // if user has ros.plugin permission, allow access to all the data\n const rosPluginDecision = await authorize(\n _,\n rosPluginPermissions,\n permissions,\n httpAuth,\n );\n\n logger.info(`Checking decision:`, rosPluginDecision);\n\n if (rosPluginDecision.result === AuthorizeResult.ALLOW) {\n finalDecision = AuthorizeResult.ALLOW;\n\n const actor = await resolveActor(_, options);\n emitAuditLog(options, {\n actor,\n action: 'access_check',\n resource: '/access',\n decision: 'ALLOW',\n });\n\n const body = {\n decision: finalDecision,\n authorizeClusterIds: [],\n };\n return response.json(body);\n }\n\n // RBAC Filtering logic for Cluster & Project\n const ALL_CLUSTERS_MAP_CACHE_KEY = 'all_clusters_map';\n const ALL_PROJECTS_CACHE_KEY = 'all_projects';\n\n let clusterDataMap: Record<string, string> = {};\n let allProjects: string[] = [];\n\n // Check the cluster & project data in the cache first\n const clusterMapDataFromCache = (await cache.get(\n ALL_CLUSTERS_MAP_CACHE_KEY,\n )) as Record<string, string> | undefined;\n const projectDataFromCache = (await cache.get(ALL_PROJECTS_CACHE_KEY)) as\n | string[]\n | undefined;\n\n if (clusterMapDataFromCache && projectDataFromCache) {\n clusterDataMap = clusterMapDataFromCache;\n allProjects = projectDataFromCache;\n } else {\n try {\n // token\n const token = await getTokenFromApi(options);\n\n // hit /recommendation API endpoint\n const optimizationResponse =\n await optimizationApi.getRecommendationList(\n {\n query: {\n limit: -1,\n orderHow: 'desc',\n orderBy: 'last_reported',\n },\n },\n { token },\n );\n\n // OptimizationsClient already transforms to camelCase when token is provided\n const recommendationList = await optimizationResponse.json();\n\n // Check if response contains errors\n if ((recommendationList as any).errors) {\n logger.error(\n 'API returned errors:',\n (recommendationList as any).errors,\n );\n return response.status(500).json({\n decision: AuthorizeResult.DENY,\n error: 'API returned errors',\n authorizeClusterIds: [],\n authorizeProjects: [],\n });\n }\n\n // retrive cluster data from the API result\n if (recommendationList.data) {\n recommendationList.data.map(recommendation => {\n if (recommendation.clusterAlias && recommendation.clusterUuid)\n clusterDataMap[recommendation.clusterAlias] =\n recommendation.clusterUuid;\n });\n\n allProjects = [\n ...new Set(\n recommendationList.data.map(\n recommendation => recommendation.project,\n ),\n ),\n ].filter(project => project !== undefined) as string[];\n\n // store it in Cache\n await cache.set(ALL_CLUSTERS_MAP_CACHE_KEY, clusterDataMap, {\n ttl: 15 * 60 * 1000,\n });\n await cache.set(ALL_PROJECTS_CACHE_KEY, allProjects, {\n ttl: 15 * 60 * 1000,\n });\n }\n } catch (error) {\n logger.error('Error fetching recommendations', error);\n\n // Return unauthorized response on any error\n return response.status(500).json({\n decision: AuthorizeResult.DENY,\n error: 'Failed to fetch cluster data',\n authorizeClusterIds: [],\n authorizeProjects: [],\n });\n }\n }\n\n // RBAC Filtering: Single batch call for both cluster and cluster-project permissions\n logger.info(\n `Checking permissions for ${\n Object.keys(clusterDataMap).length\n } clusters and ${allProjects.length} projects`,\n );\n logger.info(`Cluster names: ${Object.keys(clusterDataMap).join(', ')}`);\n logger.info(`Projects: ${allProjects.join(', ')}`);\n\n const { authorizedClusterIds, authorizedClusterProjects } =\n await filterAuthorizedClustersAndProjects(\n _,\n permissions,\n httpAuth,\n clusterDataMap,\n allProjects,\n );\n\n logger.info(\n `Authorization results: ${authorizedClusterIds.length} cluster IDs, ${authorizedClusterProjects.length} cluster-project combinations`,\n );\n logger.info(`Authorized cluster IDs: ${authorizedClusterIds.join(', ')}`);\n logger.info(\n `Authorized cluster-projects: ${authorizedClusterProjects\n .map(cp => `${cp.cluster}.${cp.project}`)\n .join(', ')}`,\n );\n\n // Combine cluster IDs from both cluster-level and project-level permissions\n const finalAuthorizedClusterIds = [\n ...new Set([\n ...authorizedClusterIds,\n ...authorizedClusterProjects.map(result => result.cluster),\n ]),\n ];\n\n const authorizeProjects = authorizedClusterProjects.map(\n result => result.project,\n );\n\n if (finalAuthorizedClusterIds.length > 0) {\n finalDecision = AuthorizeResult.ALLOW;\n } else {\n finalDecision = AuthorizeResult.DENY;\n }\n\n const actor = await resolveActor(_, options);\n emitAuditLog(options, {\n actor,\n action: 'access_check',\n resource: '/access',\n decision: finalDecision === AuthorizeResult.ALLOW ? 'ALLOW' : 'DENY',\n filters: {\n clusters: finalAuthorizedClusterIds,\n projects: authorizeProjects,\n },\n });\n\n const body = {\n decision: finalDecision,\n authorizeClusterIds: finalAuthorizedClusterIds,\n authorizeProjects,\n };\n\n return response.json(body);\n };\n"],"names":["permissions","AuthorizeResult","authorize","rosPluginPermissions","actor","resolveActor","emitAuditLog","body","getTokenFromApi","filterAuthorizedClustersAndProjects"],"mappings":"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"}
1
+ {"version":3,"file":"access.cjs.js","sources":["../../src/routes/access.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { RequestHandler } from 'express';\nimport type { RouterOptions } from '../models/RouterOptions';\nimport {\n authorize,\n filterAuthorizedClustersAndProjects,\n} from '../util/checkPermissions';\nimport { rosPluginPermissions } from '@red-hat-developer-hub/plugin-cost-management-common/permissions';\nimport { getTokenFromApi } from '../util/tokenUtil';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport { resolveActor, emitAuditLog } from '../util/auditLog';\n\nexport const getAccess: (options: RouterOptions) => RequestHandler =\n options => async (_, response) => {\n const { logger, permissions, httpAuth, cache, optimizationApi } = options;\n let finalDecision: AuthorizeResult = AuthorizeResult.DENY;\n\n // Check for ros.plugin permisssion\n // if user has ros.plugin permission, allow access to all the data\n const rosPluginDecision = await authorize(\n _,\n rosPluginPermissions,\n permissions,\n httpAuth,\n );\n\n logger.info(`Checking decision:`, rosPluginDecision);\n\n if (rosPluginDecision.result === AuthorizeResult.ALLOW) {\n finalDecision = AuthorizeResult.ALLOW;\n\n const actor = await resolveActor(_, options);\n emitAuditLog(options, {\n actor,\n action: 'access_check',\n resource: '/access',\n decision: 'ALLOW',\n });\n\n const body = {\n decision: finalDecision,\n authorizeClusterIds: [],\n };\n return response.json(body);\n }\n\n // RBAC Filtering logic for Cluster & Project\n const ALL_CLUSTERS_MAP_CACHE_KEY = 'all_clusters_map';\n const ALL_PROJECTS_CACHE_KEY = 'all_projects';\n\n let clusterDataMap: Record<string, string> = {};\n let allProjects: string[] = [];\n\n // Check the cluster & project data in the cache first\n const clusterMapDataFromCache = (await cache.get(\n ALL_CLUSTERS_MAP_CACHE_KEY,\n )) as Record<string, string> | undefined;\n const projectDataFromCache = (await cache.get(ALL_PROJECTS_CACHE_KEY)) as\n | string[]\n | undefined;\n\n if (clusterMapDataFromCache && projectDataFromCache) {\n clusterDataMap = clusterMapDataFromCache;\n allProjects = projectDataFromCache;\n } else {\n try {\n // token\n const token = await getTokenFromApi(options);\n\n // hit /recommendation API endpoint\n const optimizationResponse =\n await optimizationApi.getRecommendationList(\n {\n query: {\n limit: -1,\n orderHow: 'desc',\n orderBy: 'last_reported',\n },\n },\n { token },\n );\n\n // OptimizationsClient already transforms to camelCase when token is provided\n const recommendationList = await optimizationResponse.json();\n\n // Check if response contains errors\n if ((recommendationList as any).errors) {\n logger.error(\n 'API returned errors:',\n (recommendationList as any).errors,\n );\n return response.status(500).json({\n decision: AuthorizeResult.DENY,\n error: 'API returned errors',\n authorizeClusterIds: [],\n authorizeProjects: [],\n });\n }\n\n // retrive cluster data from the API result\n if (recommendationList.data) {\n recommendationList.data.map(recommendation => {\n if (recommendation.clusterAlias && recommendation.clusterUuid)\n clusterDataMap[recommendation.clusterAlias] =\n recommendation.clusterUuid;\n });\n\n allProjects = [\n ...new Set(\n recommendationList.data.map(\n recommendation => recommendation.project,\n ),\n ),\n ].filter(project => project !== undefined) as string[];\n\n // store it in Cache\n await cache.set(ALL_CLUSTERS_MAP_CACHE_KEY, clusterDataMap, {\n ttl: 15 * 60 * 1000,\n });\n await cache.set(ALL_PROJECTS_CACHE_KEY, allProjects, {\n ttl: 15 * 60 * 1000,\n });\n }\n } catch (error) {\n logger.error('Error fetching recommendations', error);\n\n // Return unauthorized response on any error\n return response.status(500).json({\n decision: AuthorizeResult.DENY,\n error: 'Failed to fetch cluster data',\n authorizeClusterIds: [],\n authorizeProjects: [],\n });\n }\n }\n\n // RBAC Filtering: Single batch call for both cluster and cluster-project permissions\n logger.info(\n `Checking permissions for ${\n Object.keys(clusterDataMap).length\n } clusters and ${allProjects.length} projects`,\n );\n logger.info(`Cluster names: ${Object.keys(clusterDataMap).join(', ')}`);\n logger.info(`Projects: ${allProjects.join(', ')}`);\n\n const { authorizedClusterIds, authorizedClusterProjects } =\n await filterAuthorizedClustersAndProjects(\n _,\n permissions,\n httpAuth,\n clusterDataMap,\n allProjects,\n );\n\n logger.info(\n `Authorization results: ${authorizedClusterIds.length} cluster IDs, ${authorizedClusterProjects.length} cluster-project combinations`,\n );\n logger.info(`Authorized cluster IDs: ${authorizedClusterIds.join(', ')}`);\n logger.info(\n `Authorized cluster-projects: ${authorizedClusterProjects\n .map(cp => `${cp.cluster}.${cp.project}`)\n .join(', ')}`,\n );\n\n // Combine cluster IDs from both cluster-level and project-level permissions\n const finalAuthorizedClusterIds = [\n ...new Set([\n ...authorizedClusterIds,\n ...authorizedClusterProjects.map(result => result.cluster),\n ]),\n ];\n\n const authorizeProjects = authorizedClusterProjects.map(\n result => result.project,\n );\n\n if (finalAuthorizedClusterIds.length > 0) {\n finalDecision = AuthorizeResult.ALLOW;\n } else {\n finalDecision = AuthorizeResult.DENY;\n }\n\n const actor = await resolveActor(_, options);\n emitAuditLog(options, {\n actor,\n action: 'access_check',\n resource: '/access',\n decision: finalDecision === AuthorizeResult.ALLOW ? 'ALLOW' : 'DENY',\n filters: {\n clusters: finalAuthorizedClusterIds,\n projects: authorizeProjects,\n },\n });\n\n const body = {\n decision: finalDecision,\n authorizeClusterIds: finalAuthorizedClusterIds,\n authorizeProjects,\n };\n\n return response.json(body);\n };\n"],"names":["permissions","AuthorizeResult","authorize","rosPluginPermissions","actor","resolveActor","emitAuditLog","body","getTokenFromApi","filterAuthorizedClustersAndProjects"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"applyRecommendation.cjs.js","sources":["../../src/routes/applyRecommendation.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { RequestHandler } from 'express';\nimport type { RouterOptions } from '../models/RouterOptions';\nimport { authorize } from '../util/checkPermissions';\nimport { rosApplyPermissions } from '@red-hat-developer-hub/plugin-cost-management-common/permissions';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport { resolveActor, emitAuditLog } from '../util/auditLog';\n\nconst ALLOWED_RESOURCE_TYPES = new Set([\n 'deployment',\n 'replicaset',\n 'daemonset',\n 'statefulset',\n 'deploymentconfig',\n 'replicationcontroller',\n]);\n\ninterface ApplyRecommendationBody {\n workflowId: string;\n inputData: {\n clusterName: string;\n resourceType: string;\n resourceNamespace: string;\n resourceName: string;\n containerName: string;\n containerResources: {\n limits?: { cpu?: number; memory?: number };\n requests?: { cpu?: number; memory?: number };\n };\n };\n}\n\nfunction validateBody(\n body: unknown,\n):\n | { valid: true; data: ApplyRecommendationBody }\n | { valid: false; error: string } {\n const b = body as ApplyRecommendationBody;\n\n if (!b?.workflowId || typeof b.workflowId !== 'string') {\n return { valid: false, error: 'Missing or invalid workflowId' };\n }\n\n const input = b.inputData;\n if (!input || typeof input !== 'object') {\n return { valid: false, error: 'Missing inputData' };\n }\n\n if (!input.resourceType || !ALLOWED_RESOURCE_TYPES.has(input.resourceType)) {\n return {\n valid: false,\n error: `Invalid resourceType: ${\n input.resourceType ?? 'undefined'\n }. Allowed: ${[...ALLOWED_RESOURCE_TYPES].join(', ')}`,\n };\n }\n\n for (const field of [\n 'clusterName',\n 'resourceNamespace',\n 'resourceName',\n 'containerName',\n ] as const) {\n if (!input[field] || typeof input[field] !== 'string') {\n return { valid: false, error: `Missing or invalid ${field}` };\n }\n }\n\n if (\n !input.containerResources ||\n typeof input.containerResources !== 'object'\n ) {\n return { valid: false, error: 'Missing containerResources' };\n }\n\n return { valid: true, data: b };\n}\n\n/**\n * Backend endpoint that validates inputs, checks ros.apply permission,\n * and forwards the workflow execution to the Orchestrator plugin.\n */\nexport const applyRecommendation: (options: RouterOptions) => RequestHandler =\n options => async (req, res) => {\n const { logger, httpAuth, permissions, discovery, auth } = options;\n\n const validation = validateBody(req.body);\n if (!validation.valid) {\n return res.status(400).json({ error: validation.error });\n }\n const { workflowId, inputData } = validation.data;\n\n const actor = await resolveActor(req, options);\n\n const decision = await authorize(\n req,\n rosApplyPermissions,\n permissions,\n httpAuth,\n );\n if (decision.result !== AuthorizeResult.ALLOW) {\n emitAuditLog(options, {\n actor,\n action: 'apply_recommendation',\n resource: `/apply-recommendation/${workflowId}`,\n decision: 'DENY',\n meta: {\n workflowId,\n cluster: inputData.clusterName,\n namespace: inputData.resourceNamespace,\n workload: inputData.resourceName,\n resourceType: inputData.resourceType,\n },\n });\n return res\n .status(403)\n .json({ error: 'Access denied: ros.apply permission required' });\n }\n\n try {\n const orchestratorBase = await discovery.getBaseUrl('orchestrator');\n const { token } = await auth.getPluginRequestToken({\n onBehalfOf: await httpAuth.credentials(req),\n targetPluginId: 'orchestrator',\n });\n\n const executeUrl = `${orchestratorBase}/v2/workflows/${encodeURIComponent(\n workflowId,\n )}/execute`;\n\n const upstreamResponse = await fetch(executeUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({\n inputData: {\n clusterName: inputData.clusterName,\n resourceType: inputData.resourceType,\n resourceNamespace: inputData.resourceNamespace,\n resourceName: inputData.resourceName,\n containerName: inputData.containerName,\n containerResources: inputData.containerResources,\n },\n }),\n });\n\n const contentType = upstreamResponse.headers.get('content-type') || '';\n let payload: unknown;\n try {\n payload = contentType.includes('application/json')\n ? await upstreamResponse.json()\n : { message: await upstreamResponse.text() };\n } catch {\n payload = { error: 'Upstream returned unparseable response' };\n }\n\n if (!upstreamResponse.ok) {\n emitAuditLog(options, {\n actor,\n action: 'apply_recommendation',\n resource: `/apply-recommendation/${workflowId}`,\n decision: 'ALLOW',\n meta: {\n workflowId,\n cluster: inputData.clusterName,\n namespace: inputData.resourceNamespace,\n workload: inputData.resourceName,\n resourceType: inputData.resourceType,\n upstreamStatus: upstreamResponse.status,\n outcome: 'upstream_error',\n },\n });\n return res.status(upstreamResponse.status).json(payload);\n }\n\n emitAuditLog(options, {\n actor,\n action: 'apply_recommendation',\n resource: `/apply-recommendation/${workflowId}`,\n decision: 'ALLOW',\n meta: {\n workflowId,\n instanceId: (payload as { id?: string }).id,\n cluster: inputData.clusterName,\n namespace: inputData.resourceNamespace,\n workload: inputData.resourceName,\n resourceType: inputData.resourceType,\n outcome: 'success',\n },\n });\n\n return res.status(200).json(payload);\n } catch (error) {\n logger.error('Apply recommendation proxy error', error);\n return res\n .status(500)\n .json({ error: 'Internal error executing workflow' });\n }\n };\n"],"names":["permissions","resolveActor","authorize","rosApplyPermissions","AuthorizeResult","emitAuditLog"],"mappings":"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"}
1
+ {"version":3,"file":"applyRecommendation.cjs.js","sources":["../../src/routes/applyRecommendation.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { RequestHandler } from 'express';\nimport type { RouterOptions } from '../models/RouterOptions';\nimport { authorize } from '../util/checkPermissions';\nimport { rosApplyPermissions } from '@red-hat-developer-hub/plugin-cost-management-common/permissions';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport { resolveActor, emitAuditLog } from '../util/auditLog';\n\nconst ALLOWED_RESOURCE_TYPES = new Set([\n 'deployment',\n 'replicaset',\n 'daemonset',\n 'statefulset',\n 'deploymentconfig',\n 'replicationcontroller',\n]);\n\ninterface ApplyRecommendationBody {\n workflowId: string;\n inputData: {\n clusterName: string;\n resourceType: string;\n resourceNamespace: string;\n resourceName: string;\n containerName: string;\n containerResources: {\n limits?: { cpu?: number; memory?: number };\n requests?: { cpu?: number; memory?: number };\n };\n };\n}\n\nfunction validateBody(\n body: unknown,\n):\n | { valid: true; data: ApplyRecommendationBody }\n | { valid: false; error: string } {\n const b = body as ApplyRecommendationBody;\n\n if (!b?.workflowId || typeof b.workflowId !== 'string') {\n return { valid: false, error: 'Missing or invalid workflowId' };\n }\n\n const input = b.inputData;\n if (!input || typeof input !== 'object') {\n return { valid: false, error: 'Missing inputData' };\n }\n\n if (!input.resourceType || !ALLOWED_RESOURCE_TYPES.has(input.resourceType)) {\n return {\n valid: false,\n error: `Invalid resourceType: ${\n input.resourceType ?? 'undefined'\n }. Allowed: ${[...ALLOWED_RESOURCE_TYPES].join(', ')}`,\n };\n }\n\n for (const field of [\n 'clusterName',\n 'resourceNamespace',\n 'resourceName',\n 'containerName',\n ] as const) {\n if (!input[field] || typeof input[field] !== 'string') {\n return { valid: false, error: `Missing or invalid ${field}` };\n }\n }\n\n if (\n !input.containerResources ||\n typeof input.containerResources !== 'object'\n ) {\n return { valid: false, error: 'Missing containerResources' };\n }\n\n return { valid: true, data: b };\n}\n\n/**\n * Backend endpoint that validates inputs, checks ros.apply permission,\n * and forwards the workflow execution to the Orchestrator plugin.\n */\nexport const applyRecommendation: (options: RouterOptions) => RequestHandler =\n options => async (req, res) => {\n const { logger, httpAuth, permissions, discovery, auth } = options;\n\n const validation = validateBody(req.body);\n if (!validation.valid) {\n return res.status(400).json({ error: validation.error });\n }\n const { workflowId, inputData } = validation.data;\n\n const actor = await resolveActor(req, options);\n\n const decision = await authorize(\n req,\n rosApplyPermissions,\n permissions,\n httpAuth,\n );\n if (decision.result !== AuthorizeResult.ALLOW) {\n emitAuditLog(options, {\n actor,\n action: 'apply_recommendation',\n resource: `/apply-recommendation/${workflowId}`,\n decision: 'DENY',\n meta: {\n workflowId,\n cluster: inputData.clusterName,\n namespace: inputData.resourceNamespace,\n workload: inputData.resourceName,\n resourceType: inputData.resourceType,\n },\n });\n return res\n .status(403)\n .json({ error: 'Access denied: ros.apply permission required' });\n }\n\n try {\n const orchestratorBase = await discovery.getBaseUrl('orchestrator');\n const { token } = await auth.getPluginRequestToken({\n onBehalfOf: await httpAuth.credentials(req),\n targetPluginId: 'orchestrator',\n });\n\n const executeUrl = `${orchestratorBase}/v2/workflows/${encodeURIComponent(\n workflowId,\n )}/execute`;\n\n const upstreamResponse = await fetch(executeUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({\n inputData: {\n clusterName: inputData.clusterName,\n resourceType: inputData.resourceType,\n resourceNamespace: inputData.resourceNamespace,\n resourceName: inputData.resourceName,\n containerName: inputData.containerName,\n containerResources: inputData.containerResources,\n },\n }),\n });\n\n const contentType = upstreamResponse.headers.get('content-type') || '';\n let payload: unknown;\n try {\n payload = contentType.includes('application/json')\n ? await upstreamResponse.json()\n : { message: await upstreamResponse.text() };\n } catch {\n payload = { error: 'Upstream returned unparseable response' };\n }\n\n if (!upstreamResponse.ok) {\n emitAuditLog(options, {\n actor,\n action: 'apply_recommendation',\n resource: `/apply-recommendation/${workflowId}`,\n decision: 'ALLOW',\n meta: {\n workflowId,\n cluster: inputData.clusterName,\n namespace: inputData.resourceNamespace,\n workload: inputData.resourceName,\n resourceType: inputData.resourceType,\n upstreamStatus: upstreamResponse.status,\n outcome: 'upstream_error',\n },\n });\n return res.status(upstreamResponse.status).json(payload);\n }\n\n emitAuditLog(options, {\n actor,\n action: 'apply_recommendation',\n resource: `/apply-recommendation/${workflowId}`,\n decision: 'ALLOW',\n meta: {\n workflowId,\n instanceId: (payload as { id?: string }).id,\n cluster: inputData.clusterName,\n namespace: inputData.resourceNamespace,\n workload: inputData.resourceName,\n resourceType: inputData.resourceType,\n outcome: 'success',\n },\n });\n\n return res.status(200).json(payload);\n } catch (error) {\n logger.error('Apply recommendation proxy error', error);\n return res\n .status(500)\n .json({ error: 'Internal error executing workflow' });\n }\n };\n"],"names":["permissions","resolveActor","authorize","rosApplyPermissions","AuthorizeResult","emitAuditLog"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"costManagementAccess.cjs.js","sources":["../../src/routes/costManagementAccess.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { RequestHandler } from 'express';\nimport type { RouterOptions } from '../models/RouterOptions';\nimport {\n authorize,\n filterAuthorizedClustersAndProjects,\n} from '../util/checkPermissions';\nimport { costPluginPermissions } from '@red-hat-developer-hub/plugin-cost-management-common/permissions';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport { getTokenFromApi } from '../util/tokenUtil';\nimport { resolveActor, emitAuditLog } from '../util/auditLog';\n\n// Cache keys for cost management clusters and projects\nconst COST_CLUSTERS_CACHE_KEY = 'cost_clusters';\nconst COST_PROJECTS_CACHE_KEY = 'cost_projects';\nconst CACHE_TTL = 15 * 60 * 1000; // 15 minutes\n\nexport const getCostManagementAccess: (\n options: RouterOptions,\n) => RequestHandler = options => async (_, response) => {\n const { logger, permissions, httpAuth, cache, costManagementApi } = options;\n let finalDecision = AuthorizeResult.DENY;\n\n // Check for cost.plugin permission\n // If user has cost.plugin permission, allow access to all data\n const costPluginDecision = await authorize(\n _,\n costPluginPermissions,\n permissions,\n httpAuth,\n );\n\n logger.info(`Checking cost.plugin permission:`, costPluginDecision);\n\n if (costPluginDecision.result === AuthorizeResult.ALLOW) {\n finalDecision = AuthorizeResult.ALLOW;\n\n const actor = await resolveActor(_, options);\n emitAuditLog(options, {\n actor,\n action: 'access_check',\n resource: '/access/cost-management',\n decision: 'ALLOW',\n });\n\n const body = {\n decision: finalDecision,\n authorizedClusterNames: [],\n authorizeProjects: [],\n };\n return response.json(body);\n }\n\n // RBAC Filtering logic for Cluster & Project using cost/{clusterName} and cost/{clusterName}/{projectName} permissions\n let clusterDataMap: Record<string, string> = {};\n let allProjects: string[] = [];\n\n // Check the cluster & project data in the cache first\n const clustersFromCache = (await cache.get(COST_CLUSTERS_CACHE_KEY)) as\n | Record<string, string>\n | undefined;\n const projectsFromCache = (await cache.get(COST_PROJECTS_CACHE_KEY)) as\n | string[]\n | undefined;\n\n if (clustersFromCache && projectsFromCache) {\n clusterDataMap = clustersFromCache;\n allProjects = projectsFromCache;\n logger.info(\n `Using cached data: ${Object.keys(clusterDataMap).length} clusters, ${\n allProjects.length\n } projects`,\n );\n } else {\n // Fetch clusters and projects from Cost Management API\n try {\n const token = await getTokenFromApi(options);\n\n // Fetch clusters and projects in parallel for better performance\n const [clustersResponse, projectsResponse] = await Promise.all([\n costManagementApi.searchOpenShiftClusters('', { token, limit: 1000 }),\n costManagementApi.searchOpenShiftProjects('', { token, limit: 1000 }),\n ]);\n\n const clustersData = await clustersResponse.json();\n const projectsData = await projectsResponse.json();\n\n // Extract cluster names from response\n clustersData.data?.forEach(\n (cluster: { value: string; cluster_alias: string }) => {\n if (cluster.cluster_alias && cluster.value) {\n clusterDataMap[cluster.cluster_alias] = cluster.value;\n }\n },\n );\n\n // Extract unique project names\n allProjects = [\n ...new Set(\n projectsData.data?.map((project: { value: string }) => project.value),\n ),\n ].filter(project => project !== undefined) as string[];\n\n logger.info(\n `Fetched ${Object.keys(clusterDataMap).length} clusters and ${\n allProjects.length\n } projects from Cost Management API`,\n );\n\n // Store in cache\n await Promise.all([\n cache.set(COST_CLUSTERS_CACHE_KEY, clusterDataMap, {\n ttl: CACHE_TTL,\n }),\n cache.set(COST_PROJECTS_CACHE_KEY, allProjects, {\n ttl: CACHE_TTL,\n }),\n ]);\n } catch (error) {\n logger.error('Error fetching cost management data', error);\n\n // Return unauthorized response on any error\n return response.status(500).json({\n decision: AuthorizeResult.DENY,\n error: 'Failed to fetch cluster data',\n authorizedClusterNames: [],\n authorizeProjects: [],\n });\n }\n }\n\n // RBAC Filtering: Single batch call for both cluster and cluster-project permissions\n\n const { authorizedClusterIds, authorizedClusterProjects } =\n await filterAuthorizedClustersAndProjects(\n _,\n permissions,\n httpAuth,\n clusterDataMap,\n allProjects,\n 'cost',\n );\n\n // Combine cluster names from both cluster-level and project-level permissions\n const finalAuthorizedClusterNames = [\n ...new Set([\n ...authorizedClusterIds,\n ...authorizedClusterProjects.map(result => result.cluster),\n ]),\n ];\n\n const authorizeProjects = authorizedClusterProjects.map(\n result => result.project,\n );\n\n // If user has access to at least one cluster, allow access\n if (finalAuthorizedClusterNames.length > 0) {\n finalDecision = AuthorizeResult.ALLOW;\n }\n\n const actor = await resolveActor(_, options);\n emitAuditLog(options, {\n actor,\n action: 'access_check',\n resource: '/access/cost-management',\n decision: finalDecision === AuthorizeResult.ALLOW ? 'ALLOW' : 'DENY',\n filters: {\n clusters: finalAuthorizedClusterNames,\n projects: authorizeProjects,\n },\n });\n\n const body = {\n decision: finalDecision,\n authorizedClusterNames: finalAuthorizedClusterNames,\n authorizeProjects,\n };\n\n return response.json(body);\n};\n"],"names":["permissions","AuthorizeResult","authorize","costPluginPermissions","actor","resolveActor","emitAuditLog","body","getTokenFromApi","filterAuthorizedClustersAndProjects"],"mappings":"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"}
1
+ {"version":3,"file":"costManagementAccess.cjs.js","sources":["../../src/routes/costManagementAccess.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { RequestHandler } from 'express';\nimport type { RouterOptions } from '../models/RouterOptions';\nimport {\n authorize,\n filterAuthorizedClustersAndProjects,\n} from '../util/checkPermissions';\nimport { costPluginPermissions } from '@red-hat-developer-hub/plugin-cost-management-common/permissions';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport { getTokenFromApi } from '../util/tokenUtil';\nimport { resolveActor, emitAuditLog } from '../util/auditLog';\n\n// Cache keys for cost management clusters and projects\nconst COST_CLUSTERS_CACHE_KEY = 'cost_clusters';\nconst COST_PROJECTS_CACHE_KEY = 'cost_projects';\nconst CACHE_TTL = 15 * 60 * 1000; // 15 minutes\n\nexport const getCostManagementAccess: (\n options: RouterOptions,\n) => RequestHandler = options => async (_, response) => {\n const { logger, permissions, httpAuth, cache, costManagementApi } = options;\n let finalDecision: AuthorizeResult = AuthorizeResult.DENY;\n\n // Check for cost.plugin permission\n // If user has cost.plugin permission, allow access to all data\n const costPluginDecision = await authorize(\n _,\n costPluginPermissions,\n permissions,\n httpAuth,\n );\n\n logger.info(`Checking cost.plugin permission:`, costPluginDecision);\n\n if (costPluginDecision.result === AuthorizeResult.ALLOW) {\n finalDecision = AuthorizeResult.ALLOW;\n\n const actor = await resolveActor(_, options);\n emitAuditLog(options, {\n actor,\n action: 'access_check',\n resource: '/access/cost-management',\n decision: 'ALLOW',\n });\n\n const body = {\n decision: finalDecision,\n authorizedClusterNames: [],\n authorizeProjects: [],\n };\n return response.json(body);\n }\n\n // RBAC Filtering logic for Cluster & Project using cost/{clusterName} and cost/{clusterName}/{projectName} permissions\n let clusterDataMap: Record<string, string> = {};\n let allProjects: string[] = [];\n\n // Check the cluster & project data in the cache first\n const clustersFromCache = (await cache.get(COST_CLUSTERS_CACHE_KEY)) as\n | Record<string, string>\n | undefined;\n const projectsFromCache = (await cache.get(COST_PROJECTS_CACHE_KEY)) as\n | string[]\n | undefined;\n\n if (clustersFromCache && projectsFromCache) {\n clusterDataMap = clustersFromCache;\n allProjects = projectsFromCache;\n logger.info(\n `Using cached data: ${Object.keys(clusterDataMap).length} clusters, ${\n allProjects.length\n } projects`,\n );\n } else {\n // Fetch clusters and projects from Cost Management API\n try {\n const token = await getTokenFromApi(options);\n\n // Fetch clusters and projects in parallel for better performance\n const [clustersResponse, projectsResponse] = await Promise.all([\n costManagementApi.searchOpenShiftClusters('', { token, limit: 1000 }),\n costManagementApi.searchOpenShiftProjects('', { token, limit: 1000 }),\n ]);\n\n const clustersData = await clustersResponse.json();\n const projectsData = await projectsResponse.json();\n\n // Extract cluster names from response\n clustersData.data?.forEach(\n (cluster: { value: string; cluster_alias: string }) => {\n if (cluster.cluster_alias && cluster.value) {\n clusterDataMap[cluster.cluster_alias] = cluster.value;\n }\n },\n );\n\n // Extract unique project names\n allProjects = [\n ...new Set(\n projectsData.data?.map((project: { value: string }) => project.value),\n ),\n ].filter(project => project !== undefined) as string[];\n\n logger.info(\n `Fetched ${Object.keys(clusterDataMap).length} clusters and ${\n allProjects.length\n } projects from Cost Management API`,\n );\n\n // Store in cache\n await Promise.all([\n cache.set(COST_CLUSTERS_CACHE_KEY, clusterDataMap, {\n ttl: CACHE_TTL,\n }),\n cache.set(COST_PROJECTS_CACHE_KEY, allProjects, {\n ttl: CACHE_TTL,\n }),\n ]);\n } catch (error) {\n logger.error('Error fetching cost management data', error);\n\n // Return unauthorized response on any error\n return response.status(500).json({\n decision: AuthorizeResult.DENY,\n error: 'Failed to fetch cluster data',\n authorizedClusterNames: [],\n authorizeProjects: [],\n });\n }\n }\n\n // RBAC Filtering: Single batch call for both cluster and cluster-project permissions\n\n const { authorizedClusterIds, authorizedClusterProjects } =\n await filterAuthorizedClustersAndProjects(\n _,\n permissions,\n httpAuth,\n clusterDataMap,\n allProjects,\n 'cost',\n );\n\n // Combine cluster names from both cluster-level and project-level permissions\n const finalAuthorizedClusterNames = [\n ...new Set([\n ...authorizedClusterIds,\n ...authorizedClusterProjects.map(result => result.cluster),\n ]),\n ];\n\n const authorizeProjects = authorizedClusterProjects.map(\n result => result.project,\n );\n\n // If user has access to at least one cluster, allow access\n if (finalAuthorizedClusterNames.length > 0) {\n finalDecision = AuthorizeResult.ALLOW;\n }\n\n const actor = await resolveActor(_, options);\n emitAuditLog(options, {\n actor,\n action: 'access_check',\n resource: '/access/cost-management',\n decision: finalDecision === AuthorizeResult.ALLOW ? 'ALLOW' : 'DENY',\n filters: {\n clusters: finalAuthorizedClusterNames,\n projects: authorizeProjects,\n },\n });\n\n const body = {\n decision: finalDecision,\n authorizedClusterNames: finalAuthorizedClusterNames,\n authorizeProjects,\n };\n\n return response.json(body);\n};\n"],"names":["permissions","AuthorizeResult","authorize","costPluginPermissions","actor","resolveActor","emitAuditLog","body","getTokenFromApi","filterAuthorizedClustersAndProjects"],"mappings":"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"}
@@ -257,6 +257,11 @@ const secureProxy = (options) => async (req, res) => {
257
257
  return res.send(await upstreamResponse.text());
258
258
  } catch (error) {
259
259
  options.logger.error("Secure proxy error", error);
260
+ if (error instanceof tokenUtil.SsoAuthenticationError) {
261
+ return res.status(502).json({
262
+ error: "Unable to authenticate with the hybrid cloud console. Please check your service account credentials (clientId/clientSecret) in the RHDH Cost Management configuration."
263
+ });
264
+ }
260
265
  return res.status(500).json({ error: "Internal proxy error" });
261
266
  }
262
267
  };
@@ -1 +1 @@
1
- {"version":3,"file":"secureProxy.cjs.js","sources":["../../src/routes/secureProxy.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { RequestHandler, Request } from 'express';\nimport type { RouterOptions } from '../models/RouterOptions';\nimport {\n authorize,\n filterAuthorizedClustersAndProjects,\n} from '../util/checkPermissions';\nimport {\n rosPluginPermissions,\n costPluginPermissions,\n} from '@red-hat-developer-hub/plugin-cost-management-common/permissions';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport { getTokenFromApi } from '../util/tokenUtil';\nimport { DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL } from '../util/constant';\nimport { resolveActor, emitAuditLog } from '../util/auditLog';\n\nconst CACHE_TTL = 15 * 60 * 1000;\n\ninterface AccessResult {\n decision: string;\n clusterFilters: string[];\n projectFilters: string[];\n filterStyle: 'ros' | 'cost';\n}\n\n/**\n * Determines the RBAC scope for a given proxy path and resolves\n * the authorized cluster/project filters server-side.\n */\nasync function resolveAccess(\n req: Request,\n proxyPath: string,\n options: RouterOptions,\n): Promise<AccessResult> {\n const isOptimizations = proxyPath.startsWith('recommendations/');\n\n if (isOptimizations) {\n return resolveOptimizationsAccess(req, options);\n }\n return resolveCostManagementAccess(req, options);\n}\n\ninterface CacheConfig {\n clusterKey: string;\n projectKey: string;\n}\n\ntype DataFetcher = (\n options: RouterOptions,\n) => Promise<{ clusters: Record<string, string>; projects: string[] } | null>;\n\n/**\n * Common RBAC resolution: check plugin-level permission, fetch + cache\n * cluster/project data, filter authorised entries, and return the result.\n */\nasync function resolveAccessForSection(\n req: Request,\n options: RouterOptions,\n pluginPerms: typeof rosPluginPermissions,\n filterStyle: AccessResult['filterStyle'],\n cacheKeys: CacheConfig,\n fetchData: DataFetcher,\n): Promise<AccessResult> {\n const { permissions, httpAuth, cache } = options;\n const deny = (): AccessResult => ({\n decision: 'DENY',\n clusterFilters: [],\n projectFilters: [],\n filterStyle,\n });\n\n const pluginDecision = await authorize(\n req,\n pluginPerms,\n permissions,\n httpAuth,\n );\n if (pluginDecision.result === AuthorizeResult.ALLOW) {\n return {\n decision: 'ALLOW',\n clusterFilters: [],\n projectFilters: [],\n filterStyle,\n };\n }\n\n let clusterDataMap: Record<string, string> = {};\n let allProjects: string[] = [];\n\n const cachedClusters = (await cache.get(cacheKeys.clusterKey)) as\n | Record<string, string>\n | undefined;\n const cachedProjects = (await cache.get(cacheKeys.projectKey)) as\n | string[]\n | undefined;\n\n if (cachedClusters && cachedProjects) {\n clusterDataMap = cachedClusters;\n allProjects = cachedProjects;\n } else {\n const result = await fetchData(options);\n if (!result) return deny();\n\n clusterDataMap = result.clusters;\n allProjects = result.projects;\n\n await Promise.all([\n cache.set(cacheKeys.clusterKey, clusterDataMap, { ttl: CACHE_TTL }),\n cache.set(cacheKeys.projectKey, allProjects, { ttl: CACHE_TTL }),\n ]);\n }\n\n const { authorizedClusterIds, authorizedClusterProjects } =\n await filterAuthorizedClustersAndProjects(\n req,\n permissions,\n httpAuth,\n clusterDataMap,\n allProjects,\n ...(filterStyle === 'cost' ? (['cost'] as const) : []),\n );\n\n const finalClusters = [\n ...new Set([\n ...authorizedClusterIds,\n ...authorizedClusterProjects.map(r => r.cluster),\n ]),\n ];\n const finalProjects = authorizedClusterProjects.map(r => r.project);\n\n if (finalClusters.length === 0) return deny();\n\n return {\n decision: 'ALLOW',\n clusterFilters: finalClusters,\n projectFilters: finalProjects,\n filterStyle,\n };\n}\n\nasync function resolveOptimizationsAccess(\n req: Request,\n options: RouterOptions,\n): Promise<AccessResult> {\n return resolveAccessForSection(\n req,\n options,\n rosPluginPermissions,\n 'ros',\n { clusterKey: 'all_clusters_map', projectKey: 'all_projects' },\n async opts => {\n const token = await getTokenFromApi(opts);\n const response = await opts.optimizationApi.getRecommendationList(\n { query: { limit: -1, orderHow: 'desc', orderBy: 'last_reported' } },\n { token },\n );\n const list = await response.json();\n\n if ((list as any).errors || !list.data) return null;\n\n const clusters: Record<string, string> = {};\n list.data.forEach(r => {\n if (r.clusterAlias && r.clusterUuid) {\n clusters[r.clusterAlias] = r.clusterUuid;\n }\n });\n const projects = [...new Set(list.data.map(r => r.project))].filter(\n (p): p is string => p !== undefined,\n );\n\n return { clusters, projects };\n },\n );\n}\n\nasync function resolveCostManagementAccess(\n req: Request,\n options: RouterOptions,\n): Promise<AccessResult> {\n return resolveAccessForSection(\n req,\n options,\n costPluginPermissions,\n 'cost',\n { clusterKey: 'cost_clusters', projectKey: 'cost_projects' },\n async opts => {\n let token: string;\n try {\n token = await getTokenFromApi(opts);\n } catch {\n return null;\n }\n\n try {\n const [clustersResp, projectsResp] = await Promise.all([\n opts.costManagementApi.searchOpenShiftClusters('', {\n token,\n limit: 1000,\n }),\n opts.costManagementApi.searchOpenShiftProjects('', {\n token,\n limit: 1000,\n }),\n ]);\n\n const clustersData = await clustersResp.json();\n const projectsData = await projectsResp.json();\n\n if (\n (clustersData as any).errors ||\n (projectsData as any).errors ||\n !clustersData.data ||\n !projectsData.data\n ) {\n return null;\n }\n\n const clusters: Record<string, string> = {};\n clustersData.data.forEach(\n (c: { value: string; cluster_alias: string }) => {\n if (c.cluster_alias && c.value) clusters[c.cluster_alias] = c.value;\n },\n );\n const projects = [\n ...new Set(projectsData.data.map((p: { value: string }) => p.value)),\n ].filter((p): p is string => p !== undefined);\n\n return { clusters, projects };\n } catch {\n return null;\n }\n },\n );\n}\n\nfunction isPathTraversal(proxyPath: string): boolean {\n return /(?:^|\\/)\\.\\.(\\/|$)/.test(proxyPath) || proxyPath.startsWith('/');\n}\n\n/**\n * Parses the raw query string, stripping RBAC-controlled keys and\n * decoding percent-encoded parameters. Returns null if encoding is malformed.\n */\nfunction parseClientQueryParams(\n originalUrl: string,\n rbacControlledKeys: Set<string>,\n): { key: string; value: string }[] | null {\n const rawQuery = originalUrl.split('?')[1] || '';\n const rawParams = rawQuery.split('&').filter(p => p.length > 0);\n const result: { key: string; value: string }[] = [];\n\n for (const param of rawParams) {\n const eqIdx = param.indexOf('=');\n const rawKey = eqIdx >= 0 ? param.substring(0, eqIdx) : param;\n const rawVal = eqIdx >= 0 ? param.substring(eqIdx + 1) : '';\n\n try {\n const decodedKey = decodeURIComponent(rawKey);\n const decodedVal = decodeURIComponent(rawVal);\n if (!rbacControlledKeys.has(decodedKey)) {\n result.push({ key: decodedKey, value: decodedVal });\n }\n } catch {\n return null;\n }\n }\n\n return result;\n}\n\n/**\n * Appends server-side RBAC cluster/project filters to the target URL\n * using the appropriate key format for ROS vs Cost Management APIs.\n */\nfunction injectRbacFilters(targetUrl: URL, access: AccessResult): void {\n const clusterKey =\n access.filterStyle === 'ros' ? 'cluster' : 'filter[exact:cluster]';\n const projectKey =\n access.filterStyle === 'ros' ? 'project' : 'filter[exact:project]';\n\n access.clusterFilters.forEach(c =>\n targetUrl.searchParams.append(clusterKey, c),\n );\n access.projectFilters.forEach(p =>\n targetUrl.searchParams.append(projectKey, p),\n );\n}\n\n/**\n * Server-side proxy that keeps the SSO token on the backend and enforces\n * RBAC before forwarding requests to the Cost Management API.\n *\n * Replaces the previous architecture where the frontend received the SSO\n * token via GET /token and called the Backstage proxy directly.\n */\nexport const secureProxy: (options: RouterOptions) => RequestHandler =\n options => async (req, res) => {\n const { config } = options;\n const proxyPath = req.params[0];\n\n if (!proxyPath) {\n return res.status(400).json({ error: 'Missing proxy path' });\n }\n\n if (isPathTraversal(proxyPath)) {\n return res\n .status(400)\n .json({ error: 'Invalid proxy path: traversal not allowed' });\n }\n\n try {\n const access = await resolveAccess(req, proxyPath, options);\n const actor = await resolveActor(req, options);\n\n if (access.decision !== 'ALLOW') {\n emitAuditLog(options, {\n actor,\n action: 'data_access',\n resource: proxyPath,\n decision: 'DENY',\n filters: {\n clusters: access.clusterFilters,\n projects: access.projectFilters,\n },\n });\n return res.status(403).json({ error: 'Access denied by RBAC policy' });\n }\n\n const token = await getTokenFromApi(options);\n\n const targetBase =\n config?.getOptionalString('costManagementProxyBaseUrl') ??\n DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL;\n const basePath = `${targetBase}/cost-management/v1/`;\n const targetUrl = new URL(proxyPath, basePath);\n\n if (!targetUrl.pathname.startsWith(new URL(basePath).pathname)) {\n return res\n .status(400)\n .json({ error: 'Invalid proxy path: traversal not allowed' });\n }\n\n // Express qs parser converts bracket keys like filter[time_scope_value]\n // into nested objects, losing the flat key format the RHCC API expects.\n const rbacControlledKeys = new Set(\n access.filterStyle === 'ros'\n ? ['cluster', 'project']\n : ['filter[exact:cluster]', 'filter[exact:project]'],\n );\n\n const clientParams = parseClientQueryParams(\n req.originalUrl,\n rbacControlledKeys,\n );\n if (!clientParams) {\n return res\n .status(400)\n .json({ error: 'Malformed percent-encoding in query string' });\n }\n\n for (const { key, value } of clientParams) {\n targetUrl.searchParams.append(key, value);\n }\n\n injectRbacFilters(targetUrl, access);\n\n emitAuditLog(options, {\n actor,\n action: 'data_access',\n resource: proxyPath,\n decision: 'ALLOW',\n filters: {\n clusters: access.clusterFilters,\n projects: access.projectFilters,\n },\n });\n\n const upstreamResponse = await fetch(targetUrl.toString(), {\n headers: {\n 'Content-Type': 'application/json',\n Accept: req.headers.accept || 'application/json',\n Authorization: `Bearer ${token}`,\n },\n method: 'GET',\n });\n\n const contentType = upstreamResponse.headers.get('content-type') || '';\n res.status(upstreamResponse.status);\n\n if (contentType.includes('application/json')) {\n return res.json(await upstreamResponse.json());\n }\n\n res.set('Content-Type', contentType);\n return res.send(await upstreamResponse.text());\n } catch (error) {\n options.logger.error('Secure proxy error', error);\n return res.status(500).json({ error: 'Internal proxy error' });\n }\n };\n"],"names":["authorize","AuthorizeResult","filterAuthorizedClustersAndProjects","rosPluginPermissions","getTokenFromApi","costPluginPermissions","resolveActor","emitAuditLog","DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL"],"mappings":"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"}
1
+ {"version":3,"file":"secureProxy.cjs.js","sources":["../../src/routes/secureProxy.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { RequestHandler, Request } from 'express';\nimport type { RouterOptions } from '../models/RouterOptions';\nimport {\n authorize,\n filterAuthorizedClustersAndProjects,\n} from '../util/checkPermissions';\nimport {\n rosPluginPermissions,\n costPluginPermissions,\n} from '@red-hat-developer-hub/plugin-cost-management-common/permissions';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport { getTokenFromApi, SsoAuthenticationError } from '../util/tokenUtil';\nimport { DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL } from '../util/constant';\nimport { resolveActor, emitAuditLog } from '../util/auditLog';\n\nconst CACHE_TTL = 15 * 60 * 1000;\n\ninterface AccessResult {\n decision: string;\n clusterFilters: string[];\n projectFilters: string[];\n filterStyle: 'ros' | 'cost';\n}\n\n/**\n * Determines the RBAC scope for a given proxy path and resolves\n * the authorized cluster/project filters server-side.\n */\nasync function resolveAccess(\n req: Request,\n proxyPath: string,\n options: RouterOptions,\n): Promise<AccessResult> {\n const isOptimizations = proxyPath.startsWith('recommendations/');\n\n if (isOptimizations) {\n return resolveOptimizationsAccess(req, options);\n }\n return resolveCostManagementAccess(req, options);\n}\n\ninterface CacheConfig {\n clusterKey: string;\n projectKey: string;\n}\n\ntype DataFetcher = (\n options: RouterOptions,\n) => Promise<{ clusters: Record<string, string>; projects: string[] } | null>;\n\n/**\n * Common RBAC resolution: check plugin-level permission, fetch + cache\n * cluster/project data, filter authorised entries, and return the result.\n */\nasync function resolveAccessForSection(\n req: Request,\n options: RouterOptions,\n pluginPerms: typeof rosPluginPermissions,\n filterStyle: AccessResult['filterStyle'],\n cacheKeys: CacheConfig,\n fetchData: DataFetcher,\n): Promise<AccessResult> {\n const { permissions, httpAuth, cache } = options;\n const deny = (): AccessResult => ({\n decision: 'DENY',\n clusterFilters: [],\n projectFilters: [],\n filterStyle,\n });\n\n const pluginDecision = await authorize(\n req,\n pluginPerms,\n permissions,\n httpAuth,\n );\n if (pluginDecision.result === AuthorizeResult.ALLOW) {\n return {\n decision: 'ALLOW',\n clusterFilters: [],\n projectFilters: [],\n filterStyle,\n };\n }\n\n let clusterDataMap: Record<string, string> = {};\n let allProjects: string[] = [];\n\n const cachedClusters = (await cache.get(cacheKeys.clusterKey)) as\n | Record<string, string>\n | undefined;\n const cachedProjects = (await cache.get(cacheKeys.projectKey)) as\n | string[]\n | undefined;\n\n if (cachedClusters && cachedProjects) {\n clusterDataMap = cachedClusters;\n allProjects = cachedProjects;\n } else {\n const result = await fetchData(options);\n if (!result) return deny();\n\n clusterDataMap = result.clusters;\n allProjects = result.projects;\n\n await Promise.all([\n cache.set(cacheKeys.clusterKey, clusterDataMap, { ttl: CACHE_TTL }),\n cache.set(cacheKeys.projectKey, allProjects, { ttl: CACHE_TTL }),\n ]);\n }\n\n const { authorizedClusterIds, authorizedClusterProjects } =\n await filterAuthorizedClustersAndProjects(\n req,\n permissions,\n httpAuth,\n clusterDataMap,\n allProjects,\n ...(filterStyle === 'cost' ? (['cost'] as const) : []),\n );\n\n const finalClusters = [\n ...new Set([\n ...authorizedClusterIds,\n ...authorizedClusterProjects.map(r => r.cluster),\n ]),\n ];\n const finalProjects = authorizedClusterProjects.map(r => r.project);\n\n if (finalClusters.length === 0) return deny();\n\n return {\n decision: 'ALLOW',\n clusterFilters: finalClusters,\n projectFilters: finalProjects,\n filterStyle,\n };\n}\n\nasync function resolveOptimizationsAccess(\n req: Request,\n options: RouterOptions,\n): Promise<AccessResult> {\n return resolveAccessForSection(\n req,\n options,\n rosPluginPermissions,\n 'ros',\n { clusterKey: 'all_clusters_map', projectKey: 'all_projects' },\n async opts => {\n const token = await getTokenFromApi(opts);\n const response = await opts.optimizationApi.getRecommendationList(\n { query: { limit: -1, orderHow: 'desc', orderBy: 'last_reported' } },\n { token },\n );\n const list = await response.json();\n\n if ((list as any).errors || !list.data) return null;\n\n const clusters: Record<string, string> = {};\n list.data.forEach(r => {\n if (r.clusterAlias && r.clusterUuid) {\n clusters[r.clusterAlias] = r.clusterUuid;\n }\n });\n const projects = [...new Set(list.data.map(r => r.project))].filter(\n (p): p is string => p !== undefined,\n );\n\n return { clusters, projects };\n },\n );\n}\n\nasync function resolveCostManagementAccess(\n req: Request,\n options: RouterOptions,\n): Promise<AccessResult> {\n return resolveAccessForSection(\n req,\n options,\n costPluginPermissions,\n 'cost',\n { clusterKey: 'cost_clusters', projectKey: 'cost_projects' },\n async opts => {\n let token: string;\n try {\n token = await getTokenFromApi(opts);\n } catch {\n return null;\n }\n\n try {\n const [clustersResp, projectsResp] = await Promise.all([\n opts.costManagementApi.searchOpenShiftClusters('', {\n token,\n limit: 1000,\n }),\n opts.costManagementApi.searchOpenShiftProjects('', {\n token,\n limit: 1000,\n }),\n ]);\n\n const clustersData = await clustersResp.json();\n const projectsData = await projectsResp.json();\n\n if (\n (clustersData as any).errors ||\n (projectsData as any).errors ||\n !clustersData.data ||\n !projectsData.data\n ) {\n return null;\n }\n\n const clusters: Record<string, string> = {};\n clustersData.data.forEach(\n (c: { value: string; cluster_alias: string }) => {\n if (c.cluster_alias && c.value) clusters[c.cluster_alias] = c.value;\n },\n );\n const projects = [\n ...new Set(projectsData.data.map((p: { value: string }) => p.value)),\n ].filter((p): p is string => p !== undefined);\n\n return { clusters, projects };\n } catch {\n return null;\n }\n },\n );\n}\n\nfunction isPathTraversal(proxyPath: string): boolean {\n return /(?:^|\\/)\\.\\.(\\/|$)/.test(proxyPath) || proxyPath.startsWith('/');\n}\n\n/**\n * Parses the raw query string, stripping RBAC-controlled keys and\n * decoding percent-encoded parameters. Returns null if encoding is malformed.\n */\nfunction parseClientQueryParams(\n originalUrl: string,\n rbacControlledKeys: Set<string>,\n): { key: string; value: string }[] | null {\n const rawQuery = originalUrl.split('?')[1] || '';\n const rawParams = rawQuery.split('&').filter(p => p.length > 0);\n const result: { key: string; value: string }[] = [];\n\n for (const param of rawParams) {\n const eqIdx = param.indexOf('=');\n const rawKey = eqIdx >= 0 ? param.substring(0, eqIdx) : param;\n const rawVal = eqIdx >= 0 ? param.substring(eqIdx + 1) : '';\n\n try {\n const decodedKey = decodeURIComponent(rawKey);\n const decodedVal = decodeURIComponent(rawVal);\n if (!rbacControlledKeys.has(decodedKey)) {\n result.push({ key: decodedKey, value: decodedVal });\n }\n } catch {\n return null;\n }\n }\n\n return result;\n}\n\n/**\n * Appends server-side RBAC cluster/project filters to the target URL\n * using the appropriate key format for ROS vs Cost Management APIs.\n */\nfunction injectRbacFilters(targetUrl: URL, access: AccessResult): void {\n const clusterKey =\n access.filterStyle === 'ros' ? 'cluster' : 'filter[exact:cluster]';\n const projectKey =\n access.filterStyle === 'ros' ? 'project' : 'filter[exact:project]';\n\n access.clusterFilters.forEach(c =>\n targetUrl.searchParams.append(clusterKey, c),\n );\n access.projectFilters.forEach(p =>\n targetUrl.searchParams.append(projectKey, p),\n );\n}\n\n/**\n * Server-side proxy that keeps the SSO token on the backend and enforces\n * RBAC before forwarding requests to the Cost Management API.\n *\n * Replaces the previous architecture where the frontend received the SSO\n * token via GET /token and called the Backstage proxy directly.\n */\nexport const secureProxy: (options: RouterOptions) => RequestHandler =\n options => async (req, res) => {\n const { config } = options;\n const proxyPath = req.params[0];\n\n if (!proxyPath) {\n return res.status(400).json({ error: 'Missing proxy path' });\n }\n\n if (isPathTraversal(proxyPath)) {\n return res\n .status(400)\n .json({ error: 'Invalid proxy path: traversal not allowed' });\n }\n\n try {\n const access = await resolveAccess(req, proxyPath, options);\n const actor = await resolveActor(req, options);\n\n if (access.decision !== 'ALLOW') {\n emitAuditLog(options, {\n actor,\n action: 'data_access',\n resource: proxyPath,\n decision: 'DENY',\n filters: {\n clusters: access.clusterFilters,\n projects: access.projectFilters,\n },\n });\n return res.status(403).json({ error: 'Access denied by RBAC policy' });\n }\n\n const token = await getTokenFromApi(options);\n\n const targetBase =\n config?.getOptionalString('costManagementProxyBaseUrl') ??\n DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL;\n const basePath = `${targetBase}/cost-management/v1/`;\n const targetUrl = new URL(proxyPath, basePath);\n\n if (!targetUrl.pathname.startsWith(new URL(basePath).pathname)) {\n return res\n .status(400)\n .json({ error: 'Invalid proxy path: traversal not allowed' });\n }\n\n // Express qs parser converts bracket keys like filter[time_scope_value]\n // into nested objects, losing the flat key format the RHCC API expects.\n const rbacControlledKeys = new Set(\n access.filterStyle === 'ros'\n ? ['cluster', 'project']\n : ['filter[exact:cluster]', 'filter[exact:project]'],\n );\n\n const clientParams = parseClientQueryParams(\n req.originalUrl,\n rbacControlledKeys,\n );\n if (!clientParams) {\n return res\n .status(400)\n .json({ error: 'Malformed percent-encoding in query string' });\n }\n\n for (const { key, value } of clientParams) {\n targetUrl.searchParams.append(key, value);\n }\n\n injectRbacFilters(targetUrl, access);\n\n emitAuditLog(options, {\n actor,\n action: 'data_access',\n resource: proxyPath,\n decision: 'ALLOW',\n filters: {\n clusters: access.clusterFilters,\n projects: access.projectFilters,\n },\n });\n\n const upstreamResponse = await fetch(targetUrl.toString(), {\n headers: {\n 'Content-Type': 'application/json',\n Accept: req.headers.accept || 'application/json',\n Authorization: `Bearer ${token}`,\n },\n method: 'GET',\n });\n\n const contentType = upstreamResponse.headers.get('content-type') || '';\n res.status(upstreamResponse.status);\n\n if (contentType.includes('application/json')) {\n return res.json(await upstreamResponse.json());\n }\n\n res.set('Content-Type', contentType);\n return res.send(await upstreamResponse.text());\n } catch (error) {\n options.logger.error('Secure proxy error', error);\n\n if (error instanceof SsoAuthenticationError) {\n return res.status(502).json({\n error:\n 'Unable to authenticate with the hybrid cloud console. ' +\n 'Please check your service account credentials (clientId/clientSecret) in the RHDH Cost Management configuration.',\n });\n }\n\n return res.status(500).json({ error: 'Internal proxy error' });\n }\n };\n"],"names":["authorize","AuthorizeResult","filterAuthorizedClustersAndProjects","rosPluginPermissions","getTokenFromApi","costPluginPermissions","resolveActor","emitAuditLog","DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL","SsoAuthenticationError"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"costManagementService.cjs.js","sources":["../../src/service/costManagementService.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createServiceFactory,\n createServiceRef,\n} from '@backstage/backend-plugin-api';\nimport type { CostManagementSlimApi } from '@red-hat-developer-hub/plugin-cost-management-common/clients';\nimport { CostManagementSlimClient } from '@red-hat-developer-hub/plugin-cost-management-common/clients';\nimport { DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL } from '../util/constant';\n\nexport const costManagementServiceRef = createServiceRef<CostManagementSlimApi>(\n {\n id: 'cost-management-client',\n defaultFactory: async service =>\n createServiceFactory({\n service,\n deps: {\n configApi: coreServices.rootConfig,\n },\n async factory({ configApi }): Promise<CostManagementSlimApi> {\n // Note: The client appends /cost-management/v1/... to this base URL\n const baseUrl =\n configApi.getOptionalString('costManagementProxyBaseUrl') ??\n DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL;\n\n return new CostManagementSlimClient({\n discoveryApi: {\n async getBaseUrl(_pluginId?: string) {\n return baseUrl;\n },\n },\n }) as CostManagementSlimApi;\n },\n }),\n },\n);\n"],"names":["createServiceRef","createServiceFactory","coreServices","DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL","CostManagementSlimClient"],"mappings":";;;;;;AAyBO,MAAM,wBAA2B,GAAAA,iCAAA;AAAA,EACtC;AAAA,IACE,EAAI,EAAA,wBAAA;AAAA,IACJ,cAAA,EAAgB,OAAM,OAAA,KACpBC,qCAAqB,CAAA;AAAA,MACnB,OAAA;AAAA,MACA,IAAM,EAAA;AAAA,QACJ,WAAWC,6BAAa,CAAA;AAAA,OAC1B;AAAA,MACA,MAAM,OAAA,CAAQ,EAAE,SAAA,EAA6C,EAAA;AAE3D,QAAA,MAAM,OACJ,GAAA,SAAA,CAAU,iBAAkB,CAAA,4BAA4B,CACxD,IAAAC,+CAAA;AAEF,QAAA,OAAO,IAAIC,gCAAyB,CAAA;AAAA,UAClC,YAAc,EAAA;AAAA,YACZ,MAAM,WAAW,SAAoB,EAAA;AACnC,cAAO,OAAA,OAAA;AAAA;AACT;AACF,SACD,CAAA;AAAA;AACH,KACD;AAAA;AAEP;;;;"}
1
+ {"version":3,"file":"costManagementService.cjs.js","sources":["../../src/service/costManagementService.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createServiceFactory,\n createServiceRef,\n} from '@backstage/backend-plugin-api';\nimport type { CostManagementSlimApi } from '@red-hat-developer-hub/plugin-cost-management-common/clients';\nimport { CostManagementSlimClient } from '@red-hat-developer-hub/plugin-cost-management-common/clients';\nimport { DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL } from '../util/constant';\n\nexport const costManagementServiceRef = createServiceRef<CostManagementSlimApi>(\n {\n id: 'cost-management-client',\n defaultFactory: async service =>\n createServiceFactory({\n service,\n deps: {\n configApi: coreServices.rootConfig,\n },\n async factory({ configApi }): Promise<CostManagementSlimApi> {\n // Note: The client appends /cost-management/v1/... to this base URL\n const baseUrl =\n configApi.getOptionalString('costManagementProxyBaseUrl') ??\n DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL;\n\n return new CostManagementSlimClient({\n discoveryApi: {\n async getBaseUrl(_pluginId?: string) {\n return baseUrl;\n },\n },\n }) as CostManagementSlimApi;\n },\n }),\n },\n);\n"],"names":["createServiceRef","createServiceFactory","coreServices","DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL","CostManagementSlimClient"],"mappings":";;;;;;AAyBO,MAAM,wBAAA,GAA2BA,iCAAA;AAAA,EACtC;AAAA,IACE,EAAA,EAAI,wBAAA;AAAA,IACJ,cAAA,EAAgB,OAAM,OAAA,KACpBC,qCAAA,CAAqB;AAAA,MACnB,OAAA;AAAA,MACA,IAAA,EAAM;AAAA,QACJ,WAAWC,6BAAA,CAAa;AAAA,OAC1B;AAAA,MACA,MAAM,OAAA,CAAQ,EAAE,SAAA,EAAU,EAAmC;AAE3D,QAAA,MAAM,OAAA,GACJ,SAAA,CAAU,iBAAA,CAAkB,4BAA4B,CAAA,IACxDC,+CAAA;AAEF,QAAA,OAAO,IAAIC,gCAAA,CAAyB;AAAA,UAClC,YAAA,EAAc;AAAA,YACZ,MAAM,WAAW,SAAA,EAAoB;AACnC,cAAA,OAAO,OAAA;AAAA,YACT;AAAA;AACF,SACD,CAAA;AAAA,MACH;AAAA,KACD;AAAA;AAEP;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"optimizationsService.cjs.js","sources":["../../src/service/optimizationsService.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createServiceFactory,\n createServiceRef,\n} from '@backstage/backend-plugin-api';\nimport type { OptimizationsApi } from '@red-hat-developer-hub/plugin-cost-management-common/clients';\nimport { OptimizationsClient } from '@red-hat-developer-hub/plugin-cost-management-common/clients';\nimport { DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL } from '../util/constant';\n\nexport const optimizationServiceRef = createServiceRef<OptimizationsApi>({\n id: 'optimization-client',\n defaultFactory: async service =>\n createServiceFactory({\n service,\n deps: {\n configApi: coreServices.rootConfig,\n },\n async factory({ configApi }): Promise<OptimizationsApi> {\n // Base URL without /cost-management/v1 since OptimizationsClient appends it\n const baseUrl =\n configApi.getOptionalString('optimizationsBaseUrl') ??\n DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL;\n\n return new OptimizationsClient({\n discoveryApi: {\n async getBaseUrl(_pluginId?: string) {\n return baseUrl;\n },\n },\n }) as OptimizationsApi;\n },\n }),\n});\n"],"names":["createServiceRef","createServiceFactory","coreServices","DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL","OptimizationsClient"],"mappings":";;;;;;AAyBO,MAAM,yBAAyBA,iCAAmC,CAAA;AAAA,EACvE,EAAI,EAAA,qBAAA;AAAA,EACJ,cAAA,EAAgB,OAAM,OAAA,KACpBC,qCAAqB,CAAA;AAAA,IACnB,OAAA;AAAA,IACA,IAAM,EAAA;AAAA,MACJ,WAAWC,6BAAa,CAAA;AAAA,KAC1B;AAAA,IACA,MAAM,OAAA,CAAQ,EAAE,SAAA,EAAwC,EAAA;AAEtD,MAAA,MAAM,OACJ,GAAA,SAAA,CAAU,iBAAkB,CAAA,sBAAsB,CAClD,IAAAC,+CAAA;AAEF,MAAA,OAAO,IAAIC,2BAAoB,CAAA;AAAA,QAC7B,YAAc,EAAA;AAAA,UACZ,MAAM,WAAW,SAAoB,EAAA;AACnC,YAAO,OAAA,OAAA;AAAA;AACT;AACF,OACD,CAAA;AAAA;AACH,GACD;AACL,CAAC;;;;"}
1
+ {"version":3,"file":"optimizationsService.cjs.js","sources":["../../src/service/optimizationsService.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createServiceFactory,\n createServiceRef,\n} from '@backstage/backend-plugin-api';\nimport type { OptimizationsApi } from '@red-hat-developer-hub/plugin-cost-management-common/clients';\nimport { OptimizationsClient } from '@red-hat-developer-hub/plugin-cost-management-common/clients';\nimport { DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL } from '../util/constant';\n\nexport const optimizationServiceRef = createServiceRef<OptimizationsApi>({\n id: 'optimization-client',\n defaultFactory: async service =>\n createServiceFactory({\n service,\n deps: {\n configApi: coreServices.rootConfig,\n },\n async factory({ configApi }): Promise<OptimizationsApi> {\n // Base URL without /cost-management/v1 since OptimizationsClient appends it\n const baseUrl =\n configApi.getOptionalString('optimizationsBaseUrl') ??\n DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL;\n\n return new OptimizationsClient({\n discoveryApi: {\n async getBaseUrl(_pluginId?: string) {\n return baseUrl;\n },\n },\n }) as OptimizationsApi;\n },\n }),\n});\n"],"names":["createServiceRef","createServiceFactory","coreServices","DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL","OptimizationsClient"],"mappings":";;;;;;AAyBO,MAAM,yBAAyBA,iCAAA,CAAmC;AAAA,EACvE,EAAA,EAAI,qBAAA;AAAA,EACJ,cAAA,EAAgB,OAAM,OAAA,KACpBC,qCAAA,CAAqB;AAAA,IACnB,OAAA;AAAA,IACA,IAAA,EAAM;AAAA,MACJ,WAAWC,6BAAA,CAAa;AAAA,KAC1B;AAAA,IACA,MAAM,OAAA,CAAQ,EAAE,SAAA,EAAU,EAA8B;AAEtD,MAAA,MAAM,OAAA,GACJ,SAAA,CAAU,iBAAA,CAAkB,sBAAsB,CAAA,IAClDC,+CAAA;AAEF,MAAA,OAAO,IAAIC,2BAAA,CAAoB;AAAA,QAC7B,YAAA,EAAc;AAAA,UACZ,MAAM,WAAW,SAAA,EAAoB;AACnC,YAAA,OAAO,OAAA;AAAA,UACT;AAAA;AACF,OACD,CAAA;AAAA,IACH;AAAA,GACD;AACL,CAAC;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport type { RouterOptions } from '../models/RouterOptions';\nimport { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';\nimport {\n rosPluginPermissions,\n rosApplyPermissions,\n costPluginPermissions,\n} from '@red-hat-developer-hub/plugin-cost-management-common/permissions';\nimport { getAccess } from '../routes/access';\nimport { getCostManagementAccess } from '../routes/costManagementAccess';\nimport { secureProxy } from '../routes/secureProxy';\nimport { applyRecommendation } from '../routes/applyRecommendation';\n\n/** @public */\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const router = Router();\n const permissionsIntegrationRouter = createPermissionIntegrationRouter({\n permissions: [\n ...rosPluginPermissions,\n ...rosApplyPermissions,\n ...costPluginPermissions,\n ],\n });\n\n router.use(express.json());\n router.use(permissionsIntegrationRouter);\n\n router.get('/health', (_req, res) => {\n res.json({ status: 'ok' });\n });\n\n router.get('/access', getAccess(options));\n\n router.get('/access/cost-management', getCostManagementAccess(options));\n\n router.post('/apply-recommendation', applyRecommendation(options));\n\n router.get('/proxy/*', secureProxy(options));\n\n return router;\n}\n"],"names":["Router","createPermissionIntegrationRouter","rosPluginPermissions","rosApplyPermissions","costPluginPermissions","express","getAccess","getCostManagementAccess","applyRecommendation","secureProxy"],"mappings":";;;;;;;;;;;;;;;;AA+BA,eAAsB,aACpB,OACyB,EAAA;AACzB,EAAA,MAAM,SAASA,uBAAO,EAAA;AACtB,EAAA,MAAM,+BAA+BC,sDAAkC,CAAA;AAAA,IACrE,WAAa,EAAA;AAAA,MACX,GAAGC,gCAAA;AAAA,MACH,GAAGC,+BAAA;AAAA,MACH,GAAGC;AAAA;AACL,GACD,CAAA;AAED,EAAO,MAAA,CAAA,GAAA,CAAIC,wBAAQ,CAAA,IAAA,EAAM,CAAA;AACzB,EAAA,MAAA,CAAO,IAAI,4BAA4B,CAAA;AAEvC,EAAA,MAAA,CAAO,GAAI,CAAA,SAAA,EAAW,CAAC,IAAA,EAAM,GAAQ,KAAA;AACnC,IAAA,GAAA,CAAI,IAAK,CAAA,EAAE,MAAQ,EAAA,IAAA,EAAM,CAAA;AAAA,GAC1B,CAAA;AAED,EAAA,MAAA,CAAO,GAAI,CAAA,SAAA,EAAWC,gBAAU,CAAA,OAAO,CAAC,CAAA;AAExC,EAAA,MAAA,CAAO,GAAI,CAAA,yBAAA,EAA2BC,4CAAwB,CAAA,OAAO,CAAC,CAAA;AAEtE,EAAA,MAAA,CAAO,IAAK,CAAA,uBAAA,EAAyBC,uCAAoB,CAAA,OAAO,CAAC,CAAA;AAEjE,EAAA,MAAA,CAAO,GAAI,CAAA,UAAA,EAAYC,uBAAY,CAAA,OAAO,CAAC,CAAA;AAE3C,EAAO,OAAA,MAAA;AACT;;;;"}
1
+ {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport type { RouterOptions } from '../models/RouterOptions';\nimport { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';\nimport {\n rosPluginPermissions,\n rosApplyPermissions,\n costPluginPermissions,\n} from '@red-hat-developer-hub/plugin-cost-management-common/permissions';\nimport { getAccess } from '../routes/access';\nimport { getCostManagementAccess } from '../routes/costManagementAccess';\nimport { secureProxy } from '../routes/secureProxy';\nimport { applyRecommendation } from '../routes/applyRecommendation';\n\n/** @public */\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const router = Router();\n const permissionsIntegrationRouter = createPermissionIntegrationRouter({\n permissions: [\n ...rosPluginPermissions,\n ...rosApplyPermissions,\n ...costPluginPermissions,\n ],\n });\n\n router.use(express.json());\n router.use(permissionsIntegrationRouter);\n\n router.get('/health', (_req, res) => {\n res.json({ status: 'ok' });\n });\n\n router.get('/access', getAccess(options));\n\n router.get('/access/cost-management', getCostManagementAccess(options));\n\n router.post('/apply-recommendation', applyRecommendation(options));\n\n router.get('/proxy/*', secureProxy(options));\n\n return router;\n}\n"],"names":["Router","createPermissionIntegrationRouter","rosPluginPermissions","rosApplyPermissions","costPluginPermissions","express","getAccess","getCostManagementAccess","applyRecommendation","secureProxy"],"mappings":";;;;;;;;;;;;;;;;AA+BA,eAAsB,aACpB,OAAA,EACyB;AACzB,EAAA,MAAM,SAASA,uBAAA,EAAO;AACtB,EAAA,MAAM,+BAA+BC,sDAAA,CAAkC;AAAA,IACrE,WAAA,EAAa;AAAA,MACX,GAAGC,gCAAA;AAAA,MACH,GAAGC,+BAAA;AAAA,MACH,GAAGC;AAAA;AACL,GACD,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAIC,wBAAA,CAAQ,IAAA,EAAM,CAAA;AACzB,EAAA,MAAA,CAAO,IAAI,4BAA4B,CAAA;AAEvC,EAAA,MAAA,CAAO,GAAA,CAAI,SAAA,EAAW,CAAC,IAAA,EAAM,GAAA,KAAQ;AACnC,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,MAAA,EAAQ,IAAA,EAAM,CAAA;AAAA,EAC3B,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAI,SAAA,EAAWC,gBAAA,CAAU,OAAO,CAAC,CAAA;AAExC,EAAA,MAAA,CAAO,GAAA,CAAI,yBAAA,EAA2BC,4CAAA,CAAwB,OAAO,CAAC,CAAA;AAEtE,EAAA,MAAA,CAAO,IAAA,CAAK,uBAAA,EAAyBC,uCAAA,CAAoB,OAAO,CAAC,CAAA;AAEjE,EAAA,MAAA,CAAO,GAAA,CAAI,UAAA,EAAYC,uBAAA,CAAY,OAAO,CAAC,CAAA;AAE3C,EAAA,OAAO,MAAA;AACT;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"auditLog.cjs.js","sources":["../../src/util/auditLog.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { Request } from 'express';\nimport type { RouterOptions } from '../models/RouterOptions';\n\nexport interface AuditEntry {\n actor: string;\n action: string;\n resource: string;\n decision: string;\n filters?: {\n clusters: string[];\n projects: string[];\n };\n meta?: Record<string, unknown>;\n}\n\n/**\n * Resolves the user entity ref from a request using the Backstage auth chain.\n * Falls back to 'unknown' if user identity cannot be determined.\n */\nexport async function resolveActor(\n req: Request,\n options: RouterOptions,\n): Promise<string> {\n try {\n const credentials = await options.httpAuth.credentials(req);\n const info = await options.userInfo.getUserInfo(credentials);\n return info.userEntityRef;\n } catch (error) {\n options.logger.warn('Failed to resolve actor identity', error);\n return 'unknown';\n }\n}\n\nexport function emitAuditLog(options: RouterOptions, entry: AuditEntry): void {\n options.logger.info(JSON.stringify({ audit: true, ...entry }));\n}\n"],"names":[],"mappings":";;AAmCsB,eAAA,YAAA,CACpB,KACA,OACiB,EAAA;AACjB,EAAI,IAAA;AACF,IAAA,MAAM,WAAc,GAAA,MAAM,OAAQ,CAAA,QAAA,CAAS,YAAY,GAAG,CAAA;AAC1D,IAAA,MAAM,IAAO,GAAA,MAAM,OAAQ,CAAA,QAAA,CAAS,YAAY,WAAW,CAAA;AAC3D,IAAA,OAAO,IAAK,CAAA,aAAA;AAAA,WACL,KAAO,EAAA;AACd,IAAQ,OAAA,CAAA,MAAA,CAAO,IAAK,CAAA,kCAAA,EAAoC,KAAK,CAAA;AAC7D,IAAO,OAAA,SAAA;AAAA;AAEX;AAEgB,SAAA,YAAA,CAAa,SAAwB,KAAyB,EAAA;AAC5E,EAAQ,OAAA,CAAA,MAAA,CAAO,IAAK,CAAA,IAAA,CAAK,SAAU,CAAA,EAAE,OAAO,IAAM,EAAA,GAAG,KAAM,EAAC,CAAC,CAAA;AAC/D;;;;;"}
1
+ {"version":3,"file":"auditLog.cjs.js","sources":["../../src/util/auditLog.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { Request } from 'express';\nimport type { RouterOptions } from '../models/RouterOptions';\n\nexport interface AuditEntry {\n actor: string;\n action: string;\n resource: string;\n decision: string;\n filters?: {\n clusters: string[];\n projects: string[];\n };\n meta?: Record<string, unknown>;\n}\n\n/**\n * Resolves the user entity ref from a request using the Backstage auth chain.\n * Falls back to 'unknown' if user identity cannot be determined.\n */\nexport async function resolveActor(\n req: Request,\n options: RouterOptions,\n): Promise<string> {\n try {\n const credentials = await options.httpAuth.credentials(req);\n const info = await options.userInfo.getUserInfo(credentials);\n return info.userEntityRef;\n } catch (error) {\n options.logger.warn('Failed to resolve actor identity', error);\n return 'unknown';\n }\n}\n\nexport function emitAuditLog(options: RouterOptions, entry: AuditEntry): void {\n options.logger.info(JSON.stringify({ audit: true, ...entry }));\n}\n"],"names":[],"mappings":";;AAmCA,eAAsB,YAAA,CACpB,KACA,OAAA,EACiB;AACjB,EAAA,IAAI;AACF,IAAA,MAAM,WAAA,GAAc,MAAM,OAAA,CAAQ,QAAA,CAAS,YAAY,GAAG,CAAA;AAC1D,IAAA,MAAM,IAAA,GAAO,MAAM,OAAA,CAAQ,QAAA,CAAS,YAAY,WAAW,CAAA;AAC3D,IAAA,OAAO,IAAA,CAAK,aAAA;AAAA,EACd,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,MAAA,CAAO,IAAA,CAAK,kCAAA,EAAoC,KAAK,CAAA;AAC7D,IAAA,OAAO,SAAA;AAAA,EACT;AACF;AAEO,SAAS,YAAA,CAAa,SAAwB,KAAA,EAAyB;AAC5E,EAAA,OAAA,CAAQ,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,EAAE,OAAO,IAAA,EAAM,GAAG,KAAA,EAAO,CAAC,CAAA;AAC/D;;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"checkPermissions.cjs.js","sources":["../../src/util/checkPermissions.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Request as HttpRequest } from 'express-serve-static-core';\nimport {\n AuthorizePermissionRequest,\n AuthorizePermissionResponse,\n AuthorizeResult,\n BasicPermission,\n} from '@backstage/plugin-permission-common';\nimport {\n PermissionsService,\n HttpAuthService,\n} from '@backstage/backend-plugin-api';\nimport {\n rosClusterProjectPermission,\n rosClusterSpecificPermission,\n costClusterSpecificPermission,\n costClusterProjectPermission,\n} from '@red-hat-developer-hub/plugin-cost-management-common/permissions';\n\n/** Permission type for cluster-level access */\nexport type ClusterPermissionType = 'ros' | 'cost';\n\nexport interface ClusterProjectResult {\n cluster: string;\n project: string;\n}\n\nexport interface CombinedAuthorizationResult {\n /** Clusters authorized via cluster-only permissions (all projects allowed) */\n authorizedClusterIds: string[];\n /** Specific cluster-project combinations authorized */\n authorizedClusterProjects: ClusterProjectResult[];\n}\n\n/**\n * Checks if the user has ANY of the given permissions (OR logic).\n * Optimized to use a single batch authorization call instead of multiple parallel calls.\n *\n * @param request - The HTTP request\n * @param anyOfPermissions - Array of permissions to check (user needs at least one)\n * @param permissionsSvc - The permissions service\n * @param httpAuth - The HTTP auth service\n * @returns Authorization response with ALLOW if any permission is granted, DENY otherwise\n */\nexport const authorize = async (\n request: HttpRequest,\n anyOfPermissions: BasicPermission[],\n permissionsSvc: PermissionsService,\n httpAuth: HttpAuthService,\n): Promise<AuthorizePermissionResponse> => {\n const credentials = await httpAuth.credentials(request);\n\n // Single batch call for all permissions\n const permissionRequests = anyOfPermissions.map(permission => ({\n permission,\n }));\n const decisions = await permissionsSvc.authorize(permissionRequests, {\n credentials,\n });\n\n // Return ALLOW if any permission is granted\n const allow = decisions.find(d => d.result === AuthorizeResult.ALLOW);\n return (\n allow || {\n result: AuthorizeResult.DENY,\n }\n );\n};\n\n/**\n * Combines cluster-only and cluster-project permission checks into a single optimized flow.\n * Uses permission hierarchy where project-level access also grants cluster access.\n *\n * Permission Hierarchy:\n * - cost/{cluster} → grants access to cluster + ALL projects in that cluster\n * - cost/{cluster}/{project} → grants access to cluster + that specific project\n *\n * @param request - The HTTP request\n * @param permissionsSvc - The permissions service\n * @param httpAuth - The HTTP auth service\n * @param clusterDataMap - Map of clusterName → clusterId\n * @param allProjects - Array of all project names\n * @param permissionType - 'ros' or 'cost' permission namespace (defaults to 'ros')\n * @returns Object containing both cluster-level and project-level authorizations\n */\nexport const filterAuthorizedClustersAndProjects = async (\n request: HttpRequest,\n permissionsSvc: PermissionsService,\n httpAuth: HttpAuthService,\n clusterDataMap: Record<string, string>,\n allProjects: string[],\n permissionType: ClusterPermissionType = 'ros',\n): Promise<CombinedAuthorizationResult> => {\n const credentials = await httpAuth.credentials(request);\n const allClusterNames: string[] = Object.keys(clusterDataMap);\n const allClusterIds: string[] = Object.values(clusterDataMap);\n\n // Early exit if no data\n if (allClusterNames.length === 0) {\n return {\n authorizedClusterIds: [],\n authorizedClusterProjects: [],\n };\n }\n\n // Select appropriate permission functions based on type\n const getClusterPermission =\n permissionType === 'cost'\n ? costClusterSpecificPermission\n : rosClusterSpecificPermission;\n\n const getClusterProjectPermission =\n permissionType === 'cost'\n ? costClusterProjectPermission\n : rosClusterProjectPermission;\n\n const numClusters = allClusterNames.length;\n\n // Step 1: Check cluster-level permissions first\n const clusterPermissionRequests: AuthorizePermissionRequest[] =\n allClusterNames.map(clusterName => {\n const perm = getClusterPermission(clusterName);\n return { permission: perm };\n });\n\n const clusterDecisions = await permissionsSvc.authorize(\n clusterPermissionRequests,\n {\n credentials,\n },\n );\n\n // Track clusters with and without full access\n const clustersWithFullAccess = new Set<string>();\n const clustersWithoutFullAccess: number[] = [];\n\n for (let i = 0; i < numClusters; i++) {\n const clusterName = allClusterNames[i];\n const clusterId = allClusterIds[i];\n const clusterIdentifier =\n permissionType === 'cost' ? clusterName : clusterId;\n const decision = clusterDecisions[i].result;\n\n if (decision === AuthorizeResult.ALLOW) {\n // User has full cluster access\n clustersWithFullAccess.add(clusterIdentifier);\n } else {\n // No cluster access - will need to check project-level permissions\n clustersWithoutFullAccess.push(i);\n }\n }\n\n // Step 2: Check project-level permissions only for clusters without full access\n const authorizedClusterProjects: ClusterProjectResult[] = [];\n const clustersGrantedViaProjects = new Set<string>();\n\n if (clustersWithoutFullAccess.length > 0 && allProjects.length > 0) {\n const numProjectChecks =\n clustersWithoutFullAccess.length * allProjects.length;\n const projectPermissionRequests: AuthorizePermissionRequest[] = new Array(\n numProjectChecks,\n );\n const projectPermissionMap: ClusterProjectResult[] = new Array(\n numProjectChecks,\n );\n\n // Build requests only for clusters that don't have full access\n let idx = 0;\n for (const clusterIdx of clustersWithoutFullAccess) {\n const clusterName = allClusterNames[clusterIdx];\n const clusterId = allClusterIds[clusterIdx];\n const clusterIdentifier =\n permissionType === 'cost' ? clusterName : clusterId;\n\n for (let j = 0; j < allProjects.length; j++) {\n const projectName = allProjects[j];\n const perm = getClusterProjectPermission(clusterName, projectName);\n\n projectPermissionRequests[idx] = {\n permission: perm,\n };\n\n projectPermissionMap[idx] = {\n cluster: clusterIdentifier,\n project: projectName,\n };\n\n idx++;\n }\n }\n\n // Batch check project-level permissions\n const projectDecisions = await permissionsSvc.authorize(\n projectPermissionRequests,\n { credentials },\n );\n\n // Process project-level results\n for (let i = 0; i < projectDecisions.length; i++) {\n const decision = projectDecisions[i].result;\n\n if (decision === AuthorizeResult.ALLOW) {\n const result = projectPermissionMap[i];\n authorizedClusterProjects.push(result);\n // Project-level permission also grants cluster access\n clustersGrantedViaProjects.add(result.cluster);\n }\n }\n }\n\n // Step 3: Combine clusters from both full access and project-level grants\n const authorizedClusterIds = [\n ...clustersWithFullAccess,\n ...clustersGrantedViaProjects,\n ];\n\n return {\n authorizedClusterIds,\n authorizedClusterProjects,\n };\n};\n"],"names":["AuthorizeResult","costClusterSpecificPermission","rosClusterSpecificPermission","costClusterProjectPermission","rosClusterProjectPermission"],"mappings":"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"}
1
+ {"version":3,"file":"checkPermissions.cjs.js","sources":["../../src/util/checkPermissions.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Request as HttpRequest } from 'express-serve-static-core';\nimport {\n AuthorizePermissionRequest,\n AuthorizePermissionResponse,\n AuthorizeResult,\n BasicPermission,\n} from '@backstage/plugin-permission-common';\nimport {\n PermissionsService,\n HttpAuthService,\n} from '@backstage/backend-plugin-api';\nimport {\n rosClusterProjectPermission,\n rosClusterSpecificPermission,\n costClusterSpecificPermission,\n costClusterProjectPermission,\n} from '@red-hat-developer-hub/plugin-cost-management-common/permissions';\n\n/** Permission type for cluster-level access */\nexport type ClusterPermissionType = 'ros' | 'cost';\n\nexport interface ClusterProjectResult {\n cluster: string;\n project: string;\n}\n\nexport interface CombinedAuthorizationResult {\n /** Clusters authorized via cluster-only permissions (all projects allowed) */\n authorizedClusterIds: string[];\n /** Specific cluster-project combinations authorized */\n authorizedClusterProjects: ClusterProjectResult[];\n}\n\n/**\n * Checks if the user has ANY of the given permissions (OR logic).\n * Optimized to use a single batch authorization call instead of multiple parallel calls.\n *\n * @param request - The HTTP request\n * @param anyOfPermissions - Array of permissions to check (user needs at least one)\n * @param permissionsSvc - The permissions service\n * @param httpAuth - The HTTP auth service\n * @returns Authorization response with ALLOW if any permission is granted, DENY otherwise\n */\nexport const authorize = async (\n request: HttpRequest,\n anyOfPermissions: BasicPermission[],\n permissionsSvc: PermissionsService,\n httpAuth: HttpAuthService,\n): Promise<AuthorizePermissionResponse> => {\n const credentials = await httpAuth.credentials(request);\n\n // Single batch call for all permissions\n const permissionRequests = anyOfPermissions.map(permission => ({\n permission,\n }));\n const decisions = await permissionsSvc.authorize(permissionRequests, {\n credentials,\n });\n\n // Return ALLOW if any permission is granted\n const allow = decisions.find(d => d.result === AuthorizeResult.ALLOW);\n return (\n allow || {\n result: AuthorizeResult.DENY,\n }\n );\n};\n\n/**\n * Combines cluster-only and cluster-project permission checks into a single optimized flow.\n * Uses permission hierarchy where project-level access also grants cluster access.\n *\n * Permission Hierarchy:\n * - cost/{cluster} → grants access to cluster + ALL projects in that cluster\n * - cost/{cluster}/{project} → grants access to cluster + that specific project\n *\n * @param request - The HTTP request\n * @param permissionsSvc - The permissions service\n * @param httpAuth - The HTTP auth service\n * @param clusterDataMap - Map of clusterName → clusterId\n * @param allProjects - Array of all project names\n * @param permissionType - 'ros' or 'cost' permission namespace (defaults to 'ros')\n * @returns Object containing both cluster-level and project-level authorizations\n */\nexport const filterAuthorizedClustersAndProjects = async (\n request: HttpRequest,\n permissionsSvc: PermissionsService,\n httpAuth: HttpAuthService,\n clusterDataMap: Record<string, string>,\n allProjects: string[],\n permissionType: ClusterPermissionType = 'ros',\n): Promise<CombinedAuthorizationResult> => {\n const credentials = await httpAuth.credentials(request);\n const allClusterNames: string[] = Object.keys(clusterDataMap);\n const allClusterIds: string[] = Object.values(clusterDataMap);\n\n // Early exit if no data\n if (allClusterNames.length === 0) {\n return {\n authorizedClusterIds: [],\n authorizedClusterProjects: [],\n };\n }\n\n // Select appropriate permission functions based on type\n const getClusterPermission =\n permissionType === 'cost'\n ? costClusterSpecificPermission\n : rosClusterSpecificPermission;\n\n const getClusterProjectPermission =\n permissionType === 'cost'\n ? costClusterProjectPermission\n : rosClusterProjectPermission;\n\n const numClusters = allClusterNames.length;\n\n // Step 1: Check cluster-level permissions first\n const clusterPermissionRequests: AuthorizePermissionRequest[] =\n allClusterNames.map(clusterName => {\n const perm = getClusterPermission(clusterName);\n return { permission: perm };\n });\n\n const clusterDecisions = await permissionsSvc.authorize(\n clusterPermissionRequests,\n {\n credentials,\n },\n );\n\n // Track clusters with and without full access\n const clustersWithFullAccess = new Set<string>();\n const clustersWithoutFullAccess: number[] = [];\n\n for (let i = 0; i < numClusters; i++) {\n const clusterName = allClusterNames[i];\n const clusterId = allClusterIds[i];\n const clusterIdentifier =\n permissionType === 'cost' ? clusterName : clusterId;\n const decision = clusterDecisions[i].result;\n\n if (decision === AuthorizeResult.ALLOW) {\n // User has full cluster access\n clustersWithFullAccess.add(clusterIdentifier);\n } else {\n // No cluster access - will need to check project-level permissions\n clustersWithoutFullAccess.push(i);\n }\n }\n\n // Step 2: Check project-level permissions only for clusters without full access\n const authorizedClusterProjects: ClusterProjectResult[] = [];\n const clustersGrantedViaProjects = new Set<string>();\n\n if (clustersWithoutFullAccess.length > 0 && allProjects.length > 0) {\n const numProjectChecks =\n clustersWithoutFullAccess.length * allProjects.length;\n const projectPermissionRequests: AuthorizePermissionRequest[] = new Array(\n numProjectChecks,\n );\n const projectPermissionMap: ClusterProjectResult[] = new Array(\n numProjectChecks,\n );\n\n // Build requests only for clusters that don't have full access\n let idx = 0;\n for (const clusterIdx of clustersWithoutFullAccess) {\n const clusterName = allClusterNames[clusterIdx];\n const clusterId = allClusterIds[clusterIdx];\n const clusterIdentifier =\n permissionType === 'cost' ? clusterName : clusterId;\n\n for (let j = 0; j < allProjects.length; j++) {\n const projectName = allProjects[j];\n const perm = getClusterProjectPermission(clusterName, projectName);\n\n projectPermissionRequests[idx] = {\n permission: perm,\n };\n\n projectPermissionMap[idx] = {\n cluster: clusterIdentifier,\n project: projectName,\n };\n\n idx++;\n }\n }\n\n // Batch check project-level permissions\n const projectDecisions = await permissionsSvc.authorize(\n projectPermissionRequests,\n { credentials },\n );\n\n // Process project-level results\n for (let i = 0; i < projectDecisions.length; i++) {\n const decision = projectDecisions[i].result;\n\n if (decision === AuthorizeResult.ALLOW) {\n const result = projectPermissionMap[i];\n authorizedClusterProjects.push(result);\n // Project-level permission also grants cluster access\n clustersGrantedViaProjects.add(result.cluster);\n }\n }\n }\n\n // Step 3: Combine clusters from both full access and project-level grants\n const authorizedClusterIds = [\n ...clustersWithFullAccess,\n ...clustersGrantedViaProjects,\n ];\n\n return {\n authorizedClusterIds,\n authorizedClusterProjects,\n };\n};\n"],"names":["AuthorizeResult","costClusterSpecificPermission","rosClusterSpecificPermission","costClusterProjectPermission","rosClusterProjectPermission"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"constant.cjs.js","sources":["../../src/util/constant.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport const DEFAULT_SSO_BASE_URL = 'https://sso.redhat.com';\n\n// Base URL without the /cost-management/v1 path since the client appends it\nexport const DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL =\n 'https://console.redhat.com/api';\n"],"names":[],"mappings":";;AAgBO,MAAM,oBAAuB,GAAA;AAG7B,MAAM,sCACX,GAAA;;;;;"}
1
+ {"version":3,"file":"constant.cjs.js","sources":["../../src/util/constant.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport const DEFAULT_SSO_BASE_URL = 'https://sso.redhat.com';\n\n// Base URL without the /cost-management/v1 path since the client appends it\nexport const DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL =\n 'https://console.redhat.com/api';\n"],"names":[],"mappings":";;AAgBO,MAAM,oBAAA,GAAuB;AAG7B,MAAM,sCAAA,GACX;;;;;"}
@@ -7,6 +7,13 @@ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'defau
7
7
 
8
8
  var assert__default = /*#__PURE__*/_interopDefaultCompat(assert);
9
9
 
10
+ class SsoAuthenticationError extends Error {
11
+ constructor(message, statusCode) {
12
+ super(message);
13
+ this.statusCode = statusCode;
14
+ this.name = "SsoAuthenticationError";
15
+ }
16
+ }
10
17
  const TOKEN_CACHE_KEY = "sso_access_token";
11
18
  const getTokenFromApi = async (options) => {
12
19
  const { logger, config, cache } = options;
@@ -67,10 +74,19 @@ const getTokenFromApi = async (options) => {
67
74
  );
68
75
  logger.info(`Token cached, expires in ${expires_in} seconds`);
69
76
  } else {
70
- throw new Error(rhSsoResponse.statusText);
77
+ let detail = "";
78
+ try {
79
+ const body = await rhSsoResponse.json();
80
+ detail = body.error_description || body.error || "";
81
+ } catch {
82
+ }
83
+ const message = detail ? `SSO authentication failed: ${detail}` : `SSO authentication failed with status ${rhSsoResponse.status} (${rhSsoResponse.statusText})`;
84
+ logger.error(message);
85
+ throw new SsoAuthenticationError(message, rhSsoResponse.status);
71
86
  }
72
87
  return accessToken;
73
88
  };
74
89
 
90
+ exports.SsoAuthenticationError = SsoAuthenticationError;
75
91
  exports.getTokenFromApi = getTokenFromApi;
76
92
  //# sourceMappingURL=tokenUtil.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tokenUtil.cjs.js","sources":["../../src/util/tokenUtil.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport assert from 'assert';\nimport { RouterOptions } from '../models/RouterOptions';\nimport { DEFAULT_SSO_BASE_URL } from './constant';\n\n// Cache key for token storage\nconst TOKEN_CACHE_KEY = 'sso_access_token';\n\nexport const getTokenFromApi = async (options: RouterOptions) => {\n const { logger, config, cache } = options;\n\n const now = Date.now();\n\n // Try to get cached token from cache service\n const cachedToken = (await cache.get(TOKEN_CACHE_KEY)) as\n | { token: string; expiresAt: number }\n | undefined;\n\n // Debug logging\n if (cachedToken) {\n const timeUntilExpiry = cachedToken.expiresAt - now;\n const timeUntilExpirySeconds = Math.floor(timeUntilExpiry / 1000);\n logger.info(\n `Cache check: Token expires in ${timeUntilExpirySeconds}s, needs >60s to be valid`,\n );\n } else {\n logger.info('Cache check: No cached token exists');\n }\n\n // Return cached token if still valid (with 60s buffer)\n if (cachedToken && cachedToken.expiresAt > now + 60000) {\n logger.info('Using cached access token');\n return cachedToken.token;\n }\n\n let accessToken = '';\n\n assert(typeof config !== 'undefined', 'Config is undefined');\n\n logger.info('Requesting new access token');\n\n const ssoBaseUrl =\n config.getOptionalString('costManagement.ssoBaseUrl') ??\n DEFAULT_SSO_BASE_URL;\n const params = {\n tokenUrl: `${ssoBaseUrl}/auth/realms/redhat-external/protocol/openid-connect/token`,\n clientId: config.getString('costManagement.clientId'),\n clientSecret: config.getString('costManagement.clientSecret'),\n scope: 'api.console',\n grantType: 'client_credentials',\n } as const;\n\n const rhSsoResponse = await fetch(params.tokenUrl, {\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams(\n Object.entries({\n client_id: params.clientId,\n client_secret: params.clientSecret,\n scope: params.scope,\n grant_type: params.grantType,\n }).map(([k, v]) => [encodeURIComponent(k), encodeURIComponent(v)]),\n ),\n method: 'POST',\n });\n\n if (rhSsoResponse.ok) {\n const { access_token, expires_in } = await rhSsoResponse.json();\n accessToken = access_token;\n\n const expiresAt = Date.now() + expires_in * 1000;\n\n // Cache token with expiry using cache service\n await cache.set(\n TOKEN_CACHE_KEY,\n {\n token: accessToken,\n expiresAt,\n },\n {\n ttl: expires_in * 1000, // TTL in milliseconds\n },\n );\n\n logger.info(`Token cached, expires in ${expires_in} seconds`);\n } else {\n throw new Error(rhSsoResponse.statusText);\n }\n\n return accessToken;\n};\n"],"names":["assert","DEFAULT_SSO_BASE_URL"],"mappings":"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"}
1
+ {"version":3,"file":"tokenUtil.cjs.js","sources":["../../src/util/tokenUtil.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport assert from 'assert';\nimport { RouterOptions } from '../models/RouterOptions';\nimport { DEFAULT_SSO_BASE_URL } from './constant';\n\nexport class SsoAuthenticationError extends Error {\n constructor(message: string, public readonly statusCode: number) {\n super(message);\n this.name = 'SsoAuthenticationError';\n }\n}\n\n// Cache key for token storage\nconst TOKEN_CACHE_KEY = 'sso_access_token';\n\nexport const getTokenFromApi = async (options: RouterOptions) => {\n const { logger, config, cache } = options;\n\n const now = Date.now();\n\n // Try to get cached token from cache service\n const cachedToken = (await cache.get(TOKEN_CACHE_KEY)) as\n | { token: string; expiresAt: number }\n | undefined;\n\n // Debug logging\n if (cachedToken) {\n const timeUntilExpiry = cachedToken.expiresAt - now;\n const timeUntilExpirySeconds = Math.floor(timeUntilExpiry / 1000);\n logger.info(\n `Cache check: Token expires in ${timeUntilExpirySeconds}s, needs >60s to be valid`,\n );\n } else {\n logger.info('Cache check: No cached token exists');\n }\n\n // Return cached token if still valid (with 60s buffer)\n if (cachedToken && cachedToken.expiresAt > now + 60000) {\n logger.info('Using cached access token');\n return cachedToken.token;\n }\n\n let accessToken = '';\n\n assert(typeof config !== 'undefined', 'Config is undefined');\n\n logger.info('Requesting new access token');\n\n const ssoBaseUrl =\n config.getOptionalString('costManagement.ssoBaseUrl') ??\n DEFAULT_SSO_BASE_URL;\n const params = {\n tokenUrl: `${ssoBaseUrl}/auth/realms/redhat-external/protocol/openid-connect/token`,\n clientId: config.getString('costManagement.clientId'),\n clientSecret: config.getString('costManagement.clientSecret'),\n scope: 'api.console',\n grantType: 'client_credentials',\n } as const;\n\n const rhSsoResponse = await fetch(params.tokenUrl, {\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams(\n Object.entries({\n client_id: params.clientId,\n client_secret: params.clientSecret,\n scope: params.scope,\n grant_type: params.grantType,\n }).map(([k, v]) => [encodeURIComponent(k), encodeURIComponent(v)]),\n ),\n method: 'POST',\n });\n\n if (rhSsoResponse.ok) {\n const { access_token, expires_in } = await rhSsoResponse.json();\n accessToken = access_token;\n\n const expiresAt = Date.now() + expires_in * 1000;\n\n // Cache token with expiry using cache service\n await cache.set(\n TOKEN_CACHE_KEY,\n {\n token: accessToken,\n expiresAt,\n },\n {\n ttl: expires_in * 1000, // TTL in milliseconds\n },\n );\n\n logger.info(`Token cached, expires in ${expires_in} seconds`);\n } else {\n let detail = '';\n try {\n const body = await rhSsoResponse.json();\n detail = body.error_description || body.error || '';\n } catch {\n // response may not be JSON\n }\n const message = detail\n ? `SSO authentication failed: ${detail}`\n : `SSO authentication failed with status ${rhSsoResponse.status} (${rhSsoResponse.statusText})`;\n logger.error(message);\n throw new SsoAuthenticationError(message, rhSsoResponse.status);\n }\n\n return accessToken;\n};\n"],"names":["assert","DEFAULT_SSO_BASE_URL"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@red-hat-developer-hub/plugin-cost-management-backend",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "backstage": {
5
5
  "pluginId": "cost-management",
6
6
  "pluginPackages": [
@@ -8,17 +8,20 @@
8
8
  "@red-hat-developer-hub/plugin-cost-management-backend",
9
9
  "@red-hat-developer-hub/plugin-cost-management-common"
10
10
  ],
11
- "role": "backend-plugin"
11
+ "role": "backend-plugin",
12
+ "features": {
13
+ ".": "@backstage/BackendFeature"
14
+ }
12
15
  },
13
16
  "configSchema": "config.d.ts",
14
17
  "dependencies": {
15
- "@backstage/backend-defaults": "^0.7.0",
16
- "@backstage/backend-dynamic-feature-service": "^0.5.3",
17
- "@backstage/backend-plugin-api": "^1.1.1",
18
- "@backstage/config": "^1.3.2",
19
- "@backstage/plugin-permission-common": "^0.8.4",
20
- "@backstage/plugin-permission-node": "^0.8.7",
21
- "@red-hat-developer-hub/plugin-cost-management-common": "^2.0.1",
18
+ "@backstage/backend-defaults": "^0.16.0",
19
+ "@backstage/backend-dynamic-feature-service": "^0.8.0",
20
+ "@backstage/backend-plugin-api": "^1.8.0",
21
+ "@backstage/config": "^1.3.6",
22
+ "@backstage/plugin-permission-common": "^0.9.7",
23
+ "@backstage/plugin-permission-node": "^0.10.11",
24
+ "@red-hat-developer-hub/plugin-cost-management-common": "^2.1.0",
22
25
  "@types/express": "4.17.25",
23
26
  "express": "^4.17.1",
24
27
  "express-promise-router": "^4.1.0",
@@ -28,14 +31,14 @@
28
31
  "yn": "^4.0.0"
29
32
  },
30
33
  "devDependencies": {
31
- "@backstage/backend-test-utils": "^1.2.1",
32
- "@backstage/cli": "^0.29.4",
33
- "@backstage/plugin-auth-backend": "^0.24.2",
34
- "@backstage/plugin-auth-backend-module-guest-provider": "^0.2.4",
35
- "@backstage/plugin-proxy-backend": "^0.5.10",
34
+ "@backstage/backend-test-utils": "^1.11.1",
35
+ "@backstage/cli": "^0.36.0",
36
+ "@backstage/plugin-auth-backend": "^0.27.3",
37
+ "@backstage/plugin-auth-backend-module-guest-provider": "^0.2.17",
38
+ "@backstage/plugin-proxy-backend": "^0.6.11",
36
39
  "@types/lodash": "4.17.24",
37
40
  "@types/supertest": "^6.0.0",
38
- "msw": "^1.0.0",
41
+ "msw": "^2.3.4",
39
42
  "supertest": "^7.0.0"
40
43
  },
41
44
  "exports": {
@@ -74,8 +77,8 @@
74
77
  "types": "./dist/index.d.ts",
75
78
  "typesVersions": {
76
79
  "*": {
77
- "index": [
78
- "dist/index.d.ts"
80
+ "package.json": [
81
+ "package.json"
79
82
  ]
80
83
  }
81
84
  }