@backstage/plugin-kubernetes-backend 0.21.7-next.0 → 0.21.7-next.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 +8 -0
- package/dist/index.d.ts +24 -2
- package/dist/package.json.cjs.js +1 -1
- package/dist/plugin.cjs.js +5 -2
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/routes/resourcesRoutes.cjs.js +75 -39
- package/dist/routes/resourcesRoutes.cjs.js.map +1 -1
- package/dist/service/KubernetesProxy.cjs.js +168 -62
- package/dist/service/KubernetesProxy.cjs.js.map +1 -1
- package/dist/service/KubernetesRouter.cjs.js +89 -40
- package/dist/service/KubernetesRouter.cjs.js.map +1 -1
- package/dist/service/ProxyAuditSession.cjs.js +128 -0
- package/dist/service/ProxyAuditSession.cjs.js.map +1 -0
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @backstage/plugin-kubernetes-backend
|
|
2
2
|
|
|
3
|
+
## 0.22.0-next.1
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c6af8ac: Added audit logging for kubernetes-backend routes. The plugin now emits auditor events for cluster list, cluster proxy, entity workload queries, custom resource queries, and the deprecated services endpoint. Administrators can filter audit logs by `eventId` values `cluster-fetch` and `resource-fetch`, and by `queryType` in event metadata.
|
|
8
|
+
|
|
9
|
+
**BREAKING**: `KubernetesProxyOptions` now requires `AuditorService`.
|
|
10
|
+
|
|
3
11
|
## 0.21.7-next.0
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
|
|
2
|
-
import { LoggerService, DiscoveryService, HttpAuthService, PermissionsService } from '@backstage/backend-plugin-api';
|
|
2
|
+
import { LoggerService, DiscoveryService, HttpAuthService, AuditorService, PermissionsService } from '@backstage/backend-plugin-api';
|
|
3
3
|
import * as k8sTypes from '@backstage/plugin-kubernetes-node';
|
|
4
4
|
import { AuthenticationStrategy, ClusterDetails, KubernetesCredential, AuthMetadata, ObjectToFetch, KubernetesClustersSupplier } from '@backstage/plugin-kubernetes-node';
|
|
5
5
|
import { KubernetesRequestAuth, KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
|
@@ -222,6 +222,7 @@ type KubernetesProxyOptions = {
|
|
|
222
222
|
authStrategy: AuthenticationStrategy;
|
|
223
223
|
discovery: DiscoveryService;
|
|
224
224
|
httpAuth: HttpAuthService;
|
|
225
|
+
auditor?: AuditorService;
|
|
225
226
|
};
|
|
226
227
|
/**
|
|
227
228
|
* A proxy that routes requests to the Kubernetes API.
|
|
@@ -234,9 +235,30 @@ declare class KubernetesProxy {
|
|
|
234
235
|
private readonly clusterSupplier;
|
|
235
236
|
private readonly authStrategy;
|
|
236
237
|
private readonly httpAuth;
|
|
238
|
+
private readonly auditor?;
|
|
237
239
|
constructor(options: KubernetesProxyOptions);
|
|
238
240
|
createRequestHandler(options: KubernetesProxyCreateRequestHandlerOptions): RequestHandler;
|
|
239
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Authorizes the request, resolves the target cluster, and dispatches
|
|
243
|
+
* to either the HTTP or WebSocket proxy path. Returns `true` when
|
|
244
|
+
* HTTP finish listeners have been attached.
|
|
245
|
+
*/
|
|
246
|
+
private authorizeAndDispatch;
|
|
247
|
+
/**
|
|
248
|
+
* Resolves the cluster, credentials, and target URL for a proxy request
|
|
249
|
+
* before dispatching to the middleware. This ensures all async preparation
|
|
250
|
+
* completes under the caller's try/catch, avoiding unhandled rejections
|
|
251
|
+
* from http-proxy-middleware's fire-and-forget upgrade() path.
|
|
252
|
+
*/
|
|
253
|
+
private prepareProxyTarget;
|
|
254
|
+
private getOrCreateMiddleware;
|
|
255
|
+
/**
|
|
256
|
+
* Branches to WebSocket upgrade or HTTP middleware dispatch and wires
|
|
257
|
+
* up the appropriate audit listeners. Returns `true` when HTTP finish
|
|
258
|
+
* listeners have been attached (used by the caller's catch block to
|
|
259
|
+
* avoid double-finalizing).
|
|
260
|
+
*/
|
|
261
|
+
private static dispatchToProxy;
|
|
240
262
|
private getClusterForRequest;
|
|
241
263
|
private static authHeadersToKubernetesRequestAuth;
|
|
242
264
|
private static headerToDictionary;
|
package/dist/package.json.cjs.js
CHANGED
package/dist/plugin.cjs.js
CHANGED
|
@@ -146,7 +146,8 @@ const kubernetesPlugin = backendPluginApi.createBackendPlugin({
|
|
|
146
146
|
catalog: pluginCatalogNode.catalogServiceRef,
|
|
147
147
|
permissions: backendPluginApi.coreServices.permissions,
|
|
148
148
|
auth: backendPluginApi.coreServices.auth,
|
|
149
|
-
httpAuth: backendPluginApi.coreServices.httpAuth
|
|
149
|
+
httpAuth: backendPluginApi.coreServices.httpAuth,
|
|
150
|
+
auditor: backendPluginApi.coreServices.auditor
|
|
150
151
|
},
|
|
151
152
|
async init({
|
|
152
153
|
http,
|
|
@@ -156,7 +157,8 @@ const kubernetesPlugin = backendPluginApi.createBackendPlugin({
|
|
|
156
157
|
catalog,
|
|
157
158
|
permissions,
|
|
158
159
|
auth,
|
|
159
|
-
httpAuth
|
|
160
|
+
httpAuth,
|
|
161
|
+
auditor
|
|
160
162
|
}) {
|
|
161
163
|
if (config.has("kubernetes")) {
|
|
162
164
|
const initializer = KubernetesInitializer.KubernetesInitializer.create({
|
|
@@ -185,6 +187,7 @@ const kubernetesPlugin = backendPluginApi.createBackendPlugin({
|
|
|
185
187
|
discovery,
|
|
186
188
|
auth,
|
|
187
189
|
httpAuth,
|
|
190
|
+
auditor,
|
|
188
191
|
authStrategyMap: Object.fromEntries(authStrategyMap.entries()),
|
|
189
192
|
fetcher,
|
|
190
193
|
clusterSupplier,
|
package/dist/plugin.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\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 { catalogServiceRef } from '@backstage/plugin-catalog-node';\n\nimport {\n type AuthenticationStrategy,\n kubernetesAuthStrategyExtensionPoint,\n type KubernetesAuthStrategyExtensionPoint,\n type KubernetesClustersSupplier,\n kubernetesClusterSupplierExtensionPoint,\n type KubernetesClusterSupplierExtensionPoint,\n KubernetesClusterSupplierFactory,\n type KubernetesFetcher,\n kubernetesFetcherExtensionPoint,\n type KubernetesFetcherExtensionPoint,\n KubernetesFetcherFactory,\n type KubernetesObjectsProvider,\n kubernetesObjectsProviderExtensionPoint,\n type KubernetesObjectsProviderExtensionPoint,\n KubernetesObjectsProviderFactory,\n KubernetesRouterExtensionPoint,\n kubernetesRouterExtensionPoint,\n KubernetesRouterFactory,\n type KubernetesServiceLocator,\n kubernetesServiceLocatorExtensionPoint,\n type KubernetesServiceLocatorExtensionPoint,\n KubernetesServiceLocatorFactory,\n} from '@backstage/plugin-kubernetes-node';\nimport { KubernetesRouter } from './service/KubernetesRouter';\nimport { KubernetesInitializer } from './service/KubernetesInitializer';\n\nclass ObjectsProvider implements KubernetesObjectsProviderExtensionPoint {\n private objectsProvider: KubernetesObjectsProviderFactory | undefined;\n\n getObjectsProvider() {\n return this.objectsProvider;\n }\n\n addObjectsProvider(\n provider: KubernetesObjectsProvider | KubernetesObjectsProviderFactory,\n ) {\n if (this.objectsProvider) {\n throw new Error(\n 'Multiple Kubernetes objects provider is not supported at this time',\n );\n }\n if (typeof provider !== 'function') {\n this.objectsProvider = async () => provider;\n } else {\n this.objectsProvider = provider;\n }\n }\n}\n\nclass ClusterSuplier implements KubernetesClusterSupplierExtensionPoint {\n private clusterSupplier: KubernetesClusterSupplierFactory | undefined;\n\n getClusterSupplier() {\n return this.clusterSupplier;\n }\n\n addClusterSupplier(\n clusterSupplier:\n | KubernetesClustersSupplier\n | KubernetesClusterSupplierFactory,\n ) {\n if (this.clusterSupplier) {\n throw new Error(\n 'Multiple Kubernetes Cluster Suppliers is not supported at this time',\n );\n }\n if (typeof clusterSupplier !== 'function') {\n this.clusterSupplier = async () => clusterSupplier;\n } else {\n this.clusterSupplier = clusterSupplier;\n }\n }\n}\n\nclass Fetcher implements KubernetesFetcherExtensionPoint {\n private fetcher: KubernetesFetcherFactory | undefined;\n\n getFetcher() {\n return this.fetcher;\n }\n\n addFetcher(fetcher: KubernetesFetcher | KubernetesFetcherFactory) {\n if (this.fetcher) {\n throw new Error(\n 'Multiple Kubernetes Fetchers is not supported at this time',\n );\n }\n if (typeof fetcher !== 'function') {\n this.fetcher = async () => fetcher;\n } else {\n this.fetcher = fetcher;\n }\n }\n}\n\nclass ServiceLocator implements KubernetesServiceLocatorExtensionPoint {\n private serviceLocator: KubernetesServiceLocatorFactory | undefined;\n\n getServiceLocator() {\n return this.serviceLocator;\n }\n\n addServiceLocator(\n serviceLocator: KubernetesServiceLocator | KubernetesServiceLocatorFactory,\n ) {\n if (this.serviceLocator) {\n throw new Error(\n 'Multiple Kubernetes Service Locators is not supported at this time',\n );\n }\n\n if (typeof serviceLocator !== 'function') {\n this.serviceLocator = async () => serviceLocator;\n } else {\n this.serviceLocator = serviceLocator;\n }\n }\n}\n\nclass AuthStrategy implements KubernetesAuthStrategyExtensionPoint {\n private authStrategies: Map<string, AuthenticationStrategy> | undefined;\n\n getAuthenticationStrategies() {\n return this.authStrategies;\n }\n\n addAuthStrategy(key: string, authStrategy: AuthenticationStrategy) {\n if (!this.authStrategies) {\n this.authStrategies = new Map<string, AuthenticationStrategy>();\n }\n\n if (key.includes('-')) {\n throw new Error('Strategy name can not include dashes');\n }\n\n this.authStrategies.set(key, authStrategy);\n }\n}\n\nclass CustomRouter implements KubernetesRouterExtensionPoint {\n private router: KubernetesRouterFactory | undefined;\n\n getRouter() {\n return this.router;\n }\n\n addRouter(router: KubernetesRouterFactory) {\n if (this.router) {\n throw new Error(\n 'Multiple Kubernetes routers is not supported at this time',\n );\n }\n\n this.router = router;\n }\n}\n\n/**\n * This is the backend plugin that provides the Kubernetes integration.\n * @public\n */\nexport const kubernetesPlugin = createBackendPlugin({\n pluginId: 'kubernetes',\n register(env) {\n const extPointObjectsProvider = new ObjectsProvider();\n const extPointClusterSuplier = new ClusterSuplier();\n const extPointAuthStrategy = new AuthStrategy();\n const extPointFetcher = new Fetcher();\n const extPointServiceLocator = new ServiceLocator();\n const extPointRouter = new CustomRouter();\n\n env.registerExtensionPoint(\n kubernetesObjectsProviderExtensionPoint,\n extPointObjectsProvider,\n );\n env.registerExtensionPoint(\n kubernetesClusterSupplierExtensionPoint,\n extPointClusterSuplier,\n );\n env.registerExtensionPoint(\n kubernetesAuthStrategyExtensionPoint,\n extPointAuthStrategy,\n );\n env.registerExtensionPoint(\n kubernetesFetcherExtensionPoint,\n extPointFetcher,\n );\n env.registerExtensionPoint(\n kubernetesServiceLocatorExtensionPoint,\n extPointServiceLocator,\n );\n env.registerExtensionPoint(kubernetesRouterExtensionPoint, extPointRouter);\n\n env.registerInit({\n deps: {\n http: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n discovery: coreServices.discovery,\n catalog: catalogServiceRef,\n permissions: coreServices.permissions,\n auth: coreServices.auth,\n httpAuth: coreServices.httpAuth,\n },\n async init({\n http,\n logger,\n config,\n discovery,\n catalog,\n permissions,\n auth,\n httpAuth,\n }) {\n // TODO: this could do with a cleanup and push some of this initialization somewhere else\n if (config.has('kubernetes')) {\n const initializer = KubernetesInitializer.create({\n logger,\n config,\n catalog,\n auth,\n fetcher: extPointFetcher.getFetcher(),\n clusterSupplier: extPointClusterSuplier.getClusterSupplier(),\n serviceLocator: extPointServiceLocator.getServiceLocator(),\n objectsProvider: extPointObjectsProvider.getObjectsProvider(),\n authStrategyMap: extPointAuthStrategy.getAuthenticationStrategies(),\n });\n\n const {\n fetcher,\n authStrategyMap,\n clusterSupplier,\n serviceLocator,\n objectsProvider,\n } = await initializer.init();\n\n const router = KubernetesRouter.create({\n logger,\n config,\n catalog,\n permissions,\n discovery,\n auth,\n httpAuth,\n authStrategyMap: Object.fromEntries(authStrategyMap.entries()),\n fetcher,\n clusterSupplier,\n serviceLocator,\n objectsProvider,\n customRouter: extPointRouter.getRouter(),\n });\n\n http.use(await router.getRouter());\n } else {\n logger.warn(\n 'Failed to initialize kubernetes backend: valid kubernetes config is missing',\n );\n }\n },\n });\n },\n});\n"],"names":["createBackendPlugin","kubernetesObjectsProviderExtensionPoint","kubernetesClusterSupplierExtensionPoint","kubernetesAuthStrategyExtensionPoint","kubernetesFetcherExtensionPoint","kubernetesServiceLocatorExtensionPoint","kubernetesRouterExtensionPoint","coreServices","catalogServiceRef","KubernetesInitializer","KubernetesRouter"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\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 { catalogServiceRef } from '@backstage/plugin-catalog-node';\n\nimport {\n type AuthenticationStrategy,\n kubernetesAuthStrategyExtensionPoint,\n type KubernetesAuthStrategyExtensionPoint,\n type KubernetesClustersSupplier,\n kubernetesClusterSupplierExtensionPoint,\n type KubernetesClusterSupplierExtensionPoint,\n KubernetesClusterSupplierFactory,\n type KubernetesFetcher,\n kubernetesFetcherExtensionPoint,\n type KubernetesFetcherExtensionPoint,\n KubernetesFetcherFactory,\n type KubernetesObjectsProvider,\n kubernetesObjectsProviderExtensionPoint,\n type KubernetesObjectsProviderExtensionPoint,\n KubernetesObjectsProviderFactory,\n KubernetesRouterExtensionPoint,\n kubernetesRouterExtensionPoint,\n KubernetesRouterFactory,\n type KubernetesServiceLocator,\n kubernetesServiceLocatorExtensionPoint,\n type KubernetesServiceLocatorExtensionPoint,\n KubernetesServiceLocatorFactory,\n} from '@backstage/plugin-kubernetes-node';\nimport { KubernetesRouter } from './service/KubernetesRouter';\nimport { KubernetesInitializer } from './service/KubernetesInitializer';\n\nclass ObjectsProvider implements KubernetesObjectsProviderExtensionPoint {\n private objectsProvider: KubernetesObjectsProviderFactory | undefined;\n\n getObjectsProvider() {\n return this.objectsProvider;\n }\n\n addObjectsProvider(\n provider: KubernetesObjectsProvider | KubernetesObjectsProviderFactory,\n ) {\n if (this.objectsProvider) {\n throw new Error(\n 'Multiple Kubernetes objects provider is not supported at this time',\n );\n }\n if (typeof provider !== 'function') {\n this.objectsProvider = async () => provider;\n } else {\n this.objectsProvider = provider;\n }\n }\n}\n\nclass ClusterSuplier implements KubernetesClusterSupplierExtensionPoint {\n private clusterSupplier: KubernetesClusterSupplierFactory | undefined;\n\n getClusterSupplier() {\n return this.clusterSupplier;\n }\n\n addClusterSupplier(\n clusterSupplier:\n | KubernetesClustersSupplier\n | KubernetesClusterSupplierFactory,\n ) {\n if (this.clusterSupplier) {\n throw new Error(\n 'Multiple Kubernetes Cluster Suppliers is not supported at this time',\n );\n }\n if (typeof clusterSupplier !== 'function') {\n this.clusterSupplier = async () => clusterSupplier;\n } else {\n this.clusterSupplier = clusterSupplier;\n }\n }\n}\n\nclass Fetcher implements KubernetesFetcherExtensionPoint {\n private fetcher: KubernetesFetcherFactory | undefined;\n\n getFetcher() {\n return this.fetcher;\n }\n\n addFetcher(fetcher: KubernetesFetcher | KubernetesFetcherFactory) {\n if (this.fetcher) {\n throw new Error(\n 'Multiple Kubernetes Fetchers is not supported at this time',\n );\n }\n if (typeof fetcher !== 'function') {\n this.fetcher = async () => fetcher;\n } else {\n this.fetcher = fetcher;\n }\n }\n}\n\nclass ServiceLocator implements KubernetesServiceLocatorExtensionPoint {\n private serviceLocator: KubernetesServiceLocatorFactory | undefined;\n\n getServiceLocator() {\n return this.serviceLocator;\n }\n\n addServiceLocator(\n serviceLocator: KubernetesServiceLocator | KubernetesServiceLocatorFactory,\n ) {\n if (this.serviceLocator) {\n throw new Error(\n 'Multiple Kubernetes Service Locators is not supported at this time',\n );\n }\n\n if (typeof serviceLocator !== 'function') {\n this.serviceLocator = async () => serviceLocator;\n } else {\n this.serviceLocator = serviceLocator;\n }\n }\n}\n\nclass AuthStrategy implements KubernetesAuthStrategyExtensionPoint {\n private authStrategies: Map<string, AuthenticationStrategy> | undefined;\n\n getAuthenticationStrategies() {\n return this.authStrategies;\n }\n\n addAuthStrategy(key: string, authStrategy: AuthenticationStrategy) {\n if (!this.authStrategies) {\n this.authStrategies = new Map<string, AuthenticationStrategy>();\n }\n\n if (key.includes('-')) {\n throw new Error('Strategy name can not include dashes');\n }\n\n this.authStrategies.set(key, authStrategy);\n }\n}\n\nclass CustomRouter implements KubernetesRouterExtensionPoint {\n private router: KubernetesRouterFactory | undefined;\n\n getRouter() {\n return this.router;\n }\n\n addRouter(router: KubernetesRouterFactory) {\n if (this.router) {\n throw new Error(\n 'Multiple Kubernetes routers is not supported at this time',\n );\n }\n\n this.router = router;\n }\n}\n\n/**\n * This is the backend plugin that provides the Kubernetes integration.\n * @public\n */\nexport const kubernetesPlugin = createBackendPlugin({\n pluginId: 'kubernetes',\n register(env) {\n const extPointObjectsProvider = new ObjectsProvider();\n const extPointClusterSuplier = new ClusterSuplier();\n const extPointAuthStrategy = new AuthStrategy();\n const extPointFetcher = new Fetcher();\n const extPointServiceLocator = new ServiceLocator();\n const extPointRouter = new CustomRouter();\n\n env.registerExtensionPoint(\n kubernetesObjectsProviderExtensionPoint,\n extPointObjectsProvider,\n );\n env.registerExtensionPoint(\n kubernetesClusterSupplierExtensionPoint,\n extPointClusterSuplier,\n );\n env.registerExtensionPoint(\n kubernetesAuthStrategyExtensionPoint,\n extPointAuthStrategy,\n );\n env.registerExtensionPoint(\n kubernetesFetcherExtensionPoint,\n extPointFetcher,\n );\n env.registerExtensionPoint(\n kubernetesServiceLocatorExtensionPoint,\n extPointServiceLocator,\n );\n env.registerExtensionPoint(kubernetesRouterExtensionPoint, extPointRouter);\n\n env.registerInit({\n deps: {\n http: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n discovery: coreServices.discovery,\n catalog: catalogServiceRef,\n permissions: coreServices.permissions,\n auth: coreServices.auth,\n httpAuth: coreServices.httpAuth,\n auditor: coreServices.auditor,\n },\n async init({\n http,\n logger,\n config,\n discovery,\n catalog,\n permissions,\n auth,\n httpAuth,\n auditor,\n }) {\n // TODO: this could do with a cleanup and push some of this initialization somewhere else\n if (config.has('kubernetes')) {\n const initializer = KubernetesInitializer.create({\n logger,\n config,\n catalog,\n auth,\n fetcher: extPointFetcher.getFetcher(),\n clusterSupplier: extPointClusterSuplier.getClusterSupplier(),\n serviceLocator: extPointServiceLocator.getServiceLocator(),\n objectsProvider: extPointObjectsProvider.getObjectsProvider(),\n authStrategyMap: extPointAuthStrategy.getAuthenticationStrategies(),\n });\n\n const {\n fetcher,\n authStrategyMap,\n clusterSupplier,\n serviceLocator,\n objectsProvider,\n } = await initializer.init();\n\n const router = KubernetesRouter.create({\n logger,\n config,\n catalog,\n permissions,\n discovery,\n auth,\n httpAuth,\n auditor,\n authStrategyMap: Object.fromEntries(authStrategyMap.entries()),\n fetcher,\n clusterSupplier,\n serviceLocator,\n objectsProvider,\n customRouter: extPointRouter.getRouter(),\n });\n\n http.use(await router.getRouter());\n } else {\n logger.warn(\n 'Failed to initialize kubernetes backend: valid kubernetes config is missing',\n );\n }\n },\n });\n },\n});\n"],"names":["createBackendPlugin","kubernetesObjectsProviderExtensionPoint","kubernetesClusterSupplierExtensionPoint","kubernetesAuthStrategyExtensionPoint","kubernetesFetcherExtensionPoint","kubernetesServiceLocatorExtensionPoint","kubernetesRouterExtensionPoint","coreServices","catalogServiceRef","KubernetesInitializer","KubernetesRouter"],"mappings":"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"}
|
|
@@ -5,9 +5,9 @@ var errors = require('@backstage/errors');
|
|
|
5
5
|
var requirePermission = require('../auth/requirePermission.cjs.js');
|
|
6
6
|
var pluginKubernetesCommon = require('@backstage/plugin-kubernetes-common');
|
|
7
7
|
|
|
8
|
-
const addResourceRoutesToRouter = (router, catalog, objectsProvider, httpAuth, permissionApi) => {
|
|
8
|
+
const addResourceRoutesToRouter = (router, catalog, objectsProvider, httpAuth, permissionApi, auditor, logger) => {
|
|
9
9
|
const getEntityByReq = async (req) => {
|
|
10
|
-
const rawEntityRef = req.body
|
|
10
|
+
const rawEntityRef = req.body?.entityRef;
|
|
11
11
|
if (rawEntityRef && typeof rawEntityRef !== "string") {
|
|
12
12
|
throw new errors.InputError(`entity query must be a string`);
|
|
13
13
|
} else if (!rawEntityRef) {
|
|
@@ -30,46 +30,82 @@ const addResourceRoutesToRouter = (router, catalog, objectsProvider, httpAuth, p
|
|
|
30
30
|
return entity;
|
|
31
31
|
};
|
|
32
32
|
router.post("/resources/workloads/query", async (req, res) => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
const entityRef = typeof req.body?.entityRef === "string" ? req.body.entityRef : void 0;
|
|
34
|
+
const auditorEvent = await auditor.createEvent({
|
|
35
|
+
eventId: "resource-fetch",
|
|
36
|
+
request: req,
|
|
37
|
+
meta: { queryType: "workloads", entityRef }
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
await requirePermission.requirePermission(
|
|
41
|
+
permissionApi,
|
|
42
|
+
pluginKubernetesCommon.kubernetesResourcesReadPermission,
|
|
43
|
+
httpAuth,
|
|
44
|
+
req
|
|
45
|
+
);
|
|
46
|
+
const entity = await getEntityByReq(req);
|
|
47
|
+
const response = await objectsProvider.getKubernetesObjectsByEntity(
|
|
48
|
+
{
|
|
49
|
+
entity,
|
|
50
|
+
auth: req.body.auth
|
|
51
|
+
},
|
|
52
|
+
{ credentials: await httpAuth.credentials(req) }
|
|
53
|
+
);
|
|
54
|
+
res.json(response);
|
|
55
|
+
auditorEvent.success().catch(
|
|
56
|
+
(error) => logger.error(
|
|
57
|
+
"Failed to emit audit event resource-fetch (workloads)",
|
|
58
|
+
error
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
63
|
+
await auditorEvent.fail({ error: err });
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
48
66
|
});
|
|
49
67
|
router.post("/resources/custom/query", async (req, res) => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
const entityRef = typeof req.body?.entityRef === "string" ? req.body.entityRef : void 0;
|
|
69
|
+
const auditorEvent = await auditor.createEvent({
|
|
70
|
+
eventId: "resource-fetch",
|
|
71
|
+
request: req,
|
|
72
|
+
meta: { queryType: "custom", entityRef }
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
await requirePermission.requirePermission(
|
|
76
|
+
permissionApi,
|
|
77
|
+
pluginKubernetesCommon.kubernetesResourcesReadPermission,
|
|
78
|
+
httpAuth,
|
|
79
|
+
req
|
|
80
|
+
);
|
|
81
|
+
const entity = await getEntityByReq(req);
|
|
82
|
+
if (!req.body.customResources) {
|
|
83
|
+
throw new errors.InputError("customResources is a required field");
|
|
84
|
+
} else if (!Array.isArray(req.body.customResources)) {
|
|
85
|
+
throw new errors.InputError("customResources must be an array");
|
|
86
|
+
} else if (req.body.customResources.length === 0) {
|
|
87
|
+
throw new errors.InputError("at least 1 customResource is required");
|
|
88
|
+
}
|
|
89
|
+
const response = await objectsProvider.getCustomResourcesByEntity(
|
|
90
|
+
{
|
|
91
|
+
entity,
|
|
92
|
+
customResources: req.body.customResources,
|
|
93
|
+
auth: req.body.auth
|
|
94
|
+
},
|
|
95
|
+
{ credentials: await httpAuth.credentials(req) }
|
|
96
|
+
);
|
|
97
|
+
res.json(response);
|
|
98
|
+
auditorEvent.success().catch(
|
|
99
|
+
(error) => logger.error(
|
|
100
|
+
"Failed to emit audit event resource-fetch (custom)",
|
|
101
|
+
error
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
106
|
+
await auditorEvent.fail({ error: err });
|
|
107
|
+
throw error;
|
|
63
108
|
}
|
|
64
|
-
const response = await objectsProvider.getCustomResourcesByEntity(
|
|
65
|
-
{
|
|
66
|
-
entity,
|
|
67
|
-
customResources: req.body.customResources,
|
|
68
|
-
auth: req.body.auth
|
|
69
|
-
},
|
|
70
|
-
{ credentials: await httpAuth.credentials(req) }
|
|
71
|
-
);
|
|
72
|
-
res.json(response);
|
|
73
109
|
});
|
|
74
110
|
};
|
|
75
111
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resourcesRoutes.cjs.js","sources":["../../src/routes/resourcesRoutes.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\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 */\nimport {\n CompoundEntityRef,\n parseEntityRef,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport { InputError } from '@backstage/errors';\nimport express, { Request } from 'express';\nimport { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node';\nimport {
|
|
1
|
+
{"version":3,"file":"resourcesRoutes.cjs.js","sources":["../../src/routes/resourcesRoutes.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\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 */\nimport {\n CompoundEntityRef,\n parseEntityRef,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport { InputError } from '@backstage/errors';\nimport express, { Request } from 'express';\nimport { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node';\nimport {\n AuditorService,\n HttpAuthService,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport { PermissionEvaluator } from '@backstage/plugin-permission-common';\nimport { requirePermission } from '../auth/requirePermission';\nimport { kubernetesResourcesReadPermission } from '@backstage/plugin-kubernetes-common';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\n\nexport const addResourceRoutesToRouter = (\n router: express.Router,\n catalog: CatalogService,\n objectsProvider: KubernetesObjectsProvider,\n httpAuth: HttpAuthService,\n permissionApi: PermissionEvaluator,\n auditor: AuditorService,\n logger: LoggerService,\n) => {\n const getEntityByReq = async (req: Request<any>) => {\n const rawEntityRef = req.body?.entityRef;\n if (rawEntityRef && typeof rawEntityRef !== 'string') {\n throw new InputError(`entity query must be a string`);\n } else if (!rawEntityRef) {\n throw new InputError('entity is a required field');\n }\n let entityRef: CompoundEntityRef | undefined = undefined;\n\n try {\n entityRef = parseEntityRef(rawEntityRef);\n } catch (error) {\n throw new InputError(`Invalid entity ref, ${error}`);\n }\n\n const entity = await catalog.getEntityByRef(entityRef, {\n credentials: await httpAuth.credentials(req),\n });\n if (!entity) {\n throw new InputError(\n `Entity ref missing, ${stringifyEntityRef(entityRef)}`,\n );\n }\n\n return entity;\n };\n\n router.post('/resources/workloads/query', async (req, res) => {\n const entityRef =\n typeof req.body?.entityRef === 'string' ? req.body.entityRef : undefined;\n\n const auditorEvent = await auditor.createEvent({\n eventId: 'resource-fetch',\n request: req,\n meta: { queryType: 'workloads', entityRef },\n });\n\n try {\n await requirePermission(\n permissionApi,\n kubernetesResourcesReadPermission,\n httpAuth,\n req,\n );\n const entity = await getEntityByReq(req);\n const response = await objectsProvider.getKubernetesObjectsByEntity(\n {\n entity,\n auth: req.body.auth,\n },\n { credentials: await httpAuth.credentials(req) },\n );\n res.json(response);\n auditorEvent\n .success()\n .catch(error =>\n logger.error(\n 'Failed to emit audit event resource-fetch (workloads)',\n error,\n ),\n );\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n await auditorEvent.fail({ error: err });\n throw error;\n }\n });\n\n router.post('/resources/custom/query', async (req, res) => {\n const entityRef =\n typeof req.body?.entityRef === 'string' ? req.body.entityRef : undefined;\n\n const auditorEvent = await auditor.createEvent({\n eventId: 'resource-fetch',\n request: req,\n meta: { queryType: 'custom', entityRef },\n });\n\n try {\n await requirePermission(\n permissionApi,\n kubernetesResourcesReadPermission,\n httpAuth,\n req,\n );\n const entity = await getEntityByReq(req);\n\n if (!req.body.customResources) {\n throw new InputError('customResources is a required field');\n } else if (!Array.isArray(req.body.customResources)) {\n throw new InputError('customResources must be an array');\n } else if (req.body.customResources.length === 0) {\n throw new InputError('at least 1 customResource is required');\n }\n\n const response = await objectsProvider.getCustomResourcesByEntity(\n {\n entity,\n customResources: req.body.customResources,\n auth: req.body.auth,\n },\n { credentials: await httpAuth.credentials(req) },\n );\n res.json(response);\n auditorEvent\n .success()\n .catch(error =>\n logger.error(\n 'Failed to emit audit event resource-fetch (custom)',\n error,\n ),\n );\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n await auditorEvent.fail({ error: err });\n throw error;\n }\n });\n};\n"],"names":["InputError","parseEntityRef","stringifyEntityRef","requirePermission","kubernetesResourcesReadPermission"],"mappings":"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"}
|
|
@@ -5,11 +5,13 @@ var pluginKubernetesCommon = require('@backstage/plugin-kubernetes-common');
|
|
|
5
5
|
var pluginPermissionCommon = require('@backstage/plugin-permission-common');
|
|
6
6
|
var httpProxyMiddleware = require('http-proxy-middleware');
|
|
7
7
|
var fs = require('fs-extra');
|
|
8
|
+
var ProxyAuditSession = require('./ProxyAuditSession.cjs.js');
|
|
8
9
|
|
|
9
10
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
10
11
|
|
|
11
12
|
var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
|
|
12
13
|
|
|
14
|
+
const PROXY_PREPARED_TARGET = /* @__PURE__ */ Symbol("kubernetesProxyPreparedTarget");
|
|
13
15
|
const HEADER_KUBERNETES_CLUSTER = "Backstage-Kubernetes-Cluster";
|
|
14
16
|
const HEADER_KUBERNETES_AUTH = "Backstage-Kubernetes-Authorization";
|
|
15
17
|
class KubernetesProxy {
|
|
@@ -18,42 +20,147 @@ class KubernetesProxy {
|
|
|
18
20
|
clusterSupplier;
|
|
19
21
|
authStrategy;
|
|
20
22
|
httpAuth;
|
|
23
|
+
auditor;
|
|
21
24
|
constructor(options) {
|
|
22
25
|
this.logger = options.logger;
|
|
23
26
|
this.clusterSupplier = options.clusterSupplier;
|
|
24
27
|
this.authStrategy = options.authStrategy;
|
|
25
28
|
this.httpAuth = options.httpAuth;
|
|
29
|
+
this.auditor = options.auditor;
|
|
26
30
|
}
|
|
27
31
|
createRequestHandler(options) {
|
|
28
32
|
const { permissionApi } = options;
|
|
29
33
|
return async (req, res, next) => {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
const clusterNameHeader = req.headers[HEADER_KUBERNETES_CLUSTER.toLowerCase()];
|
|
35
|
+
const clusterName = typeof clusterNameHeader === "string" && clusterNameHeader.length > 0 ? clusterNameHeader : "unknown";
|
|
36
|
+
const path = req.path || req.url.split("?")[0] || "";
|
|
37
|
+
let clientDisconnected = false;
|
|
38
|
+
const onSocketClose = () => {
|
|
39
|
+
clientDisconnected = true;
|
|
40
|
+
};
|
|
41
|
+
req.socket.once("close", onSocketClose);
|
|
42
|
+
let auditSession;
|
|
43
|
+
let finishListenerAttached = false;
|
|
44
|
+
try {
|
|
45
|
+
auditSession = await ProxyAuditSession.ProxyAuditSession.start(this.auditor, {
|
|
46
|
+
req,
|
|
47
|
+
clusterName,
|
|
48
|
+
method: req.method,
|
|
49
|
+
path,
|
|
50
|
+
logger: this.logger
|
|
51
|
+
});
|
|
52
|
+
finishListenerAttached = await this.authorizeAndDispatch(
|
|
53
|
+
req,
|
|
54
|
+
res,
|
|
55
|
+
next,
|
|
56
|
+
permissionApi,
|
|
57
|
+
auditSession,
|
|
58
|
+
onSocketClose,
|
|
59
|
+
() => clientDisconnected
|
|
60
|
+
);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
req.socket.removeListener("close", onSocketClose);
|
|
63
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
64
|
+
if (auditSession && !finishListenerAttached) {
|
|
65
|
+
auditSession.finalize(err);
|
|
34
66
|
}
|
|
35
|
-
|
|
36
|
-
const auth = authorizeResponse[0];
|
|
37
|
-
if (auth.result === pluginPermissionCommon.AuthorizeResult.DENY) {
|
|
38
|
-
res.status(403).json({ error: new errors.NotAllowedError("Unauthorized") });
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
const middleware = await this.getMiddleware(req);
|
|
42
|
-
if (req.header("connection")?.toLowerCase() === "upgrade" && req.header("upgrade")?.toLowerCase() === "websocket") {
|
|
43
|
-
middleware.upgrade(req, req.socket, void 0);
|
|
44
|
-
} else {
|
|
45
|
-
middleware(req, res, next);
|
|
67
|
+
throw error;
|
|
46
68
|
}
|
|
47
69
|
};
|
|
48
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Authorizes the request, resolves the target cluster, and dispatches
|
|
73
|
+
* to either the HTTP or WebSocket proxy path. Returns `true` when
|
|
74
|
+
* HTTP finish listeners have been attached.
|
|
75
|
+
*/
|
|
76
|
+
async authorizeAndDispatch(req, res, next, permissionApi, auditSession, onSocketClose, isClientDisconnected) {
|
|
77
|
+
const authorizeResponse = await permissionApi.authorize(
|
|
78
|
+
[{ permission: pluginKubernetesCommon.kubernetesProxyPermission }],
|
|
79
|
+
{ credentials: await this.httpAuth.credentials(req) }
|
|
80
|
+
);
|
|
81
|
+
if (authorizeResponse[0].result === pluginPermissionCommon.AuthorizeResult.DENY) {
|
|
82
|
+
req.socket.removeListener("close", onSocketClose);
|
|
83
|
+
auditSession.finalize(new errors.NotAllowedError("Unauthorized"));
|
|
84
|
+
res.status(403).json({
|
|
85
|
+
error: errors.serializeError(new errors.NotAllowedError("Unauthorized"))
|
|
86
|
+
});
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (isClientDisconnected()) {
|
|
90
|
+
auditSession.finalize(
|
|
91
|
+
new Error("Client disconnected before proxy dispatch")
|
|
92
|
+
);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const prepared = await this.prepareProxyTarget(req);
|
|
96
|
+
auditSession.setResolvedClusterName(prepared.cluster.name);
|
|
97
|
+
req[PROXY_PREPARED_TARGET] = prepared;
|
|
98
|
+
const middleware = this.getOrCreateMiddleware(prepared.cluster);
|
|
99
|
+
if (isClientDisconnected()) {
|
|
100
|
+
auditSession.finalize(
|
|
101
|
+
new Error("Client disconnected before proxy dispatch")
|
|
102
|
+
);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return KubernetesProxy.dispatchToProxy(
|
|
106
|
+
req,
|
|
107
|
+
res,
|
|
108
|
+
next,
|
|
109
|
+
middleware,
|
|
110
|
+
auditSession,
|
|
111
|
+
onSocketClose
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolves the cluster, credentials, and target URL for a proxy request
|
|
116
|
+
* before dispatching to the middleware. This ensures all async preparation
|
|
117
|
+
* completes under the caller's try/catch, avoiding unhandled rejections
|
|
118
|
+
* from http-proxy-middleware's fire-and-forget upgrade() path.
|
|
119
|
+
*/
|
|
120
|
+
async prepareProxyTarget(req) {
|
|
121
|
+
const cluster = await this.getClusterForRequest(req);
|
|
122
|
+
const url = new URL(cluster.url);
|
|
123
|
+
const { bufferFromFileOrString } = await import('@kubernetes/client-node');
|
|
124
|
+
const target = {
|
|
125
|
+
protocol: url.protocol,
|
|
126
|
+
host: url.hostname,
|
|
127
|
+
port: url.port,
|
|
128
|
+
ca: bufferFromFileOrString(cluster.caFile, cluster.caData)?.toString()
|
|
129
|
+
};
|
|
130
|
+
const authHeader = req.headers[HEADER_KUBERNETES_AUTH.toLocaleLowerCase("en-US")];
|
|
131
|
+
if (typeof authHeader === "string") {
|
|
132
|
+
req.headers.authorization = authHeader;
|
|
133
|
+
} else {
|
|
134
|
+
const authObj = KubernetesProxy.authHeadersToKubernetesRequestAuth(
|
|
135
|
+
req.headers
|
|
136
|
+
);
|
|
137
|
+
const credential = await this.authStrategy.getCredential(
|
|
138
|
+
cluster,
|
|
139
|
+
authObj
|
|
140
|
+
);
|
|
141
|
+
if (credential.type === "bearer token") {
|
|
142
|
+
req.headers.authorization = `Bearer ${credential.token}`;
|
|
143
|
+
} else if (credential.type === "x509 client certificate") {
|
|
144
|
+
target.key = credential.key;
|
|
145
|
+
target.cert = credential.cert;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const requestPath = req.originalUrl || req.url || "";
|
|
149
|
+
const rewrittenPath = requestPath.replace(
|
|
150
|
+
new RegExp(`^${req.baseUrl || ""}`),
|
|
151
|
+
url.pathname || ""
|
|
152
|
+
);
|
|
153
|
+
return { proxyTarget: target, rewrittenPath, cluster };
|
|
154
|
+
}
|
|
49
155
|
// We create one middleware per remote cluster and hold on to them, because
|
|
50
156
|
// the secure property isn't possible to decide on a per-request basis with a
|
|
51
157
|
// single middleware instance - and we don't expect it to change over time.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
158
|
+
// Target resolution and path rewriting are handled per-request via the
|
|
159
|
+
// PROXY_PREPARED_TARGET symbol stashed on each req by prepareProxyTarget.
|
|
160
|
+
getOrCreateMiddleware(cluster) {
|
|
161
|
+
let middleware = this.middlewareForClusterName.get(cluster.name);
|
|
55
162
|
if (!middleware) {
|
|
56
|
-
const logger = this.logger.child({ cluster:
|
|
163
|
+
const logger = this.logger.child({ cluster: cluster.name });
|
|
57
164
|
middleware = httpProxyMiddleware.createProxyMiddleware({
|
|
58
165
|
logProvider: () => ({
|
|
59
166
|
log: logger.info.bind(logger),
|
|
@@ -62,55 +169,27 @@ class KubernetesProxy {
|
|
|
62
169
|
warn: logger.warn.bind(logger),
|
|
63
170
|
error: logger.error.bind(logger)
|
|
64
171
|
}),
|
|
65
|
-
ws
|
|
66
|
-
|
|
172
|
+
// ws must be false to prevent http-proxy-middleware from auto-subscribing
|
|
173
|
+
// to the server's 'upgrade' event, which would bypass Express routing and
|
|
174
|
+
// skip audit event creation for WebSocket requests.
|
|
175
|
+
ws: false,
|
|
176
|
+
secure: !cluster.skipTLSVerify,
|
|
67
177
|
changeOrigin: true,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const url = new URL(cluster.url);
|
|
71
|
-
return path.replace(
|
|
72
|
-
new RegExp(`^${originalReq.baseUrl}`),
|
|
73
|
-
url.pathname || ""
|
|
74
|
-
);
|
|
178
|
+
router: (req) => {
|
|
179
|
+
return req[PROXY_PREPARED_TARGET]?.proxyTarget;
|
|
75
180
|
},
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const url = new URL(cluster.url);
|
|
79
|
-
const { bufferFromFileOrString } = await import('@kubernetes/client-node');
|
|
80
|
-
const target = {
|
|
81
|
-
protocol: url.protocol,
|
|
82
|
-
host: url.hostname,
|
|
83
|
-
port: url.port,
|
|
84
|
-
ca: bufferFromFileOrString(
|
|
85
|
-
cluster.caFile,
|
|
86
|
-
cluster.caData
|
|
87
|
-
)?.toString()
|
|
88
|
-
};
|
|
89
|
-
const authHeader = req.headers[HEADER_KUBERNETES_AUTH.toLocaleLowerCase("en-US")];
|
|
90
|
-
if (typeof authHeader === "string") {
|
|
91
|
-
req.headers.authorization = authHeader;
|
|
92
|
-
} else {
|
|
93
|
-
const authObj = KubernetesProxy.authHeadersToKubernetesRequestAuth(
|
|
94
|
-
req.headers
|
|
95
|
-
);
|
|
96
|
-
const credential = await this.getClusterForRequest(req).then((cd) => {
|
|
97
|
-
return this.authStrategy.getCredential(cd, authObj);
|
|
98
|
-
});
|
|
99
|
-
if (credential.type === "bearer token") {
|
|
100
|
-
req.headers.authorization = `Bearer ${credential.token}`;
|
|
101
|
-
} else if (credential.type === "x509 client certificate") {
|
|
102
|
-
target.key = credential.key;
|
|
103
|
-
target.cert = credential.cert;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return target;
|
|
181
|
+
pathRewrite: (_path, req) => {
|
|
182
|
+
return req[PROXY_PREPARED_TARGET]?.rewrittenPath ?? _path;
|
|
107
183
|
},
|
|
108
184
|
onError: (error, req, res) => {
|
|
109
185
|
const wrappedError = new errors.ForwardedError(
|
|
110
|
-
`Cluster '${
|
|
186
|
+
`Cluster '${cluster.name}' request error`,
|
|
111
187
|
error
|
|
112
188
|
);
|
|
113
189
|
logger.error("Kubernetes proxy error", wrappedError);
|
|
190
|
+
if (typeof res.status !== "function") {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
114
193
|
const body = {
|
|
115
194
|
error: errors.serializeError(wrappedError, {
|
|
116
195
|
includeStack: process.env.NODE_ENV === "development"
|
|
@@ -119,12 +198,39 @@ class KubernetesProxy {
|
|
|
119
198
|
response: { statusCode: 500 }
|
|
120
199
|
};
|
|
121
200
|
res.status(500).json(body);
|
|
122
|
-
}
|
|
201
|
+
},
|
|
202
|
+
onProxyReqWs: ProxyAuditSession.ProxyAuditSession.handleWebSocketProxyReq
|
|
123
203
|
});
|
|
124
|
-
this.middlewareForClusterName.set(
|
|
204
|
+
this.middlewareForClusterName.set(cluster.name, middleware);
|
|
125
205
|
}
|
|
126
206
|
return middleware;
|
|
127
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Branches to WebSocket upgrade or HTTP middleware dispatch and wires
|
|
210
|
+
* up the appropriate audit listeners. Returns `true` when HTTP finish
|
|
211
|
+
* listeners have been attached (used by the caller's catch block to
|
|
212
|
+
* avoid double-finalizing).
|
|
213
|
+
*/
|
|
214
|
+
static dispatchToProxy(req, res, next, middleware, auditSession, onSocketClose) {
|
|
215
|
+
const isWebSocketUpgrade = (req.header("connection") ?? "").toLowerCase().split(",").some((t) => t.trim() === "upgrade") && req.header("upgrade")?.toLowerCase() === "websocket";
|
|
216
|
+
if (isWebSocketUpgrade) {
|
|
217
|
+
auditSession.prepareForWebSocketUpgrade(req, onSocketClose);
|
|
218
|
+
try {
|
|
219
|
+
middleware.upgrade(req, req.socket, void 0);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
req.socket.removeListener("close", onSocketClose);
|
|
222
|
+
auditSession.finalize(
|
|
223
|
+
error instanceof Error ? error : new Error(String(error))
|
|
224
|
+
);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
req.socket.removeListener("close", onSocketClose);
|
|
230
|
+
auditSession.attachToHttpResponse(res);
|
|
231
|
+
middleware(req, res, next);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
128
234
|
async getClusterForRequest(req) {
|
|
129
235
|
const { KubeConfig } = await import('@kubernetes/client-node');
|
|
130
236
|
const clusterName = req.headers[HEADER_KUBERNETES_CLUSTER.toLowerCase()];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KubernetesProxy.cjs.js","sources":["../../src/service/KubernetesProxy.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\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 */\nimport {\n ErrorResponseBody,\n ForwardedError,\n NotAllowedError,\n NotFoundError,\n serializeError,\n} from '@backstage/errors';\nimport {\n ANNOTATION_KUBERNETES_AUTH_PROVIDER,\n SERVICEACCOUNT_CA_PATH,\n kubernetesProxyPermission,\n KubernetesRequestAuth,\n} from '@backstage/plugin-kubernetes-common';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport type { Cluster } from '@kubernetes/client-node';\nimport { createProxyMiddleware, RequestHandler } from 'http-proxy-middleware';\nimport fs from 'fs-extra';\n\nimport {\n AuthenticationStrategy,\n ClusterDetails,\n KubernetesClustersSupplier,\n} from '@backstage/plugin-kubernetes-node';\n\nimport type { Request } from 'express';\nimport { IncomingHttpHeaders } from 'node:http';\nimport {\n DiscoveryService,\n HttpAuthService,\n LoggerService,\n PermissionsService,\n} from '@backstage/backend-plugin-api';\n\n/**\n * The header that is used to specify the cluster name.\n *\n * @public\n */\nexport const HEADER_KUBERNETES_CLUSTER: string = 'Backstage-Kubernetes-Cluster';\n\n/**\n * The header that is used to specify the Authentication Authorities token.\n * e.x if using the google auth provider as your authentication authority then this field would be the google provided bearer token.\n * @public\n */\nexport const HEADER_KUBERNETES_AUTH: string =\n 'Backstage-Kubernetes-Authorization';\n\n/**\n * The options object expected to be passed as a parameter to KubernetesProxy.createRequestHandler().\n *\n * @public\n */\nexport type KubernetesProxyCreateRequestHandlerOptions = {\n permissionApi: PermissionsService;\n};\n\n/**\n * Options accepted as a parameter by the KubernetesProxy\n *\n * @public\n */\nexport type KubernetesProxyOptions = {\n logger: LoggerService;\n clusterSupplier: KubernetesClustersSupplier;\n authStrategy: AuthenticationStrategy;\n discovery: DiscoveryService;\n httpAuth: HttpAuthService;\n};\n\n/**\n * A proxy that routes requests to the Kubernetes API.\n *\n * @public\n */\nexport class KubernetesProxy {\n private readonly middlewareForClusterName = new Map<string, RequestHandler>();\n private readonly logger: LoggerService;\n private readonly clusterSupplier: KubernetesClustersSupplier;\n private readonly authStrategy: AuthenticationStrategy;\n private readonly httpAuth: HttpAuthService;\n\n constructor(options: KubernetesProxyOptions) {\n this.logger = options.logger;\n this.clusterSupplier = options.clusterSupplier;\n this.authStrategy = options.authStrategy;\n this.httpAuth = options.httpAuth;\n }\n\n public createRequestHandler(\n options: KubernetesProxyCreateRequestHandlerOptions,\n ): RequestHandler {\n const { permissionApi } = options;\n return async (req, res, next) => {\n const authorizeResponse = await permissionApi.authorize(\n [{ permission: kubernetesProxyPermission }],\n {\n credentials: await this.httpAuth.credentials(req),\n },\n );\n const auth = authorizeResponse[0];\n\n if (auth.result === AuthorizeResult.DENY) {\n res.status(403).json({ error: new NotAllowedError('Unauthorized') });\n return;\n }\n\n const middleware = await this.getMiddleware(req);\n\n // If req is an upgrade handshake, use middleware upgrade instead of http request handler https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade\n if (\n req.header('connection')?.toLowerCase() === 'upgrade' &&\n req.header('upgrade')?.toLowerCase() === 'websocket'\n ) {\n // Missing the `head`, since it's optional we pass undefined to avoid type issues\n middleware.upgrade!(req, req.socket, undefined);\n } else {\n middleware(req, res, next);\n }\n };\n }\n\n // We create one middleware per remote cluster and hold on to them, because\n // the secure property isn't possible to decide on a per-request basis with a\n // single middleware instance - and we don't expect it to change over time.\n private async getMiddleware(originalReq: Request): Promise<RequestHandler> {\n const originalCluster = await this.getClusterForRequest(originalReq);\n let middleware = this.middlewareForClusterName.get(originalCluster.name);\n if (!middleware) {\n const logger = this.logger.child({ cluster: originalCluster.name });\n middleware = createProxyMiddleware({\n logProvider: () => ({\n log: logger.info.bind(logger),\n debug: logger.debug.bind(logger),\n info: logger.info.bind(logger),\n warn: logger.warn.bind(logger),\n error: logger.error.bind(logger),\n }),\n ws: true,\n secure: !originalCluster.skipTLSVerify,\n changeOrigin: true,\n pathRewrite: async (path, req) => {\n // Re-evaluate the cluster on each request, in case it has changed\n const cluster = await this.getClusterForRequest(req);\n const url = new URL(cluster.url);\n return path.replace(\n new RegExp(`^${originalReq.baseUrl}`),\n url.pathname || '',\n );\n },\n router: async req => {\n // Re-evaluate the cluster on each request, in case it has changed\n const cluster = await this.getClusterForRequest(req);\n const url = new URL(cluster.url);\n\n const { bufferFromFileOrString } = await import(\n '@kubernetes/client-node'\n );\n\n const target: any = {\n protocol: url.protocol,\n host: url.hostname,\n port: url.port,\n ca: bufferFromFileOrString(\n cluster.caFile,\n cluster.caData,\n )?.toString(),\n };\n\n const authHeader =\n req.headers[HEADER_KUBERNETES_AUTH.toLocaleLowerCase('en-US')];\n if (typeof authHeader === 'string') {\n req.headers.authorization = authHeader;\n } else {\n // Map Backstage-Kubernetes-Authorization-X-X headers to a KubernetesRequestAuth object\n const authObj = KubernetesProxy.authHeadersToKubernetesRequestAuth(\n req.headers,\n );\n\n const credential = await this.getClusterForRequest(req).then(cd => {\n return this.authStrategy.getCredential(cd, authObj);\n });\n\n if (credential.type === 'bearer token') {\n req.headers.authorization = `Bearer ${credential.token}`;\n } else if (credential.type === 'x509 client certificate') {\n target.key = credential.key;\n target.cert = credential.cert;\n }\n }\n\n return target;\n },\n onError: (error, req, res) => {\n const wrappedError = new ForwardedError(\n `Cluster '${originalCluster.name}' request error`,\n error,\n );\n\n logger.error('Kubernetes proxy error', wrappedError);\n\n const body: ErrorResponseBody = {\n error: serializeError(wrappedError, {\n includeStack: process.env.NODE_ENV === 'development',\n }),\n request: { method: req.method, url: req.originalUrl },\n response: { statusCode: 500 },\n };\n res.status(500).json(body);\n },\n });\n this.middlewareForClusterName.set(originalCluster.name, middleware);\n }\n return middleware;\n }\n\n private async getClusterForRequest(req: Request): Promise<ClusterDetails> {\n const { KubeConfig } = await import('@kubernetes/client-node');\n\n const clusterName = req.headers[HEADER_KUBERNETES_CLUSTER.toLowerCase()];\n const clusters = await this.clusterSupplier.getClusters({\n credentials: await this.httpAuth.credentials(req),\n });\n\n if (!clusters || clusters.length <= 0) {\n throw new NotFoundError(`No Clusters configured`);\n }\n\n const hasClusterNameHeader =\n typeof clusterName === 'string' && clusterName.length > 0;\n\n let cluster: ClusterDetails | undefined;\n\n if (hasClusterNameHeader) {\n cluster = clusters.find(c => c.name === clusterName);\n } else if (clusters.length === 1) {\n cluster = clusters.at(0);\n }\n\n if (!cluster) {\n throw new NotFoundError(`Cluster '${clusterName}' not found`);\n }\n\n const authProvider =\n cluster.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER];\n\n if (\n authProvider === 'serviceAccount' &&\n fs.pathExistsSync(SERVICEACCOUNT_CA_PATH) &&\n !cluster.authMetadata.serviceAccountToken\n ) {\n const kc = new KubeConfig();\n kc.loadFromCluster();\n const clusterFromKubeConfig = kc.getCurrentCluster() as Cluster;\n\n const url = new URL(clusterFromKubeConfig.server);\n cluster.url = clusterFromKubeConfig.server;\n if (url.protocol === 'https:') {\n cluster.caFile = clusterFromKubeConfig.caFile;\n }\n }\n\n return cluster;\n }\n\n private static authHeadersToKubernetesRequestAuth(\n originalHeaders: IncomingHttpHeaders,\n ): KubernetesRequestAuth {\n return Object.keys(originalHeaders)\n .filter(header => header.startsWith('backstage-kubernetes-authorization'))\n .map(header =>\n KubernetesProxy.headerToDictionary(header, originalHeaders),\n )\n .filter(headerAsDic => Object.keys(headerAsDic).length !== 0)\n .reduce(KubernetesProxy.combineHeaders, {});\n }\n\n private static headerToDictionary(\n header: string,\n originalHeaders: IncomingHttpHeaders,\n ): KubernetesRequestAuth {\n const obj: KubernetesRequestAuth = {};\n const headerSplit = header.split('-');\n if (headerSplit.length >= 4) {\n const framework = headerSplit[3].toLowerCase();\n if (headerSplit.length >= 5) {\n const provider = headerSplit.slice(4).join('-').toLowerCase();\n obj[framework] = { [provider]: originalHeaders[header] };\n } else {\n obj[framework] = originalHeaders[header];\n }\n }\n return obj;\n }\n\n private static combineHeaders(\n authObj: any,\n header: any,\n ): KubernetesRequestAuth {\n const framework = Object.keys(header)[0];\n\n if (authObj[framework]) {\n authObj[framework] = {\n ...authObj[framework],\n ...header[framework],\n };\n } else {\n authObj[framework] = header[framework];\n }\n\n return authObj;\n }\n}\n"],"names":["kubernetesProxyPermission","AuthorizeResult","NotAllowedError","createProxyMiddleware","ForwardedError","serializeError","NotFoundError","ANNOTATION_KUBERNETES_AUTH_PROVIDER","fs","SERVICEACCOUNT_CA_PATH"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"KubernetesProxy.cjs.js","sources":["../../src/service/KubernetesProxy.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\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 */\nimport {\n ErrorResponseBody,\n ForwardedError,\n NotAllowedError,\n NotFoundError,\n serializeError,\n} from '@backstage/errors';\nimport {\n ANNOTATION_KUBERNETES_AUTH_PROVIDER,\n SERVICEACCOUNT_CA_PATH,\n kubernetesProxyPermission,\n KubernetesRequestAuth,\n} from '@backstage/plugin-kubernetes-common';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport type { Cluster } from '@kubernetes/client-node';\nimport { createProxyMiddleware, RequestHandler } from 'http-proxy-middleware';\nimport fs from 'fs-extra';\n\nimport {\n AuthenticationStrategy,\n ClusterDetails,\n KubernetesClustersSupplier,\n} from '@backstage/plugin-kubernetes-node';\n\nimport type { NextFunction, Request, Response } from 'express';\nimport { IncomingHttpHeaders } from 'node:http';\nimport {\n AuditorService,\n DiscoveryService,\n HttpAuthService,\n LoggerService,\n PermissionsService,\n} from '@backstage/backend-plugin-api';\nimport { ProxyAuditSession } from './ProxyAuditSession';\n\nconst PROXY_PREPARED_TARGET = Symbol('kubernetesProxyPreparedTarget');\n\ntype PreparedProxyTarget = {\n proxyTarget: any;\n rewrittenPath: string;\n cluster: ClusterDetails;\n};\n\n/**\n * The header that is used to specify the cluster name.\n *\n * @public\n */\nexport const HEADER_KUBERNETES_CLUSTER: string = 'Backstage-Kubernetes-Cluster';\n\n/**\n * The header that is used to specify the Authentication Authorities token.\n * e.x if using the google auth provider as your authentication authority then this field would be the google provided bearer token.\n * @public\n */\nexport const HEADER_KUBERNETES_AUTH: string =\n 'Backstage-Kubernetes-Authorization';\n\n/**\n * The options object expected to be passed as a parameter to KubernetesProxy.createRequestHandler().\n *\n * @public\n */\nexport type KubernetesProxyCreateRequestHandlerOptions = {\n permissionApi: PermissionsService;\n};\n\n/**\n * Options accepted as a parameter by the KubernetesProxy\n *\n * @public\n */\nexport type KubernetesProxyOptions = {\n logger: LoggerService;\n clusterSupplier: KubernetesClustersSupplier;\n authStrategy: AuthenticationStrategy;\n discovery: DiscoveryService;\n httpAuth: HttpAuthService;\n auditor?: AuditorService;\n};\n\n/**\n * A proxy that routes requests to the Kubernetes API.\n *\n * @public\n */\nexport class KubernetesProxy {\n private readonly middlewareForClusterName = new Map<string, RequestHandler>();\n private readonly logger: LoggerService;\n private readonly clusterSupplier: KubernetesClustersSupplier;\n private readonly authStrategy: AuthenticationStrategy;\n private readonly httpAuth: HttpAuthService;\n private readonly auditor?: AuditorService;\n\n constructor(options: KubernetesProxyOptions) {\n this.logger = options.logger;\n this.clusterSupplier = options.clusterSupplier;\n this.authStrategy = options.authStrategy;\n this.httpAuth = options.httpAuth;\n this.auditor = options.auditor;\n }\n\n public createRequestHandler(\n options: KubernetesProxyCreateRequestHandlerOptions,\n ): RequestHandler {\n const { permissionApi } = options;\n return async (req, res, next) => {\n const clusterNameHeader =\n req.headers[HEADER_KUBERNETES_CLUSTER.toLowerCase()];\n const clusterName =\n typeof clusterNameHeader === 'string' && clusterNameHeader.length > 0\n ? clusterNameHeader\n : 'unknown';\n const path = req.path || req.url.split('?')[0] || '';\n\n let clientDisconnected = false;\n const onSocketClose = () => {\n clientDisconnected = true;\n };\n req.socket.once('close', onSocketClose);\n\n let auditSession: ProxyAuditSession | undefined;\n let finishListenerAttached = false;\n try {\n auditSession = await ProxyAuditSession.start(this.auditor, {\n req,\n clusterName,\n method: req.method,\n path,\n logger: this.logger,\n });\n\n finishListenerAttached = await this.authorizeAndDispatch(\n req,\n res,\n next,\n permissionApi,\n auditSession,\n onSocketClose,\n () => clientDisconnected,\n );\n } catch (error) {\n req.socket.removeListener('close', onSocketClose);\n const err = error instanceof Error ? error : new Error(String(error));\n if (auditSession && !finishListenerAttached) {\n auditSession.finalize(err);\n }\n throw error;\n }\n };\n }\n\n /**\n * Authorizes the request, resolves the target cluster, and dispatches\n * to either the HTTP or WebSocket proxy path. Returns `true` when\n * HTTP finish listeners have been attached.\n */\n private async authorizeAndDispatch(\n req: Request,\n res: Response,\n next: NextFunction,\n permissionApi: PermissionsService,\n auditSession: ProxyAuditSession,\n onSocketClose: () => void,\n isClientDisconnected: () => boolean,\n ): Promise<boolean> {\n const authorizeResponse = await permissionApi.authorize(\n [{ permission: kubernetesProxyPermission }],\n { credentials: await this.httpAuth.credentials(req) },\n );\n\n if (authorizeResponse[0].result === AuthorizeResult.DENY) {\n req.socket.removeListener('close', onSocketClose);\n auditSession.finalize(new NotAllowedError('Unauthorized'));\n res.status(403).json({\n error: serializeError(new NotAllowedError('Unauthorized')),\n });\n return false;\n }\n\n if (isClientDisconnected()) {\n auditSession.finalize(\n new Error('Client disconnected before proxy dispatch'),\n );\n return false;\n }\n\n const prepared = await this.prepareProxyTarget(req);\n auditSession.setResolvedClusterName(prepared.cluster.name);\n (req as any)[PROXY_PREPARED_TARGET] = prepared;\n const middleware = this.getOrCreateMiddleware(prepared.cluster);\n\n if (isClientDisconnected()) {\n auditSession.finalize(\n new Error('Client disconnected before proxy dispatch'),\n );\n return false;\n }\n\n return KubernetesProxy.dispatchToProxy(\n req,\n res,\n next,\n middleware,\n auditSession,\n onSocketClose,\n );\n }\n\n /**\n * Resolves the cluster, credentials, and target URL for a proxy request\n * before dispatching to the middleware. This ensures all async preparation\n * completes under the caller's try/catch, avoiding unhandled rejections\n * from http-proxy-middleware's fire-and-forget upgrade() path.\n */\n private async prepareProxyTarget(req: Request): Promise<PreparedProxyTarget> {\n const cluster = await this.getClusterForRequest(req);\n const url = new URL(cluster.url);\n\n const { bufferFromFileOrString } = await import('@kubernetes/client-node');\n\n const target: any = {\n protocol: url.protocol,\n host: url.hostname,\n port: url.port,\n ca: bufferFromFileOrString(cluster.caFile, cluster.caData)?.toString(),\n };\n\n const authHeader =\n req.headers[HEADER_KUBERNETES_AUTH.toLocaleLowerCase('en-US')];\n if (typeof authHeader === 'string') {\n req.headers.authorization = authHeader;\n } else {\n const authObj = KubernetesProxy.authHeadersToKubernetesRequestAuth(\n req.headers,\n );\n\n const credential = await this.authStrategy.getCredential(\n cluster,\n authObj,\n );\n\n if (credential.type === 'bearer token') {\n req.headers.authorization = `Bearer ${credential.token}`;\n } else if (credential.type === 'x509 client certificate') {\n target.key = credential.key;\n target.cert = credential.cert;\n }\n }\n\n const requestPath = req.originalUrl || req.url || '';\n const rewrittenPath = requestPath.replace(\n new RegExp(`^${req.baseUrl || ''}`),\n url.pathname || '',\n );\n\n return { proxyTarget: target, rewrittenPath, cluster };\n }\n\n // We create one middleware per remote cluster and hold on to them, because\n // the secure property isn't possible to decide on a per-request basis with a\n // single middleware instance - and we don't expect it to change over time.\n // Target resolution and path rewriting are handled per-request via the\n // PROXY_PREPARED_TARGET symbol stashed on each req by prepareProxyTarget.\n private getOrCreateMiddleware(cluster: ClusterDetails): RequestHandler {\n let middleware = this.middlewareForClusterName.get(cluster.name);\n if (!middleware) {\n const logger = this.logger.child({ cluster: cluster.name });\n middleware = createProxyMiddleware({\n logProvider: () => ({\n log: logger.info.bind(logger),\n debug: logger.debug.bind(logger),\n info: logger.info.bind(logger),\n warn: logger.warn.bind(logger),\n error: logger.error.bind(logger),\n }),\n // ws must be false to prevent http-proxy-middleware from auto-subscribing\n // to the server's 'upgrade' event, which would bypass Express routing and\n // skip audit event creation for WebSocket requests.\n ws: false,\n secure: !cluster.skipTLSVerify,\n changeOrigin: true,\n router: req => {\n return (req as any)[PROXY_PREPARED_TARGET]?.proxyTarget;\n },\n pathRewrite: (_path, req) => {\n return (req as any)[PROXY_PREPARED_TARGET]?.rewrittenPath ?? _path;\n },\n onError: (error, req, res) => {\n const wrappedError = new ForwardedError(\n `Cluster '${cluster.name}' request error`,\n error,\n );\n\n logger.error('Kubernetes proxy error', wrappedError);\n\n if (typeof (res as { status?: unknown }).status !== 'function') {\n return;\n }\n\n const body: ErrorResponseBody = {\n error: serializeError(wrappedError, {\n includeStack: process.env.NODE_ENV === 'development',\n }),\n request: { method: req.method, url: req.originalUrl },\n response: { statusCode: 500 },\n };\n res.status(500).json(body);\n },\n onProxyReqWs: ProxyAuditSession.handleWebSocketProxyReq,\n });\n this.middlewareForClusterName.set(cluster.name, middleware);\n }\n return middleware;\n }\n\n /**\n * Branches to WebSocket upgrade or HTTP middleware dispatch and wires\n * up the appropriate audit listeners. Returns `true` when HTTP finish\n * listeners have been attached (used by the caller's catch block to\n * avoid double-finalizing).\n */\n private static dispatchToProxy(\n req: Request,\n res: Response,\n next: NextFunction,\n middleware: RequestHandler,\n auditSession: ProxyAuditSession,\n onSocketClose: () => void,\n ): boolean {\n const isWebSocketUpgrade =\n (req.header('connection') ?? '')\n .toLowerCase()\n .split(',')\n .some(t => t.trim() === 'upgrade') &&\n req.header('upgrade')?.toLowerCase() === 'websocket';\n\n if (isWebSocketUpgrade) {\n auditSession.prepareForWebSocketUpgrade(req, onSocketClose);\n\n try {\n middleware.upgrade!(req, req.socket, undefined);\n } catch (error) {\n req.socket.removeListener('close', onSocketClose);\n auditSession.finalize(\n error instanceof Error ? error : new Error(String(error)),\n );\n throw error;\n }\n return false;\n }\n\n // HTTP path: remove the early close listener; HTTP audit listeners\n // on the response take over from here.\n req.socket.removeListener('close', onSocketClose);\n auditSession.attachToHttpResponse(res);\n middleware(req, res, next);\n return true;\n }\n\n private async getClusterForRequest(req: Request): Promise<ClusterDetails> {\n const { KubeConfig } = await import('@kubernetes/client-node');\n\n const clusterName = req.headers[HEADER_KUBERNETES_CLUSTER.toLowerCase()];\n const clusters = await this.clusterSupplier.getClusters({\n credentials: await this.httpAuth.credentials(req),\n });\n\n if (!clusters || clusters.length <= 0) {\n throw new NotFoundError(`No Clusters configured`);\n }\n\n const hasClusterNameHeader =\n typeof clusterName === 'string' && clusterName.length > 0;\n\n let cluster: ClusterDetails | undefined;\n\n if (hasClusterNameHeader) {\n cluster = clusters.find(c => c.name === clusterName);\n } else if (clusters.length === 1) {\n cluster = clusters.at(0);\n }\n\n if (!cluster) {\n throw new NotFoundError(`Cluster '${clusterName}' not found`);\n }\n\n const authProvider =\n cluster.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER];\n\n if (\n authProvider === 'serviceAccount' &&\n fs.pathExistsSync(SERVICEACCOUNT_CA_PATH) &&\n !cluster.authMetadata.serviceAccountToken\n ) {\n const kc = new KubeConfig();\n kc.loadFromCluster();\n const clusterFromKubeConfig = kc.getCurrentCluster() as Cluster;\n\n const url = new URL(clusterFromKubeConfig.server);\n cluster.url = clusterFromKubeConfig.server;\n if (url.protocol === 'https:') {\n cluster.caFile = clusterFromKubeConfig.caFile;\n }\n }\n\n return cluster;\n }\n\n private static authHeadersToKubernetesRequestAuth(\n originalHeaders: IncomingHttpHeaders,\n ): KubernetesRequestAuth {\n return Object.keys(originalHeaders)\n .filter(header => header.startsWith('backstage-kubernetes-authorization'))\n .map(header =>\n KubernetesProxy.headerToDictionary(header, originalHeaders),\n )\n .filter(headerAsDic => Object.keys(headerAsDic).length !== 0)\n .reduce(KubernetesProxy.combineHeaders, {});\n }\n\n private static headerToDictionary(\n header: string,\n originalHeaders: IncomingHttpHeaders,\n ): KubernetesRequestAuth {\n const obj: KubernetesRequestAuth = {};\n const headerSplit = header.split('-');\n if (headerSplit.length >= 4) {\n const framework = headerSplit[3].toLowerCase();\n if (headerSplit.length >= 5) {\n const provider = headerSplit.slice(4).join('-').toLowerCase();\n obj[framework] = { [provider]: originalHeaders[header] };\n } else {\n obj[framework] = originalHeaders[header];\n }\n }\n return obj;\n }\n\n private static combineHeaders(\n authObj: any,\n header: any,\n ): KubernetesRequestAuth {\n const framework = Object.keys(header)[0];\n\n if (authObj[framework]) {\n authObj[framework] = {\n ...authObj[framework],\n ...header[framework],\n };\n } else {\n authObj[framework] = header[framework];\n }\n\n return authObj;\n }\n}\n"],"names":["ProxyAuditSession","kubernetesProxyPermission","AuthorizeResult","NotAllowedError","serializeError","createProxyMiddleware","ForwardedError","NotFoundError","ANNOTATION_KUBERNETES_AUTH_PROVIDER","fs","SERVICEACCOUNT_CA_PATH"],"mappings":"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"}
|
|
@@ -12,9 +12,11 @@ require('@azure/identity');
|
|
|
12
12
|
require('@google-cloud/container');
|
|
13
13
|
var DispatchStrategy = require('../auth/DispatchStrategy.cjs.js');
|
|
14
14
|
require('fs-extra');
|
|
15
|
+
var errors = require('@backstage/errors');
|
|
15
16
|
var resourcesRoutes = require('../routes/resourcesRoutes.cjs.js');
|
|
16
17
|
var KubernetesProxy = require('./KubernetesProxy.cjs.js');
|
|
17
18
|
var requirePermission = require('../auth/requirePermission.cjs.js');
|
|
19
|
+
var catalogModel = require('@backstage/catalog-model');
|
|
18
20
|
|
|
19
21
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
20
22
|
|
|
@@ -91,11 +93,13 @@ class KubernetesRouter {
|
|
|
91
93
|
clusterSupplier,
|
|
92
94
|
authStrategy,
|
|
93
95
|
discovery,
|
|
94
|
-
httpAuth
|
|
96
|
+
httpAuth,
|
|
97
|
+
auditor: this.env.auditor
|
|
95
98
|
});
|
|
96
99
|
}
|
|
97
100
|
buildDefaultRouter(objectsProvider, clusterSupplier, catalog, proxy, permissionApi, httpAuth, authStrategyMap) {
|
|
98
101
|
const logger = this.env.logger;
|
|
102
|
+
const auditor = this.env.auditor;
|
|
99
103
|
const router = Router__default.default();
|
|
100
104
|
router.use("/proxy", proxy.createRequestHandler({ permissionApi }));
|
|
101
105
|
router.use(express__default.default.json());
|
|
@@ -105,67 +109,112 @@ class KubernetesRouter {
|
|
|
105
109
|
})
|
|
106
110
|
);
|
|
107
111
|
router.post("/services/:serviceId", async (req, res) => {
|
|
108
|
-
await requirePermission.requirePermission(
|
|
109
|
-
permissionApi,
|
|
110
|
-
pluginKubernetesCommon.kubernetesResourcesReadPermission,
|
|
111
|
-
httpAuth,
|
|
112
|
-
req
|
|
113
|
-
);
|
|
114
112
|
const serviceId = req.params.serviceId;
|
|
115
113
|
const requestBody = req.body;
|
|
114
|
+
let entityRef;
|
|
115
|
+
if (requestBody?.entity) {
|
|
116
|
+
try {
|
|
117
|
+
entityRef = catalogModel.stringifyEntityRef(requestBody.entity);
|
|
118
|
+
} catch {
|
|
119
|
+
entityRef = void 0;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const auditorEvent = await auditor.createEvent({
|
|
123
|
+
eventId: "resource-fetch",
|
|
124
|
+
request: req,
|
|
125
|
+
meta: { queryType: "services", entityRef, serviceId }
|
|
126
|
+
});
|
|
116
127
|
try {
|
|
128
|
+
await requirePermission.requirePermission(
|
|
129
|
+
permissionApi,
|
|
130
|
+
pluginKubernetesCommon.kubernetesResourcesReadPermission,
|
|
131
|
+
httpAuth,
|
|
132
|
+
req
|
|
133
|
+
);
|
|
117
134
|
const response = await objectsProvider.getKubernetesObjectsByEntity(
|
|
118
135
|
{
|
|
119
|
-
entity: requestBody
|
|
120
|
-
auth: requestBody
|
|
136
|
+
entity: requestBody?.entity,
|
|
137
|
+
auth: requestBody?.auth || {}
|
|
121
138
|
},
|
|
122
139
|
{ credentials: await httpAuth.credentials(req) }
|
|
123
140
|
);
|
|
124
141
|
res.json(response);
|
|
142
|
+
auditorEvent.success().catch(
|
|
143
|
+
(error) => logger.error(
|
|
144
|
+
"Failed to emit audit event resource-fetch (services)",
|
|
145
|
+
error
|
|
146
|
+
)
|
|
147
|
+
);
|
|
125
148
|
} catch (e) {
|
|
149
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
150
|
+
await auditorEvent.fail({ error: err });
|
|
151
|
+
if (e instanceof errors.NotAllowedError) {
|
|
152
|
+
throw e;
|
|
153
|
+
}
|
|
126
154
|
logger.error(
|
|
127
|
-
`action=retrieveObjectsByServiceId service=${serviceId}, error
|
|
155
|
+
`action=retrieveObjectsByServiceId service=${serviceId}, error: ${err}`
|
|
128
156
|
);
|
|
129
|
-
res.
|
|
157
|
+
if (!res.headersSent) {
|
|
158
|
+
res.status(500).json({ error: err.message });
|
|
159
|
+
}
|
|
130
160
|
}
|
|
131
161
|
});
|
|
132
162
|
router.get("/clusters", async (req, res) => {
|
|
133
|
-
await
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
req
|
|
138
|
-
);
|
|
139
|
-
const credentials = await httpAuth.credentials(req);
|
|
140
|
-
const clusterDetails = await this.fetchClusterDetails(clusterSupplier, {
|
|
141
|
-
credentials
|
|
142
|
-
});
|
|
143
|
-
res.json({
|
|
144
|
-
items: clusterDetails.map((cd) => {
|
|
145
|
-
const oidcTokenProvider = cd.authMetadata[pluginKubernetesCommon.ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER];
|
|
146
|
-
const authProvider = cd.authMetadata[pluginKubernetesCommon.ANNOTATION_KUBERNETES_AUTH_PROVIDER];
|
|
147
|
-
const strategy = authStrategyMap[authProvider];
|
|
148
|
-
let auth = {};
|
|
149
|
-
if (strategy) {
|
|
150
|
-
auth = strategy.presentAuthMetadata(cd.authMetadata);
|
|
151
|
-
}
|
|
152
|
-
return {
|
|
153
|
-
name: cd.name,
|
|
154
|
-
title: cd.title,
|
|
155
|
-
dashboardUrl: cd.dashboardUrl,
|
|
156
|
-
authProvider,
|
|
157
|
-
...oidcTokenProvider && { oidcTokenProvider },
|
|
158
|
-
...auth && Object.keys(auth).length !== 0 && { auth }
|
|
159
|
-
};
|
|
160
|
-
})
|
|
163
|
+
const auditorEvent = await auditor.createEvent({
|
|
164
|
+
eventId: "cluster-fetch",
|
|
165
|
+
request: req,
|
|
166
|
+
meta: { queryType: "list" }
|
|
161
167
|
});
|
|
168
|
+
try {
|
|
169
|
+
await requirePermission.requirePermission(
|
|
170
|
+
permissionApi,
|
|
171
|
+
pluginKubernetesCommon.kubernetesClustersReadPermission,
|
|
172
|
+
httpAuth,
|
|
173
|
+
req
|
|
174
|
+
);
|
|
175
|
+
const credentials = await httpAuth.credentials(req);
|
|
176
|
+
const clusterDetails = await this.fetchClusterDetails(clusterSupplier, {
|
|
177
|
+
credentials
|
|
178
|
+
});
|
|
179
|
+
res.json({
|
|
180
|
+
items: clusterDetails.map((cd) => {
|
|
181
|
+
const oidcTokenProvider = cd.authMetadata[pluginKubernetesCommon.ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER];
|
|
182
|
+
const authProvider = cd.authMetadata[pluginKubernetesCommon.ANNOTATION_KUBERNETES_AUTH_PROVIDER];
|
|
183
|
+
const strategy = authStrategyMap[authProvider];
|
|
184
|
+
let auth = {};
|
|
185
|
+
if (strategy) {
|
|
186
|
+
auth = strategy.presentAuthMetadata(cd.authMetadata);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
name: cd.name,
|
|
190
|
+
title: cd.title,
|
|
191
|
+
dashboardUrl: cd.dashboardUrl,
|
|
192
|
+
authProvider,
|
|
193
|
+
...oidcTokenProvider && { oidcTokenProvider },
|
|
194
|
+
...auth && Object.keys(auth).length !== 0 && { auth }
|
|
195
|
+
};
|
|
196
|
+
})
|
|
197
|
+
});
|
|
198
|
+
auditorEvent.success().catch(
|
|
199
|
+
(error) => logger.error(
|
|
200
|
+
"Failed to emit audit event cluster-fetch (list)",
|
|
201
|
+
error
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
206
|
+
await auditorEvent.fail({ error: err });
|
|
207
|
+
throw e;
|
|
208
|
+
}
|
|
162
209
|
});
|
|
163
210
|
resourcesRoutes.addResourceRoutesToRouter(
|
|
164
211
|
router,
|
|
165
212
|
catalog,
|
|
166
213
|
objectsProvider,
|
|
167
214
|
httpAuth,
|
|
168
|
-
permissionApi
|
|
215
|
+
permissionApi,
|
|
216
|
+
auditor,
|
|
217
|
+
logger
|
|
169
218
|
);
|
|
170
219
|
return router;
|
|
171
220
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KubernetesRouter.cjs.js","sources":["../../src/service/KubernetesRouter.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\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 */\nimport { Config } from '@backstage/config';\nimport {\n ANNOTATION_KUBERNETES_AUTH_PROVIDER,\n ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER,\n kubernetesClustersReadPermission,\n kubernetesPermissions,\n kubernetesResourcesReadPermission,\n} from '@backstage/plugin-kubernetes-common';\nimport { PermissionEvaluator } from '@backstage/plugin-permission-common';\nimport { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';\nimport express from 'express';\nimport Router from 'express-promise-router';\n\nimport { DispatchStrategy } from '../auth';\n\nimport {\n AuthService,\n BackstageCredentials,\n DiscoveryService,\n HttpAuthService,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport {\n AuthenticationStrategy,\n AuthMetadata,\n KubernetesClustersSupplier,\n KubernetesFetcher,\n KubernetesObjectsProvider,\n KubernetesRouterFactory,\n KubernetesServiceLocator,\n} from '@backstage/plugin-kubernetes-node';\nimport { addResourceRoutesToRouter } from '../routes/resourcesRoutes';\nimport { ObjectsByEntityRequest } from '../types/types';\nimport { KubernetesProxy } from './KubernetesProxy';\nimport { requirePermission } from '../auth/requirePermission';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\n\nexport interface KubernetesEnvironment {\n logger: LoggerService;\n config: Config;\n catalog: CatalogService;\n discovery: DiscoveryService;\n permissions: PermissionEvaluator;\n auth: AuthService;\n httpAuth: HttpAuthService;\n authStrategyMap: { [key: string]: AuthenticationStrategy };\n fetcher: KubernetesFetcher;\n clusterSupplier: KubernetesClustersSupplier;\n serviceLocator: KubernetesServiceLocator;\n objectsProvider: KubernetesObjectsProvider;\n customRouter?: KubernetesRouterFactory;\n}\n\nexport class KubernetesRouter {\n static create(env: KubernetesEnvironment) {\n return new KubernetesRouter(env);\n }\n\n protected readonly env: KubernetesEnvironment;\n\n constructor(env: KubernetesEnvironment) {\n this.env = env;\n }\n\n public async getRouter() {\n const {\n logger,\n config,\n permissions,\n authStrategyMap,\n clusterSupplier,\n objectsProvider,\n catalog,\n discovery,\n httpAuth,\n customRouter,\n } = this.env;\n\n logger.info('Initializing Kubernetes backend');\n\n if (!config.has('kubernetes')) {\n if (process.env.NODE_ENV !== 'development') {\n throw new Error('Kubernetes configuration is missing');\n }\n logger.warn(\n 'Failed to initialize kubernetes backend: kubernetes config is missing',\n );\n return Router();\n }\n\n const proxy = this.buildProxy(\n logger,\n clusterSupplier,\n discovery,\n httpAuth,\n authStrategyMap,\n );\n\n return (\n customRouter?.({\n getDefault: () =>\n this.buildDefaultRouter(\n objectsProvider,\n clusterSupplier,\n catalog,\n proxy,\n permissions,\n httpAuth,\n authStrategyMap,\n ),\n objectsProvider,\n clusterSupplier,\n authStrategyMap,\n }) ??\n this.buildDefaultRouter(\n objectsProvider,\n clusterSupplier,\n catalog,\n proxy,\n permissions,\n httpAuth,\n authStrategyMap,\n )\n );\n }\n\n private buildProxy(\n logger: LoggerService,\n clusterSupplier: KubernetesClustersSupplier,\n discovery: DiscoveryService,\n httpAuth: HttpAuthService,\n authStrategyMap: { [key: string]: AuthenticationStrategy },\n ): KubernetesProxy {\n const authStrategy = new DispatchStrategy({\n authStrategyMap,\n });\n return new KubernetesProxy({\n logger,\n clusterSupplier,\n authStrategy,\n discovery,\n httpAuth,\n });\n }\n\n private buildDefaultRouter(\n objectsProvider: KubernetesObjectsProvider,\n clusterSupplier: KubernetesClustersSupplier,\n catalog: CatalogService,\n proxy: KubernetesProxy,\n permissionApi: PermissionEvaluator,\n httpAuth: HttpAuthService,\n authStrategyMap: { [key: string]: AuthenticationStrategy },\n ): express.Router {\n const logger = this.env.logger;\n const router = Router();\n router.use('/proxy', proxy.createRequestHandler({ permissionApi }));\n router.use(express.json());\n router.use(\n createPermissionIntegrationRouter({\n permissions: kubernetesPermissions,\n }),\n );\n\n // @deprecated\n router.post('/services/:serviceId', async (req, res) => {\n await requirePermission(\n permissionApi,\n kubernetesResourcesReadPermission,\n httpAuth,\n req,\n );\n const serviceId = req.params.serviceId;\n const requestBody: ObjectsByEntityRequest = req.body;\n try {\n const response = await objectsProvider.getKubernetesObjectsByEntity(\n {\n entity: requestBody.entity,\n auth: requestBody.auth || {},\n },\n { credentials: await httpAuth.credentials(req) },\n );\n res.json(response);\n } catch (e) {\n logger.error(\n `action=retrieveObjectsByServiceId service=${serviceId}, error=${e}`,\n );\n res.status(500).json({ error: e.message });\n }\n });\n\n router.get('/clusters', async (req, res) => {\n await requirePermission(\n permissionApi,\n kubernetesClustersReadPermission,\n httpAuth,\n req,\n );\n const credentials = await httpAuth.credentials(req);\n const clusterDetails = await this.fetchClusterDetails(clusterSupplier, {\n credentials,\n });\n res.json({\n items: clusterDetails.map(cd => {\n const oidcTokenProvider =\n cd.authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER];\n const authProvider =\n cd.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER];\n const strategy = authStrategyMap[authProvider];\n let auth: AuthMetadata = {};\n if (strategy) {\n auth = strategy.presentAuthMetadata(cd.authMetadata);\n }\n\n return {\n name: cd.name,\n title: cd.title,\n dashboardUrl: cd.dashboardUrl,\n authProvider,\n ...(oidcTokenProvider && { oidcTokenProvider }),\n ...(auth && Object.keys(auth).length !== 0 && { auth }),\n };\n }),\n });\n });\n\n addResourceRoutesToRouter(\n router,\n catalog,\n objectsProvider,\n httpAuth,\n permissionApi,\n );\n\n return router;\n }\n\n private async fetchClusterDetails(\n clusterSupplier: KubernetesClustersSupplier,\n options: { credentials: BackstageCredentials },\n ) {\n const clusterDetails = await clusterSupplier.getClusters(options);\n\n this.env.logger.debug(\n `action=loadClusterDetails numOfClustersLoaded=${clusterDetails.length}`,\n );\n\n return clusterDetails;\n }\n}\n"],"names":["Router","DispatchStrategy","KubernetesProxy","express","createPermissionIntegrationRouter","kubernetesPermissions","requirePermission","kubernetesResourcesReadPermission","kubernetesClustersReadPermission","ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER","ANNOTATION_KUBERNETES_AUTH_PROVIDER","addResourceRoutesToRouter"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"KubernetesRouter.cjs.js","sources":["../../src/service/KubernetesRouter.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\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 */\nimport { Config } from '@backstage/config';\nimport {\n ANNOTATION_KUBERNETES_AUTH_PROVIDER,\n ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER,\n kubernetesClustersReadPermission,\n kubernetesPermissions,\n kubernetesResourcesReadPermission,\n} from '@backstage/plugin-kubernetes-common';\nimport { PermissionEvaluator } from '@backstage/plugin-permission-common';\nimport { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';\nimport express from 'express';\nimport Router from 'express-promise-router';\n\nimport { DispatchStrategy } from '../auth';\nimport { NotAllowedError } from '@backstage/errors';\n\nimport {\n AuthService,\n AuditorService,\n BackstageCredentials,\n DiscoveryService,\n HttpAuthService,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport {\n AuthenticationStrategy,\n AuthMetadata,\n KubernetesClustersSupplier,\n KubernetesFetcher,\n KubernetesObjectsProvider,\n KubernetesRouterFactory,\n KubernetesServiceLocator,\n} from '@backstage/plugin-kubernetes-node';\nimport { addResourceRoutesToRouter } from '../routes/resourcesRoutes';\nimport { ObjectsByEntityRequest } from '../types/types';\nimport { KubernetesProxy } from './KubernetesProxy';\nimport { requirePermission } from '../auth/requirePermission';\nimport { CatalogService } from '@backstage/plugin-catalog-node';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\n\nexport interface KubernetesEnvironment {\n logger: LoggerService;\n config: Config;\n catalog: CatalogService;\n discovery: DiscoveryService;\n permissions: PermissionEvaluator;\n auth: AuthService;\n httpAuth: HttpAuthService;\n auditor: AuditorService;\n authStrategyMap: { [key: string]: AuthenticationStrategy };\n fetcher: KubernetesFetcher;\n clusterSupplier: KubernetesClustersSupplier;\n serviceLocator: KubernetesServiceLocator;\n objectsProvider: KubernetesObjectsProvider;\n customRouter?: KubernetesRouterFactory;\n}\n\nexport class KubernetesRouter {\n static create(env: KubernetesEnvironment) {\n return new KubernetesRouter(env);\n }\n\n protected readonly env: KubernetesEnvironment;\n\n constructor(env: KubernetesEnvironment) {\n this.env = env;\n }\n\n public async getRouter() {\n const {\n logger,\n config,\n permissions,\n authStrategyMap,\n clusterSupplier,\n objectsProvider,\n catalog,\n discovery,\n httpAuth,\n customRouter,\n } = this.env;\n\n logger.info('Initializing Kubernetes backend');\n\n if (!config.has('kubernetes')) {\n if (process.env.NODE_ENV !== 'development') {\n throw new Error('Kubernetes configuration is missing');\n }\n logger.warn(\n 'Failed to initialize kubernetes backend: kubernetes config is missing',\n );\n return Router();\n }\n\n const proxy = this.buildProxy(\n logger,\n clusterSupplier,\n discovery,\n httpAuth,\n authStrategyMap,\n );\n\n return (\n customRouter?.({\n getDefault: () =>\n this.buildDefaultRouter(\n objectsProvider,\n clusterSupplier,\n catalog,\n proxy,\n permissions,\n httpAuth,\n authStrategyMap,\n ),\n objectsProvider,\n clusterSupplier,\n authStrategyMap,\n }) ??\n this.buildDefaultRouter(\n objectsProvider,\n clusterSupplier,\n catalog,\n proxy,\n permissions,\n httpAuth,\n authStrategyMap,\n )\n );\n }\n\n private buildProxy(\n logger: LoggerService,\n clusterSupplier: KubernetesClustersSupplier,\n discovery: DiscoveryService,\n httpAuth: HttpAuthService,\n authStrategyMap: { [key: string]: AuthenticationStrategy },\n ): KubernetesProxy {\n const authStrategy = new DispatchStrategy({\n authStrategyMap,\n });\n return new KubernetesProxy({\n logger,\n clusterSupplier,\n authStrategy,\n discovery,\n httpAuth,\n auditor: this.env.auditor,\n });\n }\n\n private buildDefaultRouter(\n objectsProvider: KubernetesObjectsProvider,\n clusterSupplier: KubernetesClustersSupplier,\n catalog: CatalogService,\n proxy: KubernetesProxy,\n permissionApi: PermissionEvaluator,\n httpAuth: HttpAuthService,\n authStrategyMap: { [key: string]: AuthenticationStrategy },\n ): express.Router {\n const logger = this.env.logger;\n const auditor = this.env.auditor;\n const router = Router();\n router.use('/proxy', proxy.createRequestHandler({ permissionApi }));\n router.use(express.json());\n router.use(\n createPermissionIntegrationRouter({\n permissions: kubernetesPermissions,\n }),\n );\n\n // @deprecated\n router.post('/services/:serviceId', async (req, res) => {\n const serviceId = req.params.serviceId;\n const requestBody: ObjectsByEntityRequest = req.body;\n let entityRef: string | undefined;\n if (requestBody?.entity) {\n try {\n entityRef = stringifyEntityRef(requestBody.entity);\n } catch {\n entityRef = undefined;\n }\n }\n\n const auditorEvent = await auditor.createEvent({\n eventId: 'resource-fetch',\n request: req,\n meta: { queryType: 'services', entityRef, serviceId },\n });\n\n try {\n await requirePermission(\n permissionApi,\n kubernetesResourcesReadPermission,\n httpAuth,\n req,\n );\n const response = await objectsProvider.getKubernetesObjectsByEntity(\n {\n entity: requestBody?.entity,\n auth: requestBody?.auth || {},\n },\n { credentials: await httpAuth.credentials(req) },\n );\n res.json(response);\n auditorEvent\n .success()\n .catch(error =>\n logger.error(\n 'Failed to emit audit event resource-fetch (services)',\n error,\n ),\n );\n } catch (e) {\n const err = e instanceof Error ? e : new Error(String(e));\n await auditorEvent.fail({ error: err });\n if (e instanceof NotAllowedError) {\n throw e;\n }\n logger.error(\n `action=retrieveObjectsByServiceId service=${serviceId}, error: ${err}`,\n );\n if (!res.headersSent) {\n res.status(500).json({ error: err.message });\n }\n }\n });\n\n router.get('/clusters', async (req, res) => {\n const auditorEvent = await auditor.createEvent({\n eventId: 'cluster-fetch',\n request: req,\n meta: { queryType: 'list' },\n });\n\n try {\n await requirePermission(\n permissionApi,\n kubernetesClustersReadPermission,\n httpAuth,\n req,\n );\n const credentials = await httpAuth.credentials(req);\n const clusterDetails = await this.fetchClusterDetails(clusterSupplier, {\n credentials,\n });\n res.json({\n items: clusterDetails.map(cd => {\n const oidcTokenProvider =\n cd.authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER];\n const authProvider =\n cd.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER];\n const strategy = authStrategyMap[authProvider];\n let auth: AuthMetadata = {};\n if (strategy) {\n auth = strategy.presentAuthMetadata(cd.authMetadata);\n }\n\n return {\n name: cd.name,\n title: cd.title,\n dashboardUrl: cd.dashboardUrl,\n authProvider,\n ...(oidcTokenProvider && { oidcTokenProvider }),\n ...(auth && Object.keys(auth).length !== 0 && { auth }),\n };\n }),\n });\n auditorEvent\n .success()\n .catch(error =>\n logger.error(\n 'Failed to emit audit event cluster-fetch (list)',\n error,\n ),\n );\n } catch (e) {\n const err = e instanceof Error ? e : new Error(String(e));\n await auditorEvent.fail({ error: err });\n throw e;\n }\n });\n\n addResourceRoutesToRouter(\n router,\n catalog,\n objectsProvider,\n httpAuth,\n permissionApi,\n auditor,\n logger,\n );\n\n return router;\n }\n\n private async fetchClusterDetails(\n clusterSupplier: KubernetesClustersSupplier,\n options: { credentials: BackstageCredentials },\n ) {\n const clusterDetails = await clusterSupplier.getClusters(options);\n\n this.env.logger.debug(\n `action=loadClusterDetails numOfClustersLoaded=${clusterDetails.length}`,\n );\n\n return clusterDetails;\n }\n}\n"],"names":["Router","DispatchStrategy","KubernetesProxy","express","createPermissionIntegrationRouter","kubernetesPermissions","stringifyEntityRef","requirePermission","kubernetesResourcesReadPermission","NotAllowedError","kubernetesClustersReadPermission","ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER","ANNOTATION_KUBERNETES_AUTH_PROVIDER","addResourceRoutesToRouter"],"mappings":"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"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PROXY_WEBSOCKET_AUDIT = /* @__PURE__ */ Symbol("kubernetesProxyWebSocketAudit");
|
|
4
|
+
class ProxyAuditSession {
|
|
5
|
+
constructor(event, initialClusterName, logger) {
|
|
6
|
+
this.event = event;
|
|
7
|
+
this.initialClusterName = initialClusterName;
|
|
8
|
+
this.logger = logger;
|
|
9
|
+
}
|
|
10
|
+
resolvedClusterName;
|
|
11
|
+
finalized = false;
|
|
12
|
+
static async start(auditor, options) {
|
|
13
|
+
if (!auditor) {
|
|
14
|
+
return new ProxyAuditSession(
|
|
15
|
+
void 0,
|
|
16
|
+
options.clusterName,
|
|
17
|
+
options.logger
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const sanitizedUrl = (options.req.originalUrl ?? options.req.url).split(
|
|
21
|
+
"?"
|
|
22
|
+
)[0];
|
|
23
|
+
const sanitizedRequest = Object.create(options.req, {
|
|
24
|
+
originalUrl: { value: sanitizedUrl, enumerable: true },
|
|
25
|
+
url: { value: sanitizedUrl, enumerable: true }
|
|
26
|
+
});
|
|
27
|
+
const event = await auditor.createEvent({
|
|
28
|
+
eventId: "cluster-fetch",
|
|
29
|
+
severityLevel: "medium",
|
|
30
|
+
request: sanitizedRequest,
|
|
31
|
+
meta: {
|
|
32
|
+
queryType: "proxy",
|
|
33
|
+
clusterName: options.clusterName,
|
|
34
|
+
method: options.method,
|
|
35
|
+
path: options.path
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return new ProxyAuditSession(event, options.clusterName, options.logger);
|
|
39
|
+
}
|
|
40
|
+
setResolvedClusterName(name) {
|
|
41
|
+
this.resolvedClusterName = name;
|
|
42
|
+
}
|
|
43
|
+
finalize(error) {
|
|
44
|
+
if (this.finalized) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.finalized = true;
|
|
48
|
+
if (!this.event) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const terminalMeta = this.resolvedClusterName && this.resolvedClusterName !== this.initialClusterName ? { clusterName: this.resolvedClusterName } : void 0;
|
|
52
|
+
const audit = error ? this.event.fail({
|
|
53
|
+
error,
|
|
54
|
+
...terminalMeta && { meta: terminalMeta }
|
|
55
|
+
}) : this.event.success(terminalMeta ? { meta: terminalMeta } : void 0);
|
|
56
|
+
audit.catch(
|
|
57
|
+
(err) => this.logger.error("Failed to emit proxy audit event", err)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
attachToHttpResponse(res) {
|
|
61
|
+
const finalizeFromResponse = () => {
|
|
62
|
+
if (!res.writableFinished) {
|
|
63
|
+
this.finalize(
|
|
64
|
+
new Error("Client disconnected before response completed")
|
|
65
|
+
);
|
|
66
|
+
} else if (res.statusCode >= 400) {
|
|
67
|
+
this.finalize(
|
|
68
|
+
new Error(`Proxy responded with status ${res.statusCode}`)
|
|
69
|
+
);
|
|
70
|
+
} else {
|
|
71
|
+
this.finalize();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const finalizeOnce = () => {
|
|
75
|
+
res.removeListener("finish", finalizeOnce);
|
|
76
|
+
res.removeListener("close", finalizeOnce);
|
|
77
|
+
finalizeFromResponse();
|
|
78
|
+
};
|
|
79
|
+
res.once("finish", finalizeOnce);
|
|
80
|
+
res.once("close", finalizeOnce);
|
|
81
|
+
}
|
|
82
|
+
prepareForWebSocketUpgrade(req, onSocketClose) {
|
|
83
|
+
req[PROXY_WEBSOCKET_AUDIT] = { session: this, onSocketClose };
|
|
84
|
+
}
|
|
85
|
+
static handleWebSocketProxyReq(proxyReq, req, socket) {
|
|
86
|
+
const typedReq = req;
|
|
87
|
+
const context = typedReq[PROXY_WEBSOCKET_AUDIT];
|
|
88
|
+
if (!context) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
delete typedReq[PROXY_WEBSOCKET_AUDIT];
|
|
92
|
+
const { session, onSocketClose } = context;
|
|
93
|
+
const cleanups = [];
|
|
94
|
+
const finalize = (error) => {
|
|
95
|
+
session.finalize(error);
|
|
96
|
+
cleanups.forEach((fn) => fn());
|
|
97
|
+
req.socket.removeListener("close", onSocketClose);
|
|
98
|
+
};
|
|
99
|
+
const onUpgrade = () => finalize();
|
|
100
|
+
const onResponse = (proxyRes) => {
|
|
101
|
+
finalize(
|
|
102
|
+
new Error(
|
|
103
|
+
`Upstream rejected WebSocket upgrade with status ${proxyRes.statusCode}`
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
const onProxyError = (err) => finalize(err);
|
|
108
|
+
const onSocketError = (err) => finalize(err instanceof Error ? err : new Error(String(err)));
|
|
109
|
+
const onSocketClosed = () => finalize(
|
|
110
|
+
new Error("Client closed connection during WebSocket handshake")
|
|
111
|
+
);
|
|
112
|
+
proxyReq.once("upgrade", onUpgrade);
|
|
113
|
+
proxyReq.once("response", onResponse);
|
|
114
|
+
proxyReq.once("error", onProxyError);
|
|
115
|
+
socket.once("error", onSocketError);
|
|
116
|
+
socket.once("close", onSocketClosed);
|
|
117
|
+
cleanups.push(
|
|
118
|
+
() => proxyReq.removeListener("upgrade", onUpgrade),
|
|
119
|
+
() => proxyReq.removeListener("response", onResponse),
|
|
120
|
+
() => proxyReq.removeListener("error", onProxyError),
|
|
121
|
+
() => socket.removeListener("error", onSocketError),
|
|
122
|
+
() => socket.removeListener("close", onSocketClosed)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
exports.ProxyAuditSession = ProxyAuditSession;
|
|
128
|
+
//# sourceMappingURL=ProxyAuditSession.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProxyAuditSession.cjs.js","sources":["../../src/service/ProxyAuditSession.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\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 {\n AuditorService,\n AuditorServiceEvent,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport type { Request, Response } from 'express';\n\nconst PROXY_WEBSOCKET_AUDIT = Symbol('kubernetesProxyWebSocketAudit');\n\ntype WebSocketAuditContext = {\n session: ProxyAuditSession;\n onSocketClose: () => void;\n};\n\n/**\n * Tracks a single proxy request through audit event creation and finalization.\n * When no auditor is configured every method is a no-op.\n */\nexport class ProxyAuditSession {\n private resolvedClusterName?: string;\n private finalized = false;\n\n private constructor(\n private readonly event: AuditorServiceEvent | undefined,\n private readonly initialClusterName: string,\n private readonly logger: LoggerService,\n ) {}\n\n static async start(\n auditor: AuditorService | undefined,\n options: {\n req: Request;\n clusterName: string;\n method: string | undefined;\n path: string;\n logger: LoggerService;\n },\n ): Promise<ProxyAuditSession> {\n if (!auditor) {\n return new ProxyAuditSession(\n undefined,\n options.clusterName,\n options.logger,\n );\n }\n\n const sanitizedUrl = (options.req.originalUrl ?? options.req.url).split(\n '?',\n )[0];\n const sanitizedRequest = Object.create(options.req, {\n originalUrl: { value: sanitizedUrl, enumerable: true },\n url: { value: sanitizedUrl, enumerable: true },\n });\n\n const event = await auditor.createEvent({\n eventId: 'cluster-fetch',\n severityLevel: 'medium',\n request: sanitizedRequest,\n meta: {\n queryType: 'proxy',\n clusterName: options.clusterName,\n method: options.method,\n path: options.path,\n },\n });\n\n return new ProxyAuditSession(event, options.clusterName, options.logger);\n }\n\n setResolvedClusterName(name: string): void {\n this.resolvedClusterName = name;\n }\n\n finalize(error?: Error): void {\n if (this.finalized) {\n return;\n }\n this.finalized = true;\n\n if (!this.event) {\n return;\n }\n\n const terminalMeta =\n this.resolvedClusterName &&\n this.resolvedClusterName !== this.initialClusterName\n ? { clusterName: this.resolvedClusterName }\n : undefined;\n\n const audit = error\n ? this.event.fail({\n error,\n ...(terminalMeta && { meta: terminalMeta }),\n })\n : this.event.success(terminalMeta ? { meta: terminalMeta } : undefined);\n\n audit.catch(err =>\n this.logger.error('Failed to emit proxy audit event', err),\n );\n }\n\n attachToHttpResponse(res: Response): void {\n const finalizeFromResponse = () => {\n if (!res.writableFinished) {\n this.finalize(\n new Error('Client disconnected before response completed'),\n );\n } else if (res.statusCode >= 400) {\n this.finalize(\n new Error(`Proxy responded with status ${res.statusCode}`),\n );\n } else {\n this.finalize();\n }\n };\n\n const finalizeOnce = () => {\n res.removeListener('finish', finalizeOnce);\n res.removeListener('close', finalizeOnce);\n finalizeFromResponse();\n };\n\n res.once('finish', finalizeOnce);\n res.once('close', finalizeOnce);\n }\n\n prepareForWebSocketUpgrade(req: Request, onSocketClose: () => void): void {\n (req as Request & { [PROXY_WEBSOCKET_AUDIT]?: WebSocketAuditContext })[\n PROXY_WEBSOCKET_AUDIT\n ] = { session: this, onSocketClose };\n }\n\n static handleWebSocketProxyReq(\n proxyReq: import('http').ClientRequest,\n req: import('http').IncomingMessage,\n socket: import('net').Socket,\n ): void {\n const typedReq = req as Request & {\n [PROXY_WEBSOCKET_AUDIT]?: WebSocketAuditContext;\n };\n const context = typedReq[PROXY_WEBSOCKET_AUDIT];\n if (!context) {\n return;\n }\n delete typedReq[PROXY_WEBSOCKET_AUDIT];\n\n const { session, onSocketClose } = context;\n const cleanups: (() => void)[] = [];\n const finalize = (error?: Error) => {\n session.finalize(error);\n cleanups.forEach(fn => fn());\n req.socket.removeListener('close', onSocketClose);\n };\n\n const onUpgrade = () => finalize();\n const onResponse = (proxyRes: import('http').IncomingMessage) => {\n finalize(\n new Error(\n `Upstream rejected WebSocket upgrade with status ${proxyRes.statusCode}`,\n ),\n );\n };\n const onProxyError = (err: Error) => finalize(err);\n const onSocketError = (err: Error | unknown) =>\n finalize(err instanceof Error ? err : new Error(String(err)));\n const onSocketClosed = () =>\n finalize(\n new Error('Client closed connection during WebSocket handshake'),\n );\n\n proxyReq.once('upgrade', onUpgrade);\n proxyReq.once('response', onResponse);\n proxyReq.once('error', onProxyError);\n socket.once('error', onSocketError);\n socket.once('close', onSocketClosed);\n\n cleanups.push(\n () => proxyReq.removeListener('upgrade', onUpgrade),\n () => proxyReq.removeListener('response', onResponse),\n () => proxyReq.removeListener('error', onProxyError),\n () => socket.removeListener('error', onSocketError),\n () => socket.removeListener('close', onSocketClosed),\n );\n }\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backstage/plugin-kubernetes-backend",
|
|
3
|
-
"version": "0.21.7-next.
|
|
3
|
+
"version": "0.21.7-next.1",
|
|
4
4
|
"description": "A Backstage backend plugin that integrates towards Kubernetes",
|
|
5
5
|
"backstage": {
|
|
6
6
|
"role": "backend-plugin",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@aws-crypto/sha256-js": "^5.0.0",
|
|
50
50
|
"@aws-sdk/credential-providers": "^3.350.0",
|
|
51
51
|
"@azure/identity": "^4.0.0",
|
|
52
|
-
"@backstage/backend-plugin-api": "1.10.0-next.
|
|
52
|
+
"@backstage/backend-plugin-api": "1.10.0-next.1",
|
|
53
53
|
"@backstage/catalog-client": "1.16.1",
|
|
54
54
|
"@backstage/catalog-model": "1.9.0",
|
|
55
55
|
"@backstage/config": "1.3.8",
|
|
@@ -75,9 +75,9 @@
|
|
|
75
75
|
"node-fetch": "^2.7.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
-
"@backstage/backend-defaults": "0.17.
|
|
79
|
-
"@backstage/backend-test-utils": "1.11.6-next.
|
|
80
|
-
"@backstage/cli": "0.36.
|
|
78
|
+
"@backstage/backend-defaults": "0.17.7-next.2",
|
|
79
|
+
"@backstage/backend-test-utils": "1.11.6-next.1",
|
|
80
|
+
"@backstage/cli": "0.36.5-next.1",
|
|
81
81
|
"@backstage/plugin-permission-backend": "0.7.15-next.0",
|
|
82
82
|
"@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.22-next.0",
|
|
83
83
|
"@types/express": "^4.17.6",
|