@red-hat-developer-hub/plugin-cost-management-backend 2.2.0 → 2.2.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # @red-hat-developer-hub/plugin-cost-management-backend
2
2
 
3
+ ## 2.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - f32b8a8: fix: register dynamic RBAC permissions for cluster/project tiers (FLPATH-4207)
8
+
9
+ Cluster-specific permissions (ros/<cluster>, ros/<cluster>/<project>) were created
10
+ at runtime but never registered with createPermissionIntegrationRouter. The RHDH
11
+ RBAC backend only evaluates registered permissions — unregistered ones get DENY by
12
+ default, breaking the 3-tier RBAC model. Now fetches cluster/project data at router
13
+ init and registers all dynamic permissions. Also improves secureProxy.ts error
14
+ messages to include request path and error details.
15
+
16
+ - 558b7c3: fix: patch transitive dependency CVEs via yarn resolutions
17
+
18
+ Pins vulnerable transitive dependencies to patched versions to address open Dependabot alerts:
19
+
20
+ - 815580b: fix: additional CVE patches and dependency updates for 2.2.1
21
+
22
+ Covers the following changes merged after the initial CVE patch (558b7c3):
23
+
24
+ - chore(deps): update rhdh cost management dependencies (patch) (#3000) — bumps
25
+ `@aws-sdk/core/fast-xml-parser` to 4.5.6, `request/form-data` to 2.5.5,
26
+ `request/tough-cookie` to 4.1.4, `typeorm` to 0.3.29, and `file-type` to 21.3.4
27
+ via yarn resolutions
28
+
29
+ - fix: resolve lodash CVEs via workspace resolution (#3135) — pins lodash to 4.18.1
30
+ to address GHSA-r5fr-rjxr-66jc (Code Injection via _.template, CVSS 8.1) and
31
+ GHSA-f23m-r3pf-42rh (Prototype Pollution via _.unset/\_.omit, CVSS 6.5)
32
+
33
+ - fix: update lodash direct deps to 4.18.1 to close Dependabot alerts (#3142) —
34
+ updates pinned lodash versions in individual plugin package.json files so
35
+ Dependabot can detect the fix for GHSA-r5fr-rjxr-66jc and GHSA-f23m-r3pf-42rh
36
+
37
+ - fix: CVE patches for casbin/minimatch and fast-xml-parser (#3143) — adds
38
+ `casbin/minimatch` resolution to 7.4.8 and bumps `fast-xml-parser` to 5.7.3
39
+
40
+ - fix: upgrade @backstage-community/plugin-rbac-backend to ^7.12.4 (#3161) —
41
+ upgrades rbac-backend and rbac-common to address a Backstage backend CVE
42
+
43
+ - chore(deps): update linkifyjs to v4.3.3 (#3155) — patch version bump
44
+
45
+ - Updated dependencies [558b7c3]
46
+ - Updated dependencies [815580b]
47
+ - @red-hat-developer-hub/plugin-cost-management-common@2.2.1
48
+
3
49
  ## 2.2.0
4
50
 
5
51
  ### Minor Changes
@@ -256,13 +256,17 @@ const secureProxy = (options) => async (req, res) => {
256
256
  res.set("Content-Type", contentType);
257
257
  return res.send(await upstreamResponse.text());
258
258
  } catch (error) {
259
- options.logger.error("Secure proxy error", error);
259
+ const errorMessage = error instanceof Error ? error.message : String(error);
260
+ options.logger.error(
261
+ `Secure proxy error for path "${proxyPath}": ${errorMessage}`,
262
+ error instanceof Error ? { stack: error.stack } : {}
263
+ );
260
264
  if (error instanceof tokenUtil.SsoAuthenticationError) {
261
265
  return res.status(502).json({
262
266
  error: "Unable to authenticate with the hybrid cloud console. Please check your service account credentials (clientId/clientSecret) in the RHDH Cost Management configuration."
263
267
  });
264
268
  }
265
- return res.status(500).json({ error: "Internal proxy error" });
269
+ return res.status(500).json({ error: `Internal proxy error: ${errorMessage}` });
266
270
  }
267
271
  };
268
272
 
@@ -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, 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
+ {"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 const errorMessage =\n error instanceof Error ? error.message : String(error);\n options.logger.error(\n `Secure proxy error for path \"${proxyPath}\": ${errorMessage}`,\n error instanceof Error ? { stack: error.stack } : {},\n );\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\n .status(500)\n .json({ error: `Internal proxy error: ${errorMessage}` });\n }\n };\n"],"names":["authorize","AuthorizeResult","filterAuthorizedClustersAndProjects","rosPluginPermissions","getTokenFromApi","costPluginPermissions","resolveActor","emitAuditLog","DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL","SsoAuthenticationError"],"mappings":"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"}
@@ -8,19 +8,91 @@ var access = require('../routes/access.cjs.js');
8
8
  var costManagementAccess = require('../routes/costManagementAccess.cjs.js');
9
9
  var secureProxy = require('../routes/secureProxy.cjs.js');
10
10
  var applyRecommendation = require('../routes/applyRecommendation.cjs.js');
11
+ var tokenUtil = require('../util/tokenUtil.cjs.js');
11
12
 
12
13
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
13
14
 
14
15
  var express__default = /*#__PURE__*/_interopDefaultCompat(express);
15
16
  var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
16
17
 
18
+ function extractStrings(result, accessor) {
19
+ const values = /* @__PURE__ */ new Set();
20
+ if (result.status !== "fulfilled" || !result.value?.data) {
21
+ return values;
22
+ }
23
+ for (const item of result.value.data) {
24
+ const v = accessor(item);
25
+ if (v) values.add(v);
26
+ }
27
+ return values;
28
+ }
29
+ function buildClusterProjectPermissions(clusters, projects, clusterFn, projectFn) {
30
+ const perms = [];
31
+ for (const cluster of clusters) {
32
+ perms.push(clusterFn(cluster));
33
+ for (const project of projects) {
34
+ perms.push(projectFn(cluster, project));
35
+ }
36
+ }
37
+ return perms;
38
+ }
39
+ async function fetchDynamicPermissions(options) {
40
+ const { logger } = options;
41
+ try {
42
+ const token = await tokenUtil.getTokenFromApi(options);
43
+ const [rosData, costClusters, costProjects] = await Promise.allSettled([
44
+ options.optimizationApi.getRecommendationList(
45
+ { query: { limit: -1, orderHow: "desc", orderBy: "last_reported" } },
46
+ { token }
47
+ ).then((r) => r.json()),
48
+ options.costManagementApi.searchOpenShiftClusters("", { token, limit: 1e3 }).then((r) => r.json()),
49
+ options.costManagementApi.searchOpenShiftProjects("", { token, limit: 1e3 }).then((r) => r.json())
50
+ ]);
51
+ const rosClusterNames = extractStrings(rosData, (r) => r.clusterAlias);
52
+ const rosProjectNames = extractStrings(rosData, (r) => r.project);
53
+ const costClusterNames = extractStrings(
54
+ costClusters,
55
+ (c) => c.cluster_alias
56
+ );
57
+ const costProjectNames = extractStrings(
58
+ costProjects,
59
+ (p) => p.value
60
+ );
61
+ const permissions$1 = [
62
+ ...buildClusterProjectPermissions(
63
+ rosClusterNames,
64
+ rosProjectNames,
65
+ permissions.rosClusterSpecificPermission,
66
+ permissions.rosClusterProjectPermission
67
+ ),
68
+ ...buildClusterProjectPermissions(
69
+ costClusterNames,
70
+ costProjectNames,
71
+ permissions.costClusterSpecificPermission,
72
+ permissions.costClusterProjectPermission
73
+ )
74
+ ];
75
+ logger.info(
76
+ `Registered ${permissions$1.length} dynamic RBAC permissions (${rosClusterNames.size} ROS clusters, ${costClusterNames.size} cost clusters)`
77
+ );
78
+ return permissions$1;
79
+ } catch (error) {
80
+ logger.warn(
81
+ "Could not fetch cluster/project data for dynamic permission registration. Cluster-specific RBAC permissions will not be evaluated until the next refresh.",
82
+ error instanceof Error ? { error: error.message } : {}
83
+ );
84
+ return [];
85
+ }
86
+ }
17
87
  async function createRouter(options) {
18
88
  const router = Router__default.default();
89
+ const dynamicPermissions = await fetchDynamicPermissions(options);
19
90
  const permissionsIntegrationRouter = pluginPermissionNode.createPermissionIntegrationRouter({
20
91
  permissions: [
21
92
  ...permissions.rosPluginPermissions,
22
93
  ...permissions.rosApplyPermissions,
23
- ...permissions.costPluginPermissions
94
+ ...permissions.costPluginPermissions,
95
+ ...dynamicPermissions
24
96
  ]
25
97
  });
26
98
  router.use(express__default.default.json());
@@ -35,5 +107,7 @@ async function createRouter(options) {
35
107
  return router;
36
108
  }
37
109
 
110
+ exports.buildClusterProjectPermissions = buildClusterProjectPermissions;
38
111
  exports.createRouter = createRouter;
112
+ exports.extractStrings = extractStrings;
39
113
  //# sourceMappingURL=router.cjs.js.map
@@ -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,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
+ {"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 type { BasicPermission } from '@backstage/plugin-permission-common';\nimport {\n rosPluginPermissions,\n rosApplyPermissions,\n costPluginPermissions,\n rosClusterSpecificPermission,\n rosClusterProjectPermission,\n costClusterSpecificPermission,\n costClusterProjectPermission,\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';\nimport { getTokenFromApi } from '../util/tokenUtil';\n\n/** @internal Visible for testing */\nexport function extractStrings<T>(\n result: PromiseSettledResult<{ data?: T[] }>,\n accessor: (item: T) => string | undefined,\n): Set<string> {\n const values = new Set<string>();\n if (result.status !== 'fulfilled' || !result.value?.data) {\n return values;\n }\n for (const item of result.value.data) {\n const v = accessor(item);\n if (v) values.add(v);\n }\n return values;\n}\n\n/** @internal Visible for testing */\nexport function buildClusterProjectPermissions(\n clusters: Set<string>,\n projects: Set<string>,\n clusterFn: (cluster: string) => BasicPermission,\n projectFn: (cluster: string, project: string) => BasicPermission,\n): BasicPermission[] {\n const perms: BasicPermission[] = [];\n for (const cluster of clusters) {\n perms.push(clusterFn(cluster));\n for (const project of projects) {\n perms.push(projectFn(cluster, project));\n }\n }\n return perms;\n}\n\n/**\n * Fetches cluster and project data from the upstream APIs and builds\n * permission objects for every ros/{cluster}, ros/{cluster}/{project},\n * cost/{cluster}, and cost/{cluster}/{project} combination.\n *\n * These permissions must be registered with the permission integration\n * router so that the RBAC backend recognises them as valid and evaluates\n * the corresponding policies instead of returning DENY by default.\n */\nasync function fetchDynamicPermissions(\n options: RouterOptions,\n): Promise<BasicPermission[]> {\n const { logger } = options;\n\n try {\n const token = await getTokenFromApi(options);\n\n // Fetch all known clusters and projects so we can register a permission\n // for each one. limit:1000 is a high ceiling to capture all entries in a\n // single request; the Cost Management API defaults to 10.\n // This data is fetched once at plugin startup and determines which\n // ros/<cluster> and cost/<cluster> permissions the RBAC backend will\n // evaluate. Clusters or projects added after startup will not be\n // recognised until the Backstage instance is restarted.\n const [rosData, costClusters, costProjects] = await Promise.allSettled([\n options.optimizationApi\n .getRecommendationList(\n { query: { limit: -1, orderHow: 'desc', orderBy: 'last_reported' } },\n { token },\n )\n .then(r => r.json()),\n options.costManagementApi\n .searchOpenShiftClusters('', { token, limit: 1000 })\n .then(r => r.json()),\n options.costManagementApi\n .searchOpenShiftProjects('', { token, limit: 1000 })\n .then(r => r.json()),\n ]);\n\n const rosClusterNames = extractStrings(rosData, r => r.clusterAlias);\n const rosProjectNames = extractStrings(rosData, r => r.project);\n const costClusterNames = extractStrings(\n costClusters,\n (c: { cluster_alias: string }) => c.cluster_alias,\n );\n const costProjectNames = extractStrings(\n costProjects,\n (p: { value: string }) => p.value,\n );\n\n const permissions = [\n ...buildClusterProjectPermissions(\n rosClusterNames,\n rosProjectNames,\n rosClusterSpecificPermission,\n rosClusterProjectPermission,\n ),\n ...buildClusterProjectPermissions(\n costClusterNames,\n costProjectNames,\n costClusterSpecificPermission,\n costClusterProjectPermission,\n ),\n ];\n\n logger.info(\n `Registered ${permissions.length} dynamic RBAC permissions ` +\n `(${rosClusterNames.size} ROS clusters, ${costClusterNames.size} cost clusters)`,\n );\n\n return permissions;\n } catch (error) {\n logger.warn(\n 'Could not fetch cluster/project data for dynamic permission registration. ' +\n 'Cluster-specific RBAC permissions will not be evaluated until the next refresh.',\n error instanceof Error ? { error: error.message } : {},\n );\n return [];\n }\n}\n\n/** @public */\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const router = Router();\n\n const dynamicPermissions = await fetchDynamicPermissions(options);\n\n const permissionsIntegrationRouter = createPermissionIntegrationRouter({\n permissions: [\n ...rosPluginPermissions,\n ...rosApplyPermissions,\n ...costPluginPermissions,\n ...dynamicPermissions,\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":["getTokenFromApi","permissions","rosClusterSpecificPermission","rosClusterProjectPermission","costClusterSpecificPermission","costClusterProjectPermission","Router","createPermissionIntegrationRouter","rosPluginPermissions","rosApplyPermissions","costPluginPermissions","express","getAccess","getCostManagementAccess","applyRecommendation","secureProxy"],"mappings":";;;;;;;;;;;;;;;;;AAqCO,SAAS,cAAA,CACd,QACA,QAAA,EACa;AACb,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAY;AAC/B,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,CAAC,MAAA,CAAO,OAAO,IAAA,EAAM;AACxD,IAAA,OAAO,MAAA;AAAA,EACT;AACA,EAAA,KAAA,MAAW,IAAA,IAAQ,MAAA,CAAO,KAAA,CAAM,IAAA,EAAM;AACpC,IAAA,MAAM,CAAA,GAAI,SAAS,IAAI,CAAA;AACvB,IAAA,IAAI,CAAA,EAAG,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA;AAAA,EACrB;AACA,EAAA,OAAO,MAAA;AACT;AAGO,SAAS,8BAAA,CACd,QAAA,EACA,QAAA,EACA,SAAA,EACA,SAAA,EACmB;AACnB,EAAA,MAAM,QAA2B,EAAC;AAClC,EAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,IAAA,KAAA,CAAM,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AAC7B,IAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,MAAA,KAAA,CAAM,IAAA,CAAK,SAAA,CAAU,OAAA,EAAS,OAAO,CAAC,CAAA;AAAA,IACxC;AAAA,EACF;AACA,EAAA,OAAO,KAAA;AACT;AAWA,eAAe,wBACb,OAAA,EAC4B;AAC5B,EAAA,MAAM,EAAE,QAAO,GAAI,OAAA;AAEnB,EAAA,IAAI;AACF,IAAA,MAAM,KAAA,GAAQ,MAAMA,yBAAA,CAAgB,OAAO,CAAA;AAS3C,IAAA,MAAM,CAAC,OAAA,EAAS,YAAA,EAAc,YAAY,CAAA,GAAI,MAAM,QAAQ,UAAA,CAAW;AAAA,MACrE,QAAQ,eAAA,CACL,qBAAA;AAAA,QACC,EAAE,OAAO,EAAE,KAAA,EAAO,IAAI,QAAA,EAAU,MAAA,EAAQ,OAAA,EAAS,eAAA,EAAgB,EAAE;AAAA,QACnE,EAAE,KAAA;AAAM,OACV,CACC,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,CAAE,MAAM,CAAA;AAAA,MACrB,OAAA,CAAQ,iBAAA,CACL,uBAAA,CAAwB,EAAA,EAAI,EAAE,KAAA,EAAO,KAAA,EAAO,GAAA,EAAM,CAAA,CAClD,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,CAAE,MAAM,CAAA;AAAA,MACrB,OAAA,CAAQ,iBAAA,CACL,uBAAA,CAAwB,EAAA,EAAI,EAAE,KAAA,EAAO,KAAA,EAAO,GAAA,EAAM,CAAA,CAClD,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,CAAE,MAAM;AAAA,KACtB,CAAA;AAED,IAAA,MAAM,eAAA,GAAkB,cAAA,CAAe,OAAA,EAAS,CAAA,CAAA,KAAK,EAAE,YAAY,CAAA;AACnE,IAAA,MAAM,eAAA,GAAkB,cAAA,CAAe,OAAA,EAAS,CAAA,CAAA,KAAK,EAAE,OAAO,CAAA;AAC9D,IAAA,MAAM,gBAAA,GAAmB,cAAA;AAAA,MACvB,YAAA;AAAA,MACA,CAAC,MAAiC,CAAA,CAAE;AAAA,KACtC;AACA,IAAA,MAAM,gBAAA,GAAmB,cAAA;AAAA,MACvB,YAAA;AAAA,MACA,CAAC,MAAyB,CAAA,CAAE;AAAA,KAC9B;AAEA,IAAA,MAAMC,aAAA,GAAc;AAAA,MAClB,GAAG,8BAAA;AAAA,QACD,eAAA;AAAA,QACA,eAAA;AAAA,QACAC,wCAAA;AAAA,QACAC;AAAA,OACF;AAAA,MACA,GAAG,8BAAA;AAAA,QACD,gBAAA;AAAA,QACA,gBAAA;AAAA,QACAC,yCAAA;AAAA,QACAC;AAAA;AACF,KACF;AAEA,IAAA,MAAA,CAAO,IAAA;AAAA,MACL,CAAA,WAAA,EAAcJ,cAAY,MAAM,CAAA,2BAAA,EAC1B,gBAAgB,IAAI,CAAA,eAAA,EAAkB,iBAAiB,IAAI,CAAA,eAAA;AAAA,KACnE;AAEA,IAAA,OAAOA,aAAA;AAAA,EACT,SAAS,KAAA,EAAO;AACd,IAAA,MAAA,CAAO,IAAA;AAAA,MACL,2JAAA;AAAA,MAEA,iBAAiB,KAAA,GAAQ,EAAE,OAAO,KAAA,CAAM,OAAA,KAAY;AAAC,KACvD;AACA,IAAA,OAAO,EAAC;AAAA,EACV;AACF;AAGA,eAAsB,aACpB,OAAA,EACyB;AACzB,EAAA,MAAM,SAASK,uBAAA,EAAO;AAEtB,EAAA,MAAM,kBAAA,GAAqB,MAAM,uBAAA,CAAwB,OAAO,CAAA;AAEhE,EAAA,MAAM,+BAA+BC,sDAAA,CAAkC;AAAA,IACrE,WAAA,EAAa;AAAA,MACX,GAAGC,gCAAA;AAAA,MACH,GAAGC,+BAAA;AAAA,MACH,GAAGC,iCAAA;AAAA,MACH,GAAG;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;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@red-hat-developer-hub/plugin-cost-management-backend",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "backstage": {
5
5
  "pluginId": "cost-management",
6
6
  "pluginPackages": [
@@ -21,11 +21,11 @@
21
21
  "@backstage/config": "^1.3.6",
22
22
  "@backstage/plugin-permission-common": "^0.9.7",
23
23
  "@backstage/plugin-permission-node": "^0.10.11",
24
- "@red-hat-developer-hub/plugin-cost-management-common": "^2.2.0",
24
+ "@red-hat-developer-hub/plugin-cost-management-common": "^2.2.1",
25
25
  "@types/express": "4.17.25",
26
26
  "express": "^4.17.1",
27
27
  "express-promise-router": "^4.1.0",
28
- "lodash": "4.17.23",
28
+ "lodash": "4.18.1",
29
29
  "node-fetch": "^2.6.7",
30
30
  "winston": "^3.2.1",
31
31
  "yn": "^4.0.0"