From e6c7c85012946ade3f396385a4772c78ec698bd7 Mon Sep 17 00:00:00 2001 From: Ruben Vallejo Date: Tue, 21 Feb 2023 15:41:28 -0500 Subject: [PATCH] change decorateClusterDetailsWithAuth method to use the authTranslator provided by the KubernetesFanOutHandler constructor Signed-off-by: Ruben Vallejo --- .changeset/blue-walls-drop.md | 5 + plugins/kubernetes-backend/api-report.md | 54 ++++- ...ispatchingKubernetesAuthTranslator.test.ts | 8 +- .../DispatchingKubernetesAuthTranslator.ts | 4 +- .../src/service/KubernetesBuilder.ts | 58 ++++- .../service/KubernetesFanOutHandler.test.ts | 10 + .../src/service/KubernetesFanOutHandler.ts | 32 +-- .../src/service/KubernetesProxy.test.ts | 212 +++++++++++++++++- .../src/service/KubernetesProxy.ts | 46 ++-- .../kubernetes-backend/src/service/index.ts | 5 +- 10 files changed, 366 insertions(+), 68 deletions(-) create mode 100644 .changeset/blue-walls-drop.md diff --git a/.changeset/blue-walls-drop.md b/.changeset/blue-walls-drop.md new file mode 100644 index 0000000000..a8e9ea1d33 --- /dev/null +++ b/.changeset/blue-walls-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': minor +--- + +Plugins that instantiate the `KubernetesProxy` must now provide a parameter of the type `KubernetesProxyOptions` which includes providing a `KubernetesAuthTranslator`. The `KubernetesBuilder` now builds its own `KubernetesAuthTranslatorMap` that it provides to the `KubernetesProxy`. The `DispatchingKubernetesAuthTranslator` expects a `KubernetesTranslatorMap` to be provided as a parameter. The `KubernetesBuilder` now has a method called `setAuthTranslatorMap` which allows integrators to bring their own `KubernetesAuthTranslator's` to the `KubernetesPlugin`. diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 449131a26e..bc51d504de 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -110,6 +110,25 @@ export interface CustomResourcesByEntity extends KubernetesObjectsByEntity { // @public (undocumented) export const DEFAULT_OBJECTS: ObjectToFetch[]; +// @public +export class DispatchingKubernetesAuthTranslator + implements KubernetesAuthTranslator +{ + constructor(options: DispatchingKubernetesAuthTranslatorOptions); + // (undocumented) + decorateClusterDetailsWithAuth( + clusterDetails: ClusterDetails, + auth: KubernetesRequestAuth, + ): Promise; +} + +// @public (undocumented) +export type DispatchingKubernetesAuthTranslatorOptions = { + authTranslatorMap: { + [key: string]: KubernetesAuthTranslator; + }; +}; + // @public (undocumented) export interface FetchResponseWrapper { // (undocumented) @@ -157,23 +176,16 @@ export interface KubernetesAuthTranslator { ): Promise; } -// @public (undocumented) -export class KubernetesAuthTranslatorGenerator { - // (undocumented) - static getKubernetesAuthTranslatorInstance( - authProvider: string, - options: { - logger: Logger; - }, - ): KubernetesAuthTranslator; -} - // @public (undocumented) export class KubernetesBuilder { constructor(env: KubernetesEnvironment); // (undocumented) build(): KubernetesBuilderReturn; // (undocumented) + protected buildAuthTranslatorMap(): { + [key: string]: KubernetesAuthTranslator; + }; + // (undocumented) protected buildClusterSupplier( refreshInterval: Duration, ): KubernetesClustersSupplier; @@ -220,6 +232,10 @@ export class KubernetesBuilder { clusterSupplier: KubernetesClustersSupplier, ): Promise; // (undocumented) + protected getAuthTranslatorMap(): { + [key: string]: KubernetesAuthTranslator; + }; + // (undocumented) protected getClusterSupplier(): KubernetesClustersSupplier; // (undocumented) protected getFetcher(): KubernetesFetcher; @@ -239,6 +255,10 @@ export class KubernetesBuilder { // (undocumented) protected getServiceLocatorMethod(): ServiceLocatorMethod; // (undocumented) + setAuthTranslatorMap(authTranslatorMap: { + [key: string]: KubernetesAuthTranslator; + }): void; + // (undocumented) setClusterSupplier(clusterSupplier?: KubernetesClustersSupplier): this; // (undocumented) setDefaultClusterRefreshInterval(refreshInterval: Duration): this; @@ -261,6 +281,9 @@ export type KubernetesBuilderReturn = Promise<{ proxy: KubernetesProxy; objectsProvider: KubernetesObjectsProvider; serviceLocator: KubernetesServiceLocator; + authTranslatorMap: { + [key: string]: KubernetesAuthTranslator; + }; }>; // @public @@ -345,7 +368,7 @@ export type KubernetesObjectTypes = // @public export class KubernetesProxy { - constructor(logger: Logger, clusterSupplier: KubernetesClustersSupplier); + constructor(options: KubernetesProxyOptions); // (undocumented) createRequestHandler( options: KubernetesProxyCreateRequestHandlerOptions, @@ -357,6 +380,13 @@ export type KubernetesProxyCreateRequestHandlerOptions = { permissionApi: PermissionEvaluator; }; +// @public +export type KubernetesProxyOptions = { + logger: Logger; + clusterSupplier: KubernetesClustersSupplier; + authTranslator: KubernetesAuthTranslator; +}; + // @public export interface KubernetesServiceLocator { // (undocumented) diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.test.ts index 02b60a031b..3132bdef76 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.test.ts @@ -31,7 +31,7 @@ describe('decorateClusterDetailsWithAuth', () => { }); }); - it('can decorate cluster details if the auth provider is in the translator map', () => { + it('can decorate cluster details if the auth provider is in the translator map', async () => { const expectedClusterDetails: ClusterDetails = { url: 'notanything.com', name: 'randomName', @@ -39,11 +39,11 @@ describe('decorateClusterDetailsWithAuth', () => { serviceAccountToken: 'added by mock translator', }; - mockTranslator.decorateClusterDetailsWithAuth.mockReturnValue( - expectedClusterDetails as unknown as Promise, + mockTranslator.decorateClusterDetailsWithAuth.mockResolvedValue( + expectedClusterDetails, ); - const returnedValue = authTranslator.decorateClusterDetailsWithAuth( + const returnedValue = await authTranslator.decorateClusterDetailsWithAuth( { name: 'googleCluster', url: 'anything.com', authProvider: 'google' }, authObject, ); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.ts index 0290bc4c52..82d9538cac 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/DispatchingKubernetesAuthTranslator.ts @@ -22,7 +22,7 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; * * @public */ -export type KubernetesAuthTranslatorGeneratorOptions = { +export type DispatchingKubernetesAuthTranslatorOptions = { authTranslatorMap: { [key: string]: KubernetesAuthTranslator; }; @@ -36,7 +36,7 @@ export class DispatchingKubernetesAuthTranslator { private readonly translatorMap: { [key: string]: KubernetesAuthTranslator }; - constructor(options: KubernetesAuthTranslatorGeneratorOptions) { + constructor(options: DispatchingKubernetesAuthTranslatorOptions) { this.translatorMap = options.authTranslatorMap; } diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 367e5edef3..5e885512f7 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -24,6 +24,17 @@ import { Duration } from 'luxon'; import { Logger } from 'winston'; import { getCombinedClusterSupplier } from '../cluster-locator'; +import { + KubernetesAuthTranslator, + DispatchingKubernetesAuthTranslator, + GoogleKubernetesAuthTranslator, + NoopKubernetesAuthTranslator, + AwsIamKubernetesAuthTranslator, + GoogleServiceAccountAuthTranslator, + AzureIdentityKubernetesAuthTranslator, + OidcKubernetesAuthTranslator, +} from '../kubernetes-auth-translator'; + import { addResourceRoutesToRouter } from '../routes/resourcesRoutes'; import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator'; import { @@ -68,6 +79,7 @@ export type KubernetesBuilderReturn = Promise<{ proxy: KubernetesProxy; objectsProvider: KubernetesObjectsProvider; serviceLocator: KubernetesServiceLocator; + authTranslatorMap: { [key: string]: KubernetesAuthTranslator }; }>; /** @@ -83,6 +95,7 @@ export class KubernetesBuilder { private fetcher?: KubernetesFetcher; private serviceLocator?: KubernetesServiceLocator; private proxy?: KubernetesProxy; + private authTranslatorMap?: { [key: string]: KubernetesAuthTranslator }; static createBuilder(env: KubernetesEnvironment) { return new KubernetesBuilder(env); @@ -114,6 +127,8 @@ export class KubernetesBuilder { const clusterSupplier = this.getClusterSupplier(); + const authTranslatorMap = this.getAuthTranslatorMap(); + const proxy = this.getProxy(logger, clusterSupplier); const serviceLocator = this.getServiceLocator(); @@ -142,6 +157,7 @@ export class KubernetesBuilder { objectsProvider, router, serviceLocator, + authTranslatorMap, }; } @@ -175,6 +191,12 @@ export class KubernetesBuilder { return this; } + public setAuthTranslatorMap(authTranslatorMap: { + [key: string]: KubernetesAuthTranslator; + }) { + this.authTranslatorMap = authTranslatorMap; + } + protected buildCustomResources() { const customResources: CustomResource[] = ( this.env.config.getOptionalConfigArray('kubernetes.customResources') ?? [] @@ -210,7 +232,14 @@ export class KubernetesBuilder { protected buildObjectsProvider( options: KubernetesObjectsProviderOptions, ): KubernetesObjectsProvider { - this.objectsProvider = new KubernetesFanOutHandler(options); + const authTranslatorMap = this.getAuthTranslatorMap(); + this.objectsProvider = new KubernetesFanOutHandler({ + ...options, + authTranslator: new DispatchingKubernetesAuthTranslator({ + authTranslatorMap, + }), + }); + return this.objectsProvider; } @@ -259,7 +288,15 @@ export class KubernetesBuilder { logger: Logger, clusterSupplier: KubernetesClustersSupplier, ): KubernetesProxy { - this.proxy = new KubernetesProxy(logger, clusterSupplier); + const authTranslatorMap = this.getAuthTranslatorMap(); + const authTranslator = new DispatchingKubernetesAuthTranslator({ + authTranslatorMap, + }); + this.proxy = new KubernetesProxy({ + logger, + clusterSupplier, + authTranslator, + }); return this.proxy; } @@ -314,6 +351,19 @@ export class KubernetesBuilder { return router; } + protected buildAuthTranslatorMap() { + this.authTranslatorMap = { + google: new GoogleKubernetesAuthTranslator(), + aws: new AwsIamKubernetesAuthTranslator(), + azure: new AzureIdentityKubernetesAuthTranslator(this.env.logger), + serviceAccount: new NoopKubernetesAuthTranslator(), + googleServiceAccount: new GoogleServiceAccountAuthTranslator(), + oidc: new OidcKubernetesAuthTranslator(), + localKubectlProxy: new NoopKubernetesAuthTranslator(), + }; + return this.authTranslatorMap; + } + protected async fetchClusterDetails( clusterSupplier: KubernetesClustersSupplier, ) { @@ -393,4 +443,8 @@ export class KubernetesBuilder { ) { return this.proxy ?? this.buildProxy(logger, clusterSupplier); } + + protected getAuthTranslatorMap() { + return this.authTranslatorMap ?? this.buildAuthTranslatorMap(); + } } diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index 53aaa1ad3a..de6035b48e 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -166,6 +166,11 @@ function getKubernetesFanOutHandler(customResources: CustomResource[]) { getClustersByEntity, }, customResources: customResources, + authTranslator: { + decorateClusterDetailsWithAuth: async (clusterDetails, _) => { + return clusterDetails; + }, + }, }); } @@ -879,6 +884,11 @@ describe('getKubernetesObjectsByEntity', () => { objectType: 'services', }, ], + authTranslator: { + decorateClusterDetailsWithAuth: async (clusterDetails, _) => { + return clusterDetails; + }, + }, }); const result = await sut.getKubernetesObjectsByEntity({ diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index f71c6506bd..8441effabd 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -30,7 +30,6 @@ import { ServiceLocatorRequestContext, } from '../types/types'; import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types'; -import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator'; import { ClientContainerStatus, ClientCurrentResourceUsage, @@ -137,7 +136,9 @@ export const DEFAULT_OBJECTS: ObjectToFetch[] = [ ]; export interface KubernetesFanOutHandlerOptions - extends KubernetesObjectsProviderOptions {} + extends KubernetesObjectsProviderOptions { + authTranslator: KubernetesAuthTranslator; +} export interface KubernetesRequestBody extends ObjectsByEntityRequest {} @@ -195,7 +196,7 @@ export class KubernetesFanOutHandler { private readonly serviceLocator: KubernetesServiceLocator; private readonly customResources: CustomResource[]; private readonly objectTypesToFetch: Set; - private readonly authTranslators: Record; + private readonly authTranslator: KubernetesAuthTranslator; constructor({ logger, @@ -203,13 +204,14 @@ export class KubernetesFanOutHandler { serviceLocator, customResources, objectTypesToFetch = DEFAULT_OBJECTS, + authTranslator, }: KubernetesFanOutHandlerOptions) { this.logger = logger; this.fetcher = fetcher; this.serviceLocator = serviceLocator; this.customResources = customResources; this.objectTypesToFetch = new Set(objectTypesToFetch); - this.authTranslators = {}; + this.authTranslator = authTranslator; } async getCustomResourcesByEntity({ @@ -313,12 +315,7 @@ export class KubernetesFanOutHandler { // Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them const promiseResults = await Promise.allSettled( clusterDetails.map(cd => { - const kubernetesAuthTranslator: KubernetesAuthTranslator = - this.getAuthTranslator(cd.authProvider); - return kubernetesAuthTranslator.decorateClusterDetailsWithAuth( - cd, - auth, - ); + return this.authTranslator.decorateClusterDetailsWithAuth(cd, auth); }), ); @@ -393,19 +390,4 @@ export class KubernetesFanOutHandler { result.errors.push(...podMetrics.errors); return [result, podMetrics.responses as PodStatusFetchResponse[]]; } - - private getAuthTranslator(provider: string): KubernetesAuthTranslator { - if (this.authTranslators[provider]) { - return this.authTranslators[provider]; - } - - this.authTranslators[provider] = - KubernetesAuthTranslatorGenerator.getKubernetesAuthTranslatorInstance( - provider, - { - logger: this.logger, - }, - ); - return this.authTranslators[provider]; - } } diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index d2404af839..57eb29f0a7 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -28,16 +28,19 @@ import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; import { APPLICATION_JSON, HEADER_KUBERNETES_CLUSTER, + HEADER_KUBERNETES_AUTH, KubernetesProxy, } from './KubernetesProxy'; import { AuthorizeResult, PermissionEvaluator, } from '@backstage/plugin-permission-common'; +import { KubernetesAuthTranslator } from '../kubernetes-auth-translator'; describe('KubernetesProxy', () => { let proxy: KubernetesProxy; const worker = setupServer(); + const logger = getVoidLogger(); setupRequestMockHandlers(worker); @@ -73,9 +76,13 @@ describe('KubernetesProxy', () => { authorizeConditional: jest.fn(), }; + const authTranslator: jest.Mocked = { + decorateClusterDetailsWithAuth: jest.fn(), + }; + beforeEach(() => { jest.resetAllMocks(); - proxy = new KubernetesProxy(getVoidLogger(), clusterSupplier); + proxy = new KubernetesProxy({ logger, clusterSupplier, authTranslator }); }); it('should return a ERROR_NOT_FOUND if no clusters are found', async () => { @@ -112,21 +119,30 @@ describe('KubernetesProxy', () => { authProvider: 'serviceAccount', }, ] as ClusterDetails[]); + + permissionApi.authorize.mockReturnValue( + Promise.resolve([{ result: AuthorizeResult.ALLOW }]), + ); + + authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ + name: 'cluster1', + url: 'https://localhost:9999', + serviceAccountToken: '', + authProvider: 'serviceAccount', + } as ClusterDetails); + const app = express().use( '/mountpath', proxy.createRequestHandler({ permissionApi }), ); - permissionApi.authorize.mockReturnValue( - Promise.resolve([{ result: AuthorizeResult.ALLOW }]), - ); const requestPromise = request(app) .get('/mountpath/api') .set(HEADER_KUBERNETES_CLUSTER, 'cluster1'); worker.use( - rest.get('https://localhost:9999/api', (_, res, ctx) => + rest.get('https://localhost:9999/api', (_: any, res: any, ctx: any) => res(ctx.status(299), ctx.json(apiResponse)), ), - rest.all(requestPromise.url, (req, _res, _ctx) => req.passthrough()), + rest.all(requestPromise.url, (req: any) => req.passthrough()), ); const response = await requestPromise; @@ -134,4 +150,188 @@ describe('KubernetesProxy', () => { expect(response.status).toEqual(299); expect(response.body).toStrictEqual(apiResponse); }); + + it('should default to using a provided authorization header', async () => { + worker.use( + rest.get( + 'https://localhost:9999/api/v1/namespaces', + (req: any, res: any, ctx: any) => { + if (!req.headers.get('Authorization')) { + return res(ctx.status(401)); + } + + if (req.headers.get('Authorization') !== 'my-token') { + return res(ctx.status(403)); + } + + return res( + ctx.status(200), + ctx.json({ + kind: 'NamespaceList', + apiVersion: 'v1', + items: [], + }), + ); + }, + ), + ); + + permissionApi.authorize.mockReturnValue( + Promise.resolve([{ result: AuthorizeResult.ALLOW }]), + ); + + clusterSupplier.getClusters.mockResolvedValue([ + { + name: 'cluster1', + url: 'https://localhost:9999', + serviceAccountToken: '', + authProvider: 'serviceAccount', + }, + ] as ClusterDetails[]); + + authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ + name: 'cluster1', + url: 'https://localhost:9999', + serviceAccountToken: 'random-token', + authProvider: 'serviceAccount', + } as ClusterDetails); + + const app = express().use( + '/mountpath', + proxy.createRequestHandler({ permissionApi }), + ); + const requestPromise = request(app) + .get('/mountpath/api/v1/namespaces') + .set(HEADER_KUBERNETES_CLUSTER, 'cluster1') + .set('Authorization', 'my-token'); + + worker.use(rest.all(requestPromise.url, (req: any) => req.passthrough())); + + const response = await requestPromise; + + expect(response.status).toEqual(200); + }); + + it('should add a serviceAccountToken to the request headers if one isnt provided in request and one isnt set up in cluster details', async () => { + worker.use( + rest.get('https://localhost:9999/api/v1/namespaces', (req, res, ctx) => { + if (!req.headers.get('Authorization')) { + return res(ctx.status(401)); + } + + if (req.headers.get('Authorization') !== 'Bearer my-token') { + return res(ctx.status(403)); + } + + return res( + ctx.status(200), + ctx.json({ + kind: 'NamespaceList', + apiVersion: 'v1', + items: [], + }), + ); + }), + ); + + permissionApi.authorize.mockReturnValue( + Promise.resolve([{ result: AuthorizeResult.ALLOW }]), + ); + + clusterSupplier.getClusters.mockResolvedValue([ + { + name: 'cluster1', + url: 'https://localhost:9999', + authProvider: 'googleServiceAccount', + }, + ] as ClusterDetails[]); + + authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ + name: 'cluster1', + url: 'https://localhost:9999', + serviceAccountToken: 'my-token', + authProvider: 'googleServiceAccount', + } as ClusterDetails); + + const app = express().use( + '/mountpath', + proxy.createRequestHandler({ permissionApi }), + ); + const requestPromise = request(app) + .get('/mountpath/api/v1/namespaces') + .set(HEADER_KUBERNETES_CLUSTER, 'cluster1'); + + worker.use(rest.all(requestPromise.url, (req: any) => req.passthrough())); + + const response = await requestPromise; + + expect(response.status).toEqual(200); + expect(response.body).toStrictEqual({ + kind: 'NamespaceList', + apiVersion: 'v1', + items: [], + }); + }); + + it('should append the Backstage-Kubernetes-Auth field to the requests authorization header if one is provided', async () => { + worker.use( + rest.get('https://localhost:9999/api/v1/namespaces', (req, res, ctx) => { + if (!req.headers.get('Authorization')) { + return res(ctx.status(401)); + } + + if (req.headers.get('Authorization') !== 'tokenB') { + return res(ctx.status(403)); + } + + return res( + ctx.status(200), + ctx.json({ + kind: 'NamespaceList', + apiVersion: 'v1', + items: [], + }), + ); + }), + ); + + permissionApi.authorize.mockReturnValue( + Promise.resolve([{ result: AuthorizeResult.ALLOW }]), + ); + + clusterSupplier.getClusters.mockResolvedValue([ + { + name: 'cluster1', + url: 'https://localhost:9999', + authProvider: 'googleServiceAccount', + }, + ] as ClusterDetails[]); + + authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({ + name: 'cluster1', + url: 'https://localhost:9999', + serviceAccountToken: 'tokenA', + authProvider: 'googleServiceAccount', + } as ClusterDetails); + + const app = express().use( + '/mountpath', + proxy.createRequestHandler({ permissionApi }), + ); + const requestPromise = request(app) + .get('/mountpath/api/v1/namespaces') + .set(HEADER_KUBERNETES_CLUSTER, 'cluster1') + .set(HEADER_KUBERNETES_AUTH, 'tokenB'); + + worker.use(rest.all(requestPromise.url, (req: any) => req.passthrough())); + + const response = await requestPromise; + + expect(response.status).toEqual(200); + expect(response.body).toStrictEqual({ + kind: 'NamespaceList', + apiVersion: 'v1', + items: [], + }); + }); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts index 49a381b75a..eb6748ffba 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -32,6 +32,7 @@ import { bufferFromFileOrString } from '@kubernetes/client-node'; import type { Request, RequestHandler } from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import { Logger } from 'winston'; +import { KubernetesAuthTranslator } from '../kubernetes-auth-translator'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; export const APPLICATION_JSON: string = 'application/json'; @@ -60,6 +61,17 @@ export type KubernetesProxyCreateRequestHandlerOptions = { permissionApi: PermissionEvaluator; }; +/** + * Options accepted as a parameter by the KubernetesProxy + * + * @public + */ +export type KubernetesProxyOptions = { + logger: Logger; + clusterSupplier: KubernetesClustersSupplier; + authTranslator: KubernetesAuthTranslator; +}; + /** * A proxy that routes requests to the Kubernetes API. * @@ -67,11 +79,15 @@ export type KubernetesProxyCreateRequestHandlerOptions = { */ export class KubernetesProxy { private readonly middlewareForClusterName = new Map(); + private readonly logger: Logger; + private readonly clusterSupplier: KubernetesClustersSupplier; + private readonly authTranslator: KubernetesAuthTranslator; - constructor( - private readonly logger: Logger, - private readonly clusterSupplier: KubernetesClustersSupplier, - ) {} + constructor(options: KubernetesProxyOptions) { + this.logger = options.logger; + this.clusterSupplier = options.clusterSupplier; + this.authTranslator = options.authTranslator; + } public createRequestHandler( options: KubernetesProxyCreateRequestHandlerOptions, @@ -96,6 +112,12 @@ export class KubernetesProxy { return; } + const cluster = await this.getClusterForRequest(req).then(cd => + this.authTranslator.decorateClusterDetailsWithAuth(cd, {}), + ); + if (!req.headers.authorization) { + req.headers.authorization = `Bearer ${cluster.serviceAccountToken}`; + } const middleware = await this.getMiddleware(req); middleware(req, res, next); }; @@ -108,13 +130,6 @@ export class KubernetesProxy { const originalCluster = await this.getClusterForRequest(originalReq); let middleware = this.middlewareForClusterName.get(originalCluster.name); if (!middleware) { - // Probably too risky without permissions protecting this endpoint - // if (cluster.serviceAccountToken) { - // options.headers = { - // Authorization: `Bearer ${cluster.serviceAccountToken}`, - // }; - // } - const logger = this.logger.child({ cluster: originalCluster.name }); middleware = createProxyMiddleware({ logProvider: () => logger, @@ -146,19 +161,18 @@ export class KubernetesProxy { request: { method: req.method, url: req.originalUrl }, response: { statusCode: 500 }, }; - res.status(500).json(body); }, onProxyReq: (proxyReq, req) => { // the kubernetes proxy endpoint expects a header field labeled `Backstage-Kubernetes-Authorization` that will be used to authenticate with the Kubernetes Api. The token provided as a value should be an bearer token for the target cluster. - const token = req.header(HEADER_KUBERNETES_AUTH) ?? ''; - proxyReq.setHeader('Authorization', token); + if (req.header(HEADER_KUBERNETES_AUTH)) { + const token = req.header(HEADER_KUBERNETES_AUTH) ?? ''; + proxyReq.setHeader('Authorization', token); + } }, }); - this.middlewareForClusterName.set(originalCluster.name, middleware); } - return middleware; } diff --git a/plugins/kubernetes-backend/src/service/index.ts b/plugins/kubernetes-backend/src/service/index.ts index 2f3e907879..9c0911de37 100644 --- a/plugins/kubernetes-backend/src/service/index.ts +++ b/plugins/kubernetes-backend/src/service/index.ts @@ -21,5 +21,8 @@ export { KubernetesProxy, HEADER_KUBERNETES_AUTH, } from './KubernetesProxy'; -export type { KubernetesProxyCreateRequestHandlerOptions } from './KubernetesProxy'; +export type { + KubernetesProxyCreateRequestHandlerOptions, + KubernetesProxyOptions, +} from './KubernetesProxy'; export * from './router';