diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index 7c150689b4..a5f72861da 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -185,7 +185,9 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider { requestBody: KubernetesRequestBody, ): Promise; // (undocumented) - getBearerToken(): Promise; + getCredentials(): Promise<{ + token: string; + }>; } // Warning: (ae-missing-release-tag) "GroupedResponses" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -290,7 +292,9 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { requestBody: KubernetesRequestBody, ): Promise; // (undocumented) - getBearerToken(authProvider: string): Promise; + getCredentials(authProvider: string): Promise<{ + token: string; + }>; } // Warning: (ae-missing-release-tag) "KubernetesAuthProvidersApi" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -303,7 +307,9 @@ export interface KubernetesAuthProvidersApi { requestBody: KubernetesRequestBody, ): Promise; // (undocumented) - getBearerToken(authProvider: string): Promise; + getCredentials(authProvider: string): Promise<{ + token: string; + }>; } // Warning: (ae-missing-release-tag) "kubernetesAuthProvidersApiRef" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -436,7 +442,9 @@ export class ServerSideKubernetesAuthProvider requestBody: KubernetesRequestBody, ): Promise; // (undocumented) - getBearerToken(): Promise; + getCredentials(): Promise<{ + token: string; + }>; } // Warning: (ae-forgotten-export) The symbol "ServicesAccordionsProps" needs to be exported by the entry point index.d.ts diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.test.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.test.ts index ec18dd50bb..2c01f15b18 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.test.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.test.ts @@ -32,7 +32,7 @@ describe('KubernetesBackendClient', () => { let backendClient: KubernetesBackendClient; const kubernetesAuthProvidersApi: jest.Mocked = { decorateRequestBodyForAuth: jest.fn(), - getBearerToken: jest.fn(), + getCredentials: jest.fn(), }; let mockResponse: ObjectsByEntityResponse; const worker = setupServer(); @@ -100,6 +100,32 @@ describe('KubernetesBackendClient', () => { ]); }); + it('/clusters API throws a 404 Error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) => + res(ctx.status(404)), + ), + ); + + await expect(backendClient.getClusters()).rejects.toThrow( + 'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.', + ); + }); + + it('/clusters API throws a 500 Error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) => + res(ctx.status(500)), + ), + ); + + await expect(backendClient.getClusters()).rejects.toThrow( + 'Request failed with 500 Internal Server Error, ', + ); + }); + it('hits the /resources/custom/query API', async () => { identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); worker.use( @@ -133,7 +159,75 @@ describe('KubernetesBackendClient', () => { expect(customObject).toStrictEqual(mockResponse); }); - it('hits the /services/{entityName} api', async () => { + it('/resources/custom/query API throws a 404 error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.post( + 'http://localhost:1234/api/kubernetes/resources/custom/query', + (_, res, ctx) => res(ctx.status(404)), + ), + ); + + const request: CustomObjectsByEntityRequest = { + auth: {}, + customResources: [ + { + group: 'test-group', + apiVersion: 'v1', + plural: 'none', + }, + ], + entity: { + apiVersion: 'v1', + kind: 'pod', + metadata: { + name: 'test-name', + }, + }, + }; + + const response = backendClient.getCustomObjectsByEntity(request); + + await expect(response).rejects.toThrow( + 'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.', + ); + }); + + it('/resources/custom/query API throws a 500 error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.post( + 'http://localhost:1234/api/kubernetes/resources/custom/query', + (_, res, ctx) => res(ctx.status(500)), + ), + ); + + const request: CustomObjectsByEntityRequest = { + auth: {}, + customResources: [ + { + group: 'test-group', + apiVersion: 'v1', + plural: 'none', + }, + ], + entity: { + apiVersion: 'v1', + kind: 'pod', + metadata: { + name: 'test-name', + }, + }, + }; + + const response = backendClient.getCustomObjectsByEntity(request); + + await expect(response).rejects.toThrow( + 'Request failed with 500 Internal Server Error, ', + ); + }); + + it('hits the /services/{entityName} API', async () => { identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); worker.use( rest.post( @@ -158,6 +252,58 @@ describe('KubernetesBackendClient', () => { expect(entityObject).toStrictEqual(mockResponse); }); + it('services/{entityName} API throws a 404 error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.post( + 'http://localhost:1234/api/kubernetes/services/test-name', + (_, res, ctx) => res(ctx.status(404)), + ), + ); + + const request: KubernetesRequestBody = { + entity: { + apiVersion: 'v1', + kind: 'pod', + metadata: { + name: 'test-name', + }, + }, + }; + + const response = backendClient.getObjectsByEntity(request); + + await expect(response).rejects.toThrow( + 'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.', + ); + }); + + it('services/{entityName} API throws a 500 error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + worker.use( + rest.post( + 'http://localhost:1234/api/kubernetes/services/test-name', + (_, res, ctx) => res(ctx.status(500)), + ), + ); + + const request: KubernetesRequestBody = { + entity: { + apiVersion: 'v1', + kind: 'pod', + metadata: { + name: 'test-name', + }, + }, + }; + + const response = backendClient.getObjectsByEntity(request); + + await expect(response).rejects.toThrow( + 'Request failed with 500 Internal Server Error, ', + ); + }); + it('hits the /resources/workloads/query API', async () => { identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); worker.use( @@ -184,83 +330,210 @@ describe('KubernetesBackendClient', () => { expect(response).toStrictEqual(mockResponse); }); - it('hits the /proxy API', async () => { - identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); - kubernetesAuthProvidersApi.getBearerToken.mockResolvedValue( - 'Bearer k8-token', - ); - const nsResponse = { - kind: 'Namespace', - apiVersion: 'v1', - metadata: { - name: 'new-ns', - }, - }; - worker.use( - rest.get( - 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', - (_, res, ctx) => res(ctx.json(nsResponse)), - ), - rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) => - res(ctx.json({ items: [{ name: 'cluster-a', authProvider: 'aws' }] })), - ), - ); - - const request = { - clusterName: 'cluster-a', - path: '/api/v1/namespaces', - }; - - const response = await backendClient.proxy(request); - - expect(response).toStrictEqual(nsResponse); - }); - - it('/proxy API throws a ERROR_NOT_FOUND if the cluster in the request is not found', async () => { + it('/resources/workloads/query API throws a 404 error', async () => { identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); worker.use( - rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) => - res(ctx.json({ items: [{ name: 'cluster-b', authProvider: 'aws' }] })), + rest.post( + 'http://localhost:1234/api/kubernetes/resources/workloads/query', + (_, res, ctx) => res(ctx.status(404)), ), ); - const request = { - clusterName: 'cluster-a', - path: '/api/v1/namespaces', - }; - - await expect(backendClient.proxy(request)).rejects.toThrow(NotFoundError); - }); - - it('hits /proxy api when signed in as a guest', async () => { - // when a user is signed in as a guest the result of the getCredentials() method resolves to the {} value. - identityApi.getCredentials.mockResolvedValue({}); - kubernetesAuthProvidersApi.getBearerToken.mockResolvedValue( - 'Bearer k8-token', - ); - const nsResponse = { - kind: 'Namespace', - apiVersion: 'v1', - metadata: { - name: 'new-ns', + const request: WorkloadsByEntityRequest = { + auth: {}, + entity: { + apiVersion: 'v1', + kind: 'pod', + metadata: { + name: 'test-name', + }, }, }; + + const response = backendClient.getWorkloadsByEntity(request); + + await expect(response).rejects.toThrow( + 'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.', + ); + }); + + it('/resources/workloads/query API throws a 500 error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); worker.use( - rest.get( - 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', - (_, res, ctx) => res(ctx.status(200), ctx.json(nsResponse)), - ), - rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) => - res(ctx.json({ items: [{ name: 'cluster-a', authProvider: 'aws' }] })), + rest.post( + 'http://localhost:1234/api/kubernetes/resources/workloads/query', + (_, res, ctx) => res(ctx.status(500)), ), ); - const request = { - clusterName: 'cluster-a', - path: '/api/v1/namespaces', + const request: WorkloadsByEntityRequest = { + auth: {}, + entity: { + apiVersion: 'v1', + kind: 'pod', + metadata: { + name: 'test-name', + }, + }, }; - const response = await backendClient.proxy(request); - expect(response).toStrictEqual(nsResponse); + const response = backendClient.getWorkloadsByEntity(request); + + await expect(response).rejects.toThrow( + 'Request failed with 500 Internal Server Error, ', + ); + }); + + describe('proxy', () => { + beforeEach(() => { + worker.use( + rest.get( + 'http://localhost:1234/api/kubernetes/clusters', + (_, res, ctx) => + res( + ctx.json({ items: [{ name: 'cluster-a', authProvider: 'aws' }] }), + ), + ), + ); + }); + + it('hits the /proxy API', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({ + token: 'k8-token', + }); + const nsResponse = { + kind: 'Namespace', + apiVersion: 'v1', + metadata: { + name: 'new-ns', + }, + }; + worker.use( + rest.get( + 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', + (req, res, ctx) => + res( + req.headers.get('Backstage-Kubernetes-Authorization') === + 'Bearer k8-token' + ? ctx.json(nsResponse) + : ctx.status(403), + ), + ), + ); + + const request = { + clusterName: 'cluster-a', + path: '/api/v1/namespaces', + }; + + const response = await backendClient.proxy(request); + + await expect(response.json()).resolves.toStrictEqual(nsResponse); + }); + + it('hits /proxy api when signed in as a guest', async () => { + // when a user is signed in as a guest the result of the getCredentials() method resolves to the {} value. + identityApi.getCredentials.mockResolvedValue({}); + kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({ + token: 'k8-token', + }); + const nsResponse = { + kind: 'Namespace', + apiVersion: 'v1', + metadata: { + name: 'new-ns', + }, + }; + worker.use( + rest.get( + 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', + (req, res, ctx) => + res( + req.headers.get('Backstage-Kubernetes-Authorization') === + 'Bearer k8-token' + ? ctx.json(nsResponse) + : ctx.status(403), + ), + ), + ); + + const request = { + clusterName: 'cluster-a', + path: '/api/v1/namespaces', + }; + + const response = await backendClient.proxy(request); + await expect(response.json()).resolves.toStrictEqual(nsResponse); + }); + + it('/proxy API throws a 404 error', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({ + token: 'k8-token', + }); + worker.use( + rest.get( + 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', + (_, res, ctx) => res(ctx.status(404)), + ), + ); + + const request = { + clusterName: 'cluster-a', + path: '/api/v1/namespaces', + }; + + const response = await backendClient.proxy(request); + + expect(response.status).toEqual(404); + }); + + it('throws a ERROR_NOT_FOUND if the cluster in the request is not found', async () => { + identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); + + const request = { + clusterName: 'cluster-b', + path: '/api/v1/namespaces', + }; + + await expect(backendClient.proxy(request)).rejects.toThrow(NotFoundError); + }); + + it('responds with an 403 error when invalid k8 token is provided', async () => { + // when a user is signed in as a guest the result of the getCredentials() method resolves to the {} value. + identityApi.getCredentials.mockResolvedValue({}); + kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({ + token: 'wrong-token', + }); + + const nsResponse = { + kind: 'Namespace', + apiVersion: 'v1', + metadata: { + name: 'new-ns', + }, + }; + worker.use( + rest.get( + 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', + (req, res, ctx) => + res( + req.headers.get('Backstage-Kubernetes-Authorization') === + 'Bearer k8-token' + ? ctx.json(nsResponse) + : ctx.status(403), + ), + ), + ); + + const request = { + clusterName: 'cluster-a', + path: '/api/v1/namespaces', + }; + + const response = await backendClient.proxy(request); + expect(response.status).toEqual(403); + }); }); }); diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index dbeb1c3035..bc510589c8 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -87,8 +87,10 @@ export class KubernetesBackendClient implements KubernetesApi { return cluster; } - private async getBearerToken(authProvider: string): Promise { - return await this.kubernetesAuthProvidersApi.getBearerToken(authProvider); + private async getCredentials( + authProvider: string, + ): Promise<{ token: string }> { + return await this.kubernetesAuthProvidersApi.getCredentials(authProvider); } async getObjectsByEntity( @@ -138,25 +140,20 @@ export class KubernetesBackendClient implements KubernetesApi { init?: RequestInit; }): Promise { const { authProvider } = await this.getCluster(options.clusterName); - const k8sToken = await this.getBearerToken(authProvider); + const { token: k8sToken } = await this.getCredentials(authProvider); const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/proxy${ options.path }`; const identityResponse = await this.identityApi.getCredentials(); - const headers = identityResponse.token - ? { - ...options.init?.headers, - [`Backstage-Kubernetes-Cluster`]: options.clusterName, - [`Backstage-Kubernetes-Authorization`]: `Bearer ${k8sToken}`, - Authorization: `Bearer ${identityResponse.token}`, - } - : { - ...options.init?.headers, - [`Backstage-Kubernetes-Cluster`]: options.clusterName, - [`Backstage-Kubernetes-Authorization`]: `Bearer ${k8sToken}`, - }; - const response = await fetch(url, { ...options.init, headers }); + const headers = { + ...options.init?.headers, + [`Backstage-Kubernetes-Cluster`]: options.clusterName, + [`Backstage-Kubernetes-Authorization`]: `Bearer ${k8sToken}`, + ...(identityResponse.token && { + Authorization: `Bearer ${identityResponse.token}`, + }), + }; - return this.handleResponse(response); + return await fetch(url, { ...options.init, headers }); } } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts index 4b46884b39..5652549b8a 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts @@ -28,7 +28,7 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider { async decorateRequestBodyForAuth( requestBody: KubernetesRequestBody, ): Promise { - const googleAuthToken: string = await this.getBearerToken(); + const googleAuthToken: string = (await this.getCredentials()).token; if ('auth' in requestBody) { requestBody.auth!.google = googleAuthToken; } else { @@ -36,9 +36,11 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider { } return requestBody; } - async getBearerToken(): Promise { - return await this.authProvider.getAccessToken( - 'https://www.googleapis.com/auth/cloud-platform', - ); + async getCredentials(): Promise<{ token: string }> { + return { + token: await this.authProvider.getAccessToken( + 'https://www.googleapis.com/auth/cloud-platform', + ), + }; } } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts index 45ba243d23..1873376487 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts @@ -92,12 +92,12 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { ); } - async getBearerToken(authProvider: string): Promise { + async getCredentials(authProvider: string): Promise<{ token: string }> { const kubernetesAuthProvider: KubernetesAuthProvider | undefined = this.kubernetesAuthProviderMap.get(authProvider); if (kubernetesAuthProvider) { - return await kubernetesAuthProvider.getBearerToken(); + return await kubernetesAuthProvider.getCredentials(); } if (authProvider.startsWith('oidc.')) { diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts index 9d9d34fe1d..bd5760bde2 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/OidcKubernetesAuthProvider.ts @@ -30,7 +30,7 @@ export class OidcKubernetesAuthProvider implements KubernetesAuthProvider { async decorateRequestBodyForAuth( requestBody: KubernetesRequestBody, ): Promise { - const authToken: string = await this.getBearerToken(); + const authToken: string = (await this.getCredentials()).token; const auth = { ...requestBody.auth }; if (auth.oidc) { auth.oidc[this.providerName] = authToken; @@ -41,7 +41,9 @@ export class OidcKubernetesAuthProvider implements KubernetesAuthProvider { return requestBody; } - async getBearerToken(): Promise { - return await this.authProvider.getIdToken(); + async getCredentials(): Promise<{ token: string }> { + return { + token: await this.authProvider.getIdToken(), + }; } } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/ServerSideAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/ServerSideAuthProvider.ts index 9290a2eda3..8194f1e213 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/ServerSideAuthProvider.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/ServerSideAuthProvider.ts @@ -32,7 +32,7 @@ export class ServerSideKubernetesAuthProvider return requestBody; } - async getBearerToken(): Promise { - return ''; + async getCredentials(): Promise<{ token: string }> { + return { token: '' }; } } diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/types.ts b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts index 395db3c1bd..117c966d36 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/types.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts @@ -21,7 +21,7 @@ export interface KubernetesAuthProvider { decorateRequestBodyForAuth( requestBody: KubernetesRequestBody, ): Promise; - getBearerToken(): Promise; + getCredentials(): Promise<{ token: string }>; } export const kubernetesAuthProvidersApiRef = @@ -34,5 +34,5 @@ export interface KubernetesAuthProvidersApi { authProvider: string, requestBody: KubernetesRequestBody, ): Promise; - getBearerToken(authProvider: string): Promise; + getCredentials(authProvider: string): Promise<{ token: string }>; }