@backstage/plugin-kubernetes-backend 0.21.6 → 0.22.0-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.
@@ -10,6 +10,11 @@ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'defau
10
10
 
11
11
  var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
12
12
 
13
+ const PROXY_AUDIT_FINALIZER = /* @__PURE__ */ Symbol("kubernetesProxyAuditFinalizer");
14
+ const PROXY_SOCKET_CLOSE_LISTENER = /* @__PURE__ */ Symbol(
15
+ "kubernetesProxySocketCloseListener"
16
+ );
17
+ const PROXY_PREPARED_TARGET = /* @__PURE__ */ Symbol("kubernetesProxyPreparedTarget");
13
18
  const HEADER_KUBERNETES_CLUSTER = "Backstage-Kubernetes-Cluster";
14
19
  const HEADER_KUBERNETES_AUTH = "Backstage-Kubernetes-Authorization";
15
20
  class KubernetesProxy {
@@ -18,42 +23,150 @@ class KubernetesProxy {
18
23
  clusterSupplier;
19
24
  authStrategy;
20
25
  httpAuth;
26
+ auditor;
21
27
  constructor(options) {
22
28
  this.logger = options.logger;
23
29
  this.clusterSupplier = options.clusterSupplier;
24
30
  this.authStrategy = options.authStrategy;
25
31
  this.httpAuth = options.httpAuth;
32
+ this.auditor = options.auditor;
26
33
  }
27
34
  createRequestHandler(options) {
28
35
  const { permissionApi } = options;
29
36
  return async (req, res, next) => {
30
- const authorizeResponse = await permissionApi.authorize(
31
- [{ permission: pluginKubernetesCommon.kubernetesProxyPermission }],
32
- {
33
- credentials: await this.httpAuth.credentials(req)
37
+ const clusterNameHeader = req.headers[HEADER_KUBERNETES_CLUSTER.toLowerCase()];
38
+ const clusterName = typeof clusterNameHeader === "string" && clusterNameHeader.length > 0 ? clusterNameHeader : "unknown";
39
+ const path = req.path || req.url.split("?")[0] || "";
40
+ let clientDisconnected = false;
41
+ const onSocketClose = () => {
42
+ clientDisconnected = true;
43
+ };
44
+ req.socket.once("close", onSocketClose);
45
+ let finalizeAudit;
46
+ let finishListenerAttached = false;
47
+ try {
48
+ const sanitizedReq = KubernetesProxy.createSanitizedRequest(req);
49
+ const auditorEvent = await this.auditor.createEvent({
50
+ eventId: "cluster-fetch",
51
+ severityLevel: "medium",
52
+ request: sanitizedReq,
53
+ meta: { queryType: "proxy", clusterName, method: req.method, path }
54
+ });
55
+ const auditContext = KubernetesProxy.createAuditFinalizer(
56
+ auditorEvent,
57
+ clusterName,
58
+ this.logger
59
+ );
60
+ finalizeAudit = auditContext.finalizeAudit;
61
+ finishListenerAttached = await this.authorizeAndDispatch(
62
+ req,
63
+ res,
64
+ next,
65
+ permissionApi,
66
+ finalizeAudit,
67
+ auditContext.setResolvedClusterName,
68
+ onSocketClose,
69
+ () => clientDisconnected
70
+ );
71
+ } catch (error) {
72
+ req.socket.removeListener("close", onSocketClose);
73
+ const err = error instanceof Error ? error : new Error(String(error));
74
+ if (finalizeAudit && !finishListenerAttached) {
75
+ finalizeAudit(err);
34
76
  }
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);
77
+ throw error;
46
78
  }
47
79
  };
48
80
  }
81
+ /**
82
+ * Authorizes the request, resolves the target cluster, and dispatches
83
+ * to either the HTTP or WebSocket proxy path. Returns `true` when
84
+ * HTTP finish listeners have been attached.
85
+ */
86
+ async authorizeAndDispatch(req, res, next, permissionApi, finalizeAudit, setResolvedClusterName, onSocketClose, isClientDisconnected) {
87
+ const authorizeResponse = await permissionApi.authorize(
88
+ [{ permission: pluginKubernetesCommon.kubernetesProxyPermission }],
89
+ { credentials: await this.httpAuth.credentials(req) }
90
+ );
91
+ if (authorizeResponse[0].result === pluginPermissionCommon.AuthorizeResult.DENY) {
92
+ req.socket.removeListener("close", onSocketClose);
93
+ finalizeAudit(new errors.NotAllowedError("Unauthorized"));
94
+ res.status(403).json({
95
+ error: errors.serializeError(new errors.NotAllowedError("Unauthorized"))
96
+ });
97
+ return false;
98
+ }
99
+ if (isClientDisconnected()) {
100
+ finalizeAudit(new Error("Client disconnected before proxy dispatch"));
101
+ return false;
102
+ }
103
+ const prepared = await this.prepareProxyTarget(req);
104
+ setResolvedClusterName(prepared.cluster.name);
105
+ req[PROXY_PREPARED_TARGET] = prepared;
106
+ const middleware = this.getOrCreateMiddleware(prepared.cluster);
107
+ if (isClientDisconnected()) {
108
+ finalizeAudit(new Error("Client disconnected before proxy dispatch"));
109
+ return false;
110
+ }
111
+ return KubernetesProxy.dispatchToProxy(
112
+ req,
113
+ res,
114
+ next,
115
+ middleware,
116
+ finalizeAudit,
117
+ onSocketClose
118
+ );
119
+ }
120
+ /**
121
+ * Resolves the cluster, credentials, and target URL for a proxy request
122
+ * before dispatching to the middleware. This ensures all async preparation
123
+ * completes under the caller's try/catch, avoiding unhandled rejections
124
+ * from http-proxy-middleware's fire-and-forget upgrade() path.
125
+ */
126
+ async prepareProxyTarget(req) {
127
+ const cluster = await this.getClusterForRequest(req);
128
+ const url = new URL(cluster.url);
129
+ const { bufferFromFileOrString } = await import('@kubernetes/client-node');
130
+ const target = {
131
+ protocol: url.protocol,
132
+ host: url.hostname,
133
+ port: url.port,
134
+ ca: bufferFromFileOrString(cluster.caFile, cluster.caData)?.toString()
135
+ };
136
+ const authHeader = req.headers[HEADER_KUBERNETES_AUTH.toLocaleLowerCase("en-US")];
137
+ if (typeof authHeader === "string") {
138
+ req.headers.authorization = authHeader;
139
+ } else {
140
+ const authObj = KubernetesProxy.authHeadersToKubernetesRequestAuth(
141
+ req.headers
142
+ );
143
+ const credential = await this.authStrategy.getCredential(
144
+ cluster,
145
+ authObj
146
+ );
147
+ if (credential.type === "bearer token") {
148
+ req.headers.authorization = `Bearer ${credential.token}`;
149
+ } else if (credential.type === "x509 client certificate") {
150
+ target.key = credential.key;
151
+ target.cert = credential.cert;
152
+ }
153
+ }
154
+ const requestPath = req.originalUrl || req.url || "";
155
+ const rewrittenPath = requestPath.replace(
156
+ new RegExp(`^${req.baseUrl || ""}`),
157
+ url.pathname || ""
158
+ );
159
+ return { proxyTarget: target, rewrittenPath, cluster };
160
+ }
49
161
  // We create one middleware per remote cluster and hold on to them, because
50
162
  // the secure property isn't possible to decide on a per-request basis with a
51
163
  // single middleware instance - and we don't expect it to change over time.
52
- async getMiddleware(originalReq) {
53
- const originalCluster = await this.getClusterForRequest(originalReq);
54
- let middleware = this.middlewareForClusterName.get(originalCluster.name);
164
+ // Target resolution and path rewriting are handled per-request via the
165
+ // PROXY_PREPARED_TARGET symbol stashed on each req by prepareProxyTarget.
166
+ getOrCreateMiddleware(cluster) {
167
+ let middleware = this.middlewareForClusterName.get(cluster.name);
55
168
  if (!middleware) {
56
- const logger = this.logger.child({ cluster: originalCluster.name });
169
+ const logger = this.logger.child({ cluster: cluster.name });
57
170
  middleware = httpProxyMiddleware.createProxyMiddleware({
58
171
  logProvider: () => ({
59
172
  log: logger.info.bind(logger),
@@ -62,55 +175,27 @@ class KubernetesProxy {
62
175
  warn: logger.warn.bind(logger),
63
176
  error: logger.error.bind(logger)
64
177
  }),
65
- ws: true,
66
- secure: !originalCluster.skipTLSVerify,
178
+ // ws must be false to prevent http-proxy-middleware from auto-subscribing
179
+ // to the server's 'upgrade' event, which would bypass Express routing and
180
+ // skip audit event creation for WebSocket requests.
181
+ ws: false,
182
+ secure: !cluster.skipTLSVerify,
67
183
  changeOrigin: true,
68
- pathRewrite: async (path, req) => {
69
- const cluster = await this.getClusterForRequest(req);
70
- const url = new URL(cluster.url);
71
- return path.replace(
72
- new RegExp(`^${originalReq.baseUrl}`),
73
- url.pathname || ""
74
- );
184
+ router: (req) => {
185
+ return req[PROXY_PREPARED_TARGET]?.proxyTarget;
75
186
  },
76
- router: async (req) => {
77
- const cluster = await this.getClusterForRequest(req);
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;
187
+ pathRewrite: (_path, req) => {
188
+ return req[PROXY_PREPARED_TARGET]?.rewrittenPath ?? _path;
107
189
  },
108
190
  onError: (error, req, res) => {
109
191
  const wrappedError = new errors.ForwardedError(
110
- `Cluster '${originalCluster.name}' request error`,
192
+ `Cluster '${cluster.name}' request error`,
111
193
  error
112
194
  );
113
195
  logger.error("Kubernetes proxy error", wrappedError);
196
+ if (typeof res.status !== "function") {
197
+ return;
198
+ }
114
199
  const body = {
115
200
  error: errors.serializeError(wrappedError, {
116
201
  includeStack: process.env.NODE_ENV === "development"
@@ -119,12 +204,155 @@ class KubernetesProxy {
119
204
  response: { statusCode: 500 }
120
205
  };
121
206
  res.status(500).json(body);
122
- }
207
+ },
208
+ onProxyReqWs: KubernetesProxy.onProxyReqWs
123
209
  });
124
- this.middlewareForClusterName.set(originalCluster.name, middleware);
210
+ this.middlewareForClusterName.set(cluster.name, middleware);
125
211
  }
126
212
  return middleware;
127
213
  }
214
+ /**
215
+ * Returns a copy of the request with query parameters stripped from both
216
+ * `originalUrl` and `url` so that sensitive values (e.g. exec command
217
+ * arguments) are not written to central audit logs.
218
+ */
219
+ static createSanitizedRequest(req) {
220
+ const sanitizedUrl = (req.originalUrl ?? req.url).split("?")[0];
221
+ return Object.create(req, {
222
+ originalUrl: { value: sanitizedUrl, enumerable: true },
223
+ url: { value: sanitizedUrl, enumerable: true }
224
+ });
225
+ }
226
+ /**
227
+ * Builds a once-only audit finalizer that resolves the auditor event as
228
+ * either success or failure. Returns a setter for the resolved cluster
229
+ * name so downstream code can enrich the terminal metadata.
230
+ */
231
+ static createAuditFinalizer(auditorEvent, initialClusterName, logger) {
232
+ let resolvedClusterName;
233
+ let finalized = false;
234
+ const finalizeAudit = (error) => {
235
+ if (finalized) {
236
+ return;
237
+ }
238
+ finalized = true;
239
+ const terminalMeta = resolvedClusterName && resolvedClusterName !== initialClusterName ? { clusterName: resolvedClusterName } : void 0;
240
+ const audit = error ? auditorEvent.fail({
241
+ error,
242
+ ...terminalMeta && { meta: terminalMeta }
243
+ }) : auditorEvent.success(
244
+ terminalMeta ? { meta: terminalMeta } : void 0
245
+ );
246
+ audit.catch((err) => logger.error("Failed to emit proxy audit event", err));
247
+ };
248
+ return {
249
+ finalizeAudit,
250
+ setResolvedClusterName: (name) => {
251
+ resolvedClusterName = name;
252
+ }
253
+ };
254
+ }
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
+ static dispatchToProxy(req, res, next, middleware, finalizeAudit, onSocketClose) {
262
+ const isWebSocketUpgrade = (req.header("connection") ?? "").toLowerCase().split(",").some((t) => t.trim() === "upgrade") && req.header("upgrade")?.toLowerCase() === "websocket";
263
+ if (isWebSocketUpgrade) {
264
+ req[PROXY_AUDIT_FINALIZER] = finalizeAudit;
265
+ req[PROXY_SOCKET_CLOSE_LISTENER] = onSocketClose;
266
+ try {
267
+ middleware.upgrade(req, req.socket, void 0);
268
+ } catch (error) {
269
+ req.socket.removeListener("close", onSocketClose);
270
+ finalizeAudit(
271
+ error instanceof Error ? error : new Error(String(error))
272
+ );
273
+ throw error;
274
+ }
275
+ return false;
276
+ }
277
+ req.socket.removeListener("close", onSocketClose);
278
+ KubernetesProxy.attachHttpAuditListeners(res, finalizeAudit);
279
+ middleware(req, res, next);
280
+ return true;
281
+ }
282
+ /**
283
+ * Lifecycle handler for WebSocket proxy requests. Reads the audit
284
+ * finalizer and the early socket-close listener from the request,
285
+ * then wires once-only listeners on `upgrade` (success), `response`
286
+ * (upstream rejection), `error`, `socket error`, and `socket close`
287
+ * to resolve the audit event exactly once. The first terminal
288
+ * callback removes all observers to prevent leaks and duplicate
289
+ * finalization.
290
+ */
291
+ static onProxyReqWs(proxyReq, req, socket) {
292
+ const typedReq = req;
293
+ const finalizeAudit = typedReq[PROXY_AUDIT_FINALIZER];
294
+ const earlyCloseListener = typedReq[PROXY_SOCKET_CLOSE_LISTENER];
295
+ if (!finalizeAudit) {
296
+ return;
297
+ }
298
+ delete typedReq[PROXY_AUDIT_FINALIZER];
299
+ delete typedReq[PROXY_SOCKET_CLOSE_LISTENER];
300
+ const cleanups = [];
301
+ const finalize = (error) => {
302
+ finalizeAudit(error);
303
+ cleanups.forEach((fn) => fn());
304
+ if (earlyCloseListener) {
305
+ req.socket.removeListener("close", earlyCloseListener);
306
+ }
307
+ };
308
+ const onUpgrade = () => finalize();
309
+ const onResponse = (proxyRes) => {
310
+ finalize(
311
+ new Error(
312
+ `Upstream rejected WebSocket upgrade with status ${proxyRes.statusCode}`
313
+ )
314
+ );
315
+ };
316
+ const onProxyError = (err) => finalize(err);
317
+ const onSocketError = (err) => finalize(err instanceof Error ? err : new Error(String(err)));
318
+ const onSocketClose = () => finalize(
319
+ new Error("Client closed connection during WebSocket handshake")
320
+ );
321
+ proxyReq.once("upgrade", onUpgrade);
322
+ proxyReq.once("response", onResponse);
323
+ proxyReq.once("error", onProxyError);
324
+ socket.once("error", onSocketError);
325
+ socket.once("close", onSocketClose);
326
+ cleanups.push(
327
+ () => proxyReq.removeListener("upgrade", onUpgrade),
328
+ () => proxyReq.removeListener("response", onResponse),
329
+ () => proxyReq.removeListener("error", onProxyError),
330
+ () => socket.removeListener("error", onSocketError),
331
+ () => socket.removeListener("close", onSocketClose)
332
+ );
333
+ }
334
+ static attachHttpAuditListeners(res, finalizeAudit) {
335
+ const finalizeFromResponse = () => {
336
+ if (!res.writableFinished) {
337
+ finalizeAudit(
338
+ new Error("Client disconnected before response completed")
339
+ );
340
+ } else if (res.statusCode >= 400) {
341
+ finalizeAudit(
342
+ new Error(`Proxy responded with status ${res.statusCode}`)
343
+ );
344
+ } else {
345
+ finalizeAudit();
346
+ }
347
+ };
348
+ const finalizeOnce = () => {
349
+ res.removeListener("finish", finalizeOnce);
350
+ res.removeListener("close", finalizeOnce);
351
+ finalizeFromResponse();
352
+ };
353
+ res.once("finish", finalizeOnce);
354
+ res.once("close", finalizeOnce);
355
+ }
128
356
  async getClusterForRequest(req) {
129
357
  const { KubeConfig } = await import('@kubernetes/client-node');
130
358
  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 AuditorServiceEvent,\n DiscoveryService,\n HttpAuthService,\n LoggerService,\n PermissionsService,\n} from '@backstage/backend-plugin-api';\n\nconst PROXY_AUDIT_FINALIZER = Symbol('kubernetesProxyAuditFinalizer');\nconst PROXY_SOCKET_CLOSE_LISTENER = Symbol(\n 'kubernetesProxySocketCloseListener',\n);\nconst PROXY_PREPARED_TARGET = Symbol('kubernetesProxyPreparedTarget');\n\ntype ProxyAuditFinalizer = (error?: Error) => void;\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 finalizeAudit: ProxyAuditFinalizer | undefined;\n let finishListenerAttached = false;\n try {\n const sanitizedReq = KubernetesProxy.createSanitizedRequest(req);\n const auditorEvent = await this.auditor.createEvent({\n eventId: 'cluster-fetch',\n severityLevel: 'medium',\n request: sanitizedReq,\n meta: { queryType: 'proxy', clusterName, method: req.method, path },\n });\n\n const auditContext = KubernetesProxy.createAuditFinalizer(\n auditorEvent,\n clusterName,\n this.logger,\n );\n finalizeAudit = auditContext.finalizeAudit;\n\n finishListenerAttached = await this.authorizeAndDispatch(\n req,\n res,\n next,\n permissionApi,\n finalizeAudit,\n auditContext.setResolvedClusterName,\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 (finalizeAudit && !finishListenerAttached) {\n finalizeAudit(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 finalizeAudit: ProxyAuditFinalizer,\n setResolvedClusterName: (name: string) => void,\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 finalizeAudit(new NotAllowedError('Unauthorized'));\n res.status(403).json({\n error: serializeError(new NotAllowedError('Unauthorized')),\n });\n return false;\n }\n\n if (isClientDisconnected()) {\n finalizeAudit(new Error('Client disconnected before proxy dispatch'));\n return false;\n }\n\n const prepared = await this.prepareProxyTarget(req);\n setResolvedClusterName(prepared.cluster.name);\n (req as any)[PROXY_PREPARED_TARGET] = prepared;\n const middleware = this.getOrCreateMiddleware(prepared.cluster);\n\n if (isClientDisconnected()) {\n finalizeAudit(new Error('Client disconnected before proxy dispatch'));\n return false;\n }\n\n return KubernetesProxy.dispatchToProxy(\n req,\n res,\n next,\n middleware,\n finalizeAudit,\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: KubernetesProxy.onProxyReqWs,\n });\n this.middlewareForClusterName.set(cluster.name, middleware);\n }\n return middleware;\n }\n\n /**\n * Returns a copy of the request with query parameters stripped from both\n * `originalUrl` and `url` so that sensitive values (e.g. exec command\n * arguments) are not written to central audit logs.\n */\n private static createSanitizedRequest(req: Request): Request {\n const sanitizedUrl = (req.originalUrl ?? req.url).split('?')[0];\n return Object.create(req, {\n originalUrl: { value: sanitizedUrl, enumerable: true },\n url: { value: sanitizedUrl, enumerable: true },\n });\n }\n\n /**\n * Builds a once-only audit finalizer that resolves the auditor event as\n * either success or failure. Returns a setter for the resolved cluster\n * name so downstream code can enrich the terminal metadata.\n */\n private static createAuditFinalizer(\n auditorEvent: AuditorServiceEvent,\n initialClusterName: string,\n logger: LoggerService,\n ): {\n finalizeAudit: ProxyAuditFinalizer;\n setResolvedClusterName: (name: string) => void;\n } {\n let resolvedClusterName: string | undefined;\n let finalized = false;\n\n const finalizeAudit: ProxyAuditFinalizer = error => {\n if (finalized) {\n return;\n }\n finalized = true;\n\n const terminalMeta =\n resolvedClusterName && resolvedClusterName !== initialClusterName\n ? { clusterName: resolvedClusterName }\n : undefined;\n const audit = error\n ? auditorEvent.fail({\n error,\n ...(terminalMeta && { meta: terminalMeta }),\n })\n : auditorEvent.success(\n terminalMeta ? { meta: terminalMeta } : undefined,\n );\n audit.catch(err => logger.error('Failed to emit proxy audit event', err));\n };\n\n return {\n finalizeAudit,\n setResolvedClusterName: (name: string) => {\n resolvedClusterName = name;\n },\n };\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 finalizeAudit: ProxyAuditFinalizer,\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 (\n req as Request & {\n [PROXY_AUDIT_FINALIZER]?: ProxyAuditFinalizer;\n [PROXY_SOCKET_CLOSE_LISTENER]?: () => void;\n }\n )[PROXY_AUDIT_FINALIZER] = finalizeAudit;\n (req as any)[PROXY_SOCKET_CLOSE_LISTENER] = onSocketClose;\n\n try {\n middleware.upgrade!(req, req.socket, undefined);\n } catch (error) {\n req.socket.removeListener('close', onSocketClose);\n finalizeAudit(\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 KubernetesProxy.attachHttpAuditListeners(res, finalizeAudit);\n middleware(req, res, next);\n return true;\n }\n\n /**\n * Lifecycle handler for WebSocket proxy requests. Reads the audit\n * finalizer and the early socket-close listener from the request,\n * then wires once-only listeners on `upgrade` (success), `response`\n * (upstream rejection), `error`, `socket error`, and `socket close`\n * to resolve the audit event exactly once. The first terminal\n * callback removes all observers to prevent leaks and duplicate\n * finalization.\n */\n private static onProxyReqWs(\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_AUDIT_FINALIZER]?: ProxyAuditFinalizer;\n [PROXY_SOCKET_CLOSE_LISTENER]?: () => void;\n };\n\n const finalizeAudit = typedReq[PROXY_AUDIT_FINALIZER];\n const earlyCloseListener = typedReq[PROXY_SOCKET_CLOSE_LISTENER];\n if (!finalizeAudit) {\n return;\n }\n delete typedReq[PROXY_AUDIT_FINALIZER];\n delete typedReq[PROXY_SOCKET_CLOSE_LISTENER];\n\n const cleanups: (() => void)[] = [];\n const finalize = (error?: Error) => {\n finalizeAudit(error);\n cleanups.forEach(fn => fn());\n if (earlyCloseListener) {\n (req as Request).socket.removeListener('close', earlyCloseListener);\n }\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 onSocketClose = () =>\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', onSocketClose);\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', onSocketClose),\n );\n }\n\n private static attachHttpAuditListeners(\n res: Response,\n finalizeAudit: ProxyAuditFinalizer,\n ): void {\n const finalizeFromResponse = () => {\n if (!res.writableFinished) {\n finalizeAudit(\n new Error('Client disconnected before response completed'),\n );\n } else if (res.statusCode >= 400) {\n finalizeAudit(\n new Error(`Proxy responded with status ${res.statusCode}`),\n );\n } else {\n finalizeAudit();\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 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","serializeError","createProxyMiddleware","ForwardedError","NotFoundError","ANNOTATION_KUBERNETES_AUTH_PROVIDER","fs","SERVICEACCOUNT_CA_PATH"],"mappings":"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"}