@red-hat-developer-hub/plugin-cost-management-backend 2.0.2
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 +86 -0
- package/README.md +53 -0
- package/app-config.dynamic.yaml +3 -0
- package/config.d.ts +41 -0
- package/dist/index.cjs.js +10 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/plugin.cjs.js +77 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/routes/access.cjs.js +153 -0
- package/dist/routes/access.cjs.js.map +1 -0
- package/dist/routes/applyRecommendation.cjs.js +150 -0
- package/dist/routes/applyRecommendation.cjs.js.map +1 -0
- package/dist/routes/costManagementAccess.cjs.js +130 -0
- package/dist/routes/costManagementAccess.cjs.js.map +1 -0
- package/dist/routes/secureProxy.cjs.js +265 -0
- package/dist/routes/secureProxy.cjs.js.map +1 -0
- package/dist/service/costManagementService.cjs.js +30 -0
- package/dist/service/costManagementService.cjs.js.map +1 -0
- package/dist/service/optimizationsService.cjs.js +28 -0
- package/dist/service/optimizationsService.cjs.js.map +1 -0
- package/dist/service/router.cjs.js +39 -0
- package/dist/service/router.cjs.js.map +1 -0
- package/dist/util/auditLog.cjs.js +19 -0
- package/dist/util/auditLog.cjs.js.map +1 -0
- package/dist/util/checkPermissions.cjs.js +108 -0
- package/dist/util/checkPermissions.cjs.js.map +1 -0
- package/dist/util/constant.cjs.js +8 -0
- package/dist/util/constant.cjs.js.map +1 -0
- package/dist/util/tokenUtil.cjs.js +76 -0
- package/dist/util/tokenUtil.cjs.js.map +1 -0
- package/package.json +82 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var pluginPermissionCommon = require('@backstage/plugin-permission-common');
|
|
4
|
+
var permissions = require('@red-hat-developer-hub/plugin-cost-management-common/permissions');
|
|
5
|
+
|
|
6
|
+
const authorize = async (request, anyOfPermissions, permissionsSvc, httpAuth) => {
|
|
7
|
+
const credentials = await httpAuth.credentials(request);
|
|
8
|
+
const permissionRequests = anyOfPermissions.map((permission) => ({
|
|
9
|
+
permission
|
|
10
|
+
}));
|
|
11
|
+
const decisions = await permissionsSvc.authorize(permissionRequests, {
|
|
12
|
+
credentials
|
|
13
|
+
});
|
|
14
|
+
const allow = decisions.find((d) => d.result === pluginPermissionCommon.AuthorizeResult.ALLOW);
|
|
15
|
+
return allow || {
|
|
16
|
+
result: pluginPermissionCommon.AuthorizeResult.DENY
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
const filterAuthorizedClustersAndProjects = async (request, permissionsSvc, httpAuth, clusterDataMap, allProjects, permissionType = "ros") => {
|
|
20
|
+
const credentials = await httpAuth.credentials(request);
|
|
21
|
+
const allClusterNames = Object.keys(clusterDataMap);
|
|
22
|
+
const allClusterIds = Object.values(clusterDataMap);
|
|
23
|
+
if (allClusterNames.length === 0) {
|
|
24
|
+
return {
|
|
25
|
+
authorizedClusterIds: [],
|
|
26
|
+
authorizedClusterProjects: []
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const getClusterPermission = permissionType === "cost" ? permissions.costClusterSpecificPermission : permissions.rosClusterSpecificPermission;
|
|
30
|
+
const getClusterProjectPermission = permissionType === "cost" ? permissions.costClusterProjectPermission : permissions.rosClusterProjectPermission;
|
|
31
|
+
const numClusters = allClusterNames.length;
|
|
32
|
+
const clusterPermissionRequests = allClusterNames.map((clusterName) => {
|
|
33
|
+
const perm = getClusterPermission(clusterName);
|
|
34
|
+
return { permission: perm };
|
|
35
|
+
});
|
|
36
|
+
const clusterDecisions = await permissionsSvc.authorize(
|
|
37
|
+
clusterPermissionRequests,
|
|
38
|
+
{
|
|
39
|
+
credentials
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
const clustersWithFullAccess = /* @__PURE__ */ new Set();
|
|
43
|
+
const clustersWithoutFullAccess = [];
|
|
44
|
+
for (let i = 0; i < numClusters; i++) {
|
|
45
|
+
const clusterName = allClusterNames[i];
|
|
46
|
+
const clusterId = allClusterIds[i];
|
|
47
|
+
const clusterIdentifier = permissionType === "cost" ? clusterName : clusterId;
|
|
48
|
+
const decision = clusterDecisions[i].result;
|
|
49
|
+
if (decision === pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
50
|
+
clustersWithFullAccess.add(clusterIdentifier);
|
|
51
|
+
} else {
|
|
52
|
+
clustersWithoutFullAccess.push(i);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const authorizedClusterProjects = [];
|
|
56
|
+
const clustersGrantedViaProjects = /* @__PURE__ */ new Set();
|
|
57
|
+
if (clustersWithoutFullAccess.length > 0 && allProjects.length > 0) {
|
|
58
|
+
const numProjectChecks = clustersWithoutFullAccess.length * allProjects.length;
|
|
59
|
+
const projectPermissionRequests = new Array(
|
|
60
|
+
numProjectChecks
|
|
61
|
+
);
|
|
62
|
+
const projectPermissionMap = new Array(
|
|
63
|
+
numProjectChecks
|
|
64
|
+
);
|
|
65
|
+
let idx = 0;
|
|
66
|
+
for (const clusterIdx of clustersWithoutFullAccess) {
|
|
67
|
+
const clusterName = allClusterNames[clusterIdx];
|
|
68
|
+
const clusterId = allClusterIds[clusterIdx];
|
|
69
|
+
const clusterIdentifier = permissionType === "cost" ? clusterName : clusterId;
|
|
70
|
+
for (let j = 0; j < allProjects.length; j++) {
|
|
71
|
+
const projectName = allProjects[j];
|
|
72
|
+
const perm = getClusterProjectPermission(clusterName, projectName);
|
|
73
|
+
projectPermissionRequests[idx] = {
|
|
74
|
+
permission: perm
|
|
75
|
+
};
|
|
76
|
+
projectPermissionMap[idx] = {
|
|
77
|
+
cluster: clusterIdentifier,
|
|
78
|
+
project: projectName
|
|
79
|
+
};
|
|
80
|
+
idx++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const projectDecisions = await permissionsSvc.authorize(
|
|
84
|
+
projectPermissionRequests,
|
|
85
|
+
{ credentials }
|
|
86
|
+
);
|
|
87
|
+
for (let i = 0; i < projectDecisions.length; i++) {
|
|
88
|
+
const decision = projectDecisions[i].result;
|
|
89
|
+
if (decision === pluginPermissionCommon.AuthorizeResult.ALLOW) {
|
|
90
|
+
const result = projectPermissionMap[i];
|
|
91
|
+
authorizedClusterProjects.push(result);
|
|
92
|
+
clustersGrantedViaProjects.add(result.cluster);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const authorizedClusterIds = [
|
|
97
|
+
...clustersWithFullAccess,
|
|
98
|
+
...clustersGrantedViaProjects
|
|
99
|
+
];
|
|
100
|
+
return {
|
|
101
|
+
authorizedClusterIds,
|
|
102
|
+
authorizedClusterProjects
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
exports.authorize = authorize;
|
|
107
|
+
exports.filterAuthorizedClustersAndProjects = filterAuthorizedClustersAndProjects;
|
|
108
|
+
//# sourceMappingURL=checkPermissions.cjs.js.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SSO_BASE_URL = "https://sso.redhat.com";
|
|
4
|
+
const DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL = "https://console.redhat.com/api";
|
|
5
|
+
|
|
6
|
+
exports.DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL = DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL;
|
|
7
|
+
exports.DEFAULT_SSO_BASE_URL = DEFAULT_SSO_BASE_URL;
|
|
8
|
+
//# sourceMappingURL=constant.cjs.js.map
|
|
@@ -0,0 +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;;;;;"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var assert = require('assert');
|
|
4
|
+
var constant = require('./constant.cjs.js');
|
|
5
|
+
|
|
6
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var assert__default = /*#__PURE__*/_interopDefaultCompat(assert);
|
|
9
|
+
|
|
10
|
+
const TOKEN_CACHE_KEY = "sso_access_token";
|
|
11
|
+
const getTokenFromApi = async (options) => {
|
|
12
|
+
const { logger, config, cache } = options;
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
const cachedToken = await cache.get(TOKEN_CACHE_KEY);
|
|
15
|
+
if (cachedToken) {
|
|
16
|
+
const timeUntilExpiry = cachedToken.expiresAt - now;
|
|
17
|
+
const timeUntilExpirySeconds = Math.floor(timeUntilExpiry / 1e3);
|
|
18
|
+
logger.info(
|
|
19
|
+
`Cache check: Token expires in ${timeUntilExpirySeconds}s, needs >60s to be valid`
|
|
20
|
+
);
|
|
21
|
+
} else {
|
|
22
|
+
logger.info("Cache check: No cached token exists");
|
|
23
|
+
}
|
|
24
|
+
if (cachedToken && cachedToken.expiresAt > now + 6e4) {
|
|
25
|
+
logger.info("Using cached access token");
|
|
26
|
+
return cachedToken.token;
|
|
27
|
+
}
|
|
28
|
+
let accessToken = "";
|
|
29
|
+
assert__default.default(typeof config !== "undefined", "Config is undefined");
|
|
30
|
+
logger.info("Requesting new access token");
|
|
31
|
+
const ssoBaseUrl = config.getOptionalString("costManagement.ssoBaseUrl") ?? constant.DEFAULT_SSO_BASE_URL;
|
|
32
|
+
const params = {
|
|
33
|
+
tokenUrl: `${ssoBaseUrl}/auth/realms/redhat-external/protocol/openid-connect/token`,
|
|
34
|
+
clientId: config.getString("costManagement.clientId"),
|
|
35
|
+
clientSecret: config.getString("costManagement.clientSecret"),
|
|
36
|
+
scope: "api.console",
|
|
37
|
+
grantType: "client_credentials"
|
|
38
|
+
};
|
|
39
|
+
const rhSsoResponse = await fetch(params.tokenUrl, {
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
42
|
+
},
|
|
43
|
+
body: new URLSearchParams(
|
|
44
|
+
Object.entries({
|
|
45
|
+
client_id: params.clientId,
|
|
46
|
+
client_secret: params.clientSecret,
|
|
47
|
+
scope: params.scope,
|
|
48
|
+
grant_type: params.grantType
|
|
49
|
+
}).map(([k, v]) => [encodeURIComponent(k), encodeURIComponent(v)])
|
|
50
|
+
),
|
|
51
|
+
method: "POST"
|
|
52
|
+
});
|
|
53
|
+
if (rhSsoResponse.ok) {
|
|
54
|
+
const { access_token, expires_in } = await rhSsoResponse.json();
|
|
55
|
+
accessToken = access_token;
|
|
56
|
+
const expiresAt = Date.now() + expires_in * 1e3;
|
|
57
|
+
await cache.set(
|
|
58
|
+
TOKEN_CACHE_KEY,
|
|
59
|
+
{
|
|
60
|
+
token: accessToken,
|
|
61
|
+
expiresAt
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
ttl: expires_in * 1e3
|
|
65
|
+
// TTL in milliseconds
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
logger.info(`Token cached, expires in ${expires_in} seconds`);
|
|
69
|
+
} else {
|
|
70
|
+
throw new Error(rhSsoResponse.statusText);
|
|
71
|
+
}
|
|
72
|
+
return accessToken;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
exports.getTokenFromApi = getTokenFromApi;
|
|
76
|
+
//# sourceMappingURL=tokenUtil.cjs.js.map
|
|
@@ -0,0 +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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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@red-hat-developer-hub/plugin-cost-management-backend",
|
|
3
|
+
"version": "2.0.2",
|
|
4
|
+
"backstage": {
|
|
5
|
+
"pluginId": "cost-management",
|
|
6
|
+
"pluginPackages": [
|
|
7
|
+
"@red-hat-developer-hub/plugin-cost-management",
|
|
8
|
+
"@red-hat-developer-hub/plugin-cost-management-backend",
|
|
9
|
+
"@red-hat-developer-hub/plugin-cost-management-common"
|
|
10
|
+
],
|
|
11
|
+
"role": "backend-plugin"
|
|
12
|
+
},
|
|
13
|
+
"configSchema": "config.d.ts",
|
|
14
|
+
"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",
|
|
22
|
+
"@types/express": "4.17.25",
|
|
23
|
+
"express": "^4.17.1",
|
|
24
|
+
"express-promise-router": "^4.1.0",
|
|
25
|
+
"lodash": "4.17.23",
|
|
26
|
+
"node-fetch": "^2.6.7",
|
|
27
|
+
"winston": "^3.2.1",
|
|
28
|
+
"yn": "^4.0.0"
|
|
29
|
+
},
|
|
30
|
+
"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",
|
|
36
|
+
"@types/lodash": "4.17.24",
|
|
37
|
+
"@types/supertest": "^6.0.0",
|
|
38
|
+
"msw": "^1.0.0",
|
|
39
|
+
"supertest": "^7.0.0"
|
|
40
|
+
},
|
|
41
|
+
"exports": {
|
|
42
|
+
".": {
|
|
43
|
+
"backstage": "@backstage/BackendFeature",
|
|
44
|
+
"require": "./dist/index.cjs.js",
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"default": "./dist/index.cjs.js"
|
|
47
|
+
},
|
|
48
|
+
"./package.json": "./package.json"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"app-config.dynamic.yaml",
|
|
52
|
+
"dist",
|
|
53
|
+
"config.d.ts"
|
|
54
|
+
],
|
|
55
|
+
"license": "Apache-2.0",
|
|
56
|
+
"main": "./dist/index.cjs.js",
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"repository": {
|
|
61
|
+
"directory": "workspaces/cost-management/plugins/cost-management-backend",
|
|
62
|
+
"type": "git",
|
|
63
|
+
"url": "https://github.com/redhat-developer/rhdh-plugins"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "backstage-cli package build",
|
|
67
|
+
"clean": "backstage-cli package clean",
|
|
68
|
+
"lint": "backstage-cli package lint",
|
|
69
|
+
"postpack": "backstage-cli package postpack",
|
|
70
|
+
"prepack": "backstage-cli package prepack",
|
|
71
|
+
"start": "backstage-cli package start",
|
|
72
|
+
"test": "backstage-cli package test"
|
|
73
|
+
},
|
|
74
|
+
"types": "./dist/index.d.ts",
|
|
75
|
+
"typesVersions": {
|
|
76
|
+
"*": {
|
|
77
|
+
"index": [
|
|
78
|
+
"dist/index.d.ts"
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|