diff --git a/.changeset/two-dingos-dream.md b/.changeset/two-dingos-dream.md new file mode 100644 index 0000000000..4fbe7651c9 --- /dev/null +++ b/.changeset/two-dingos-dream.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes-react': patch +--- + +The kubernetes APIs invokes Authentication Strategies when Backstage-Kubernetes-Authorization-X-X headers are provided, this enable the possibility to invoke strategies that executes additional steps to get a kubernetes token like on pinniped or custom strategies diff --git a/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts b/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts index 7d2ef36602..fd999ab335 100644 --- a/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts +++ b/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts @@ -398,9 +398,26 @@ describe('KubernetesBackendClient', () => { identityApi.getCredentials.mockResolvedValue({ token: 'idToken' }); }); - it('hits the /proxy API', async () => { + it('hits the /proxy API with oidc as protocol and okta as auth provider', async () => { + worker.use( + rest.get( + 'http://localhost:1234/api/kubernetes/clusters', + (_, res, ctx) => + res( + ctx.json({ + items: [ + { + name: 'cluster-a', + authProvider: 'oidc', + oidcTokenProvider: 'okta', + }, + ], + }), + ), + ), + ); kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({ - token: 'k8-token', + token: 'k8-token3', }); const nsResponse = { kind: 'Namespace', @@ -414,8 +431,56 @@ describe('KubernetesBackendClient', () => { 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', (req, res, ctx) => res( - req.headers.get('Backstage-Kubernetes-Authorization') === - 'Bearer k8-token' + req.headers.get( + 'Backstage-Kubernetes-Authorization-oidc-okta', + ) === 'k8-token3' + ? 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 the /proxy API with serviceAccount as auth provider', async () => { + worker.use( + rest.get( + 'http://localhost:1234/api/kubernetes/clusters', + (_, res, ctx) => + res( + ctx.json({ + items: [ + { + name: 'cluster-a', + authProvider: 'serviceAccount', + }, + ], + }), + ), + ), + ); + + 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('Authorization') === 'Bearer idToken' ? ctx.json(nsResponse) : ctx.status(403), ), @@ -450,8 +515,8 @@ describe('KubernetesBackendClient', () => { 'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces', (req, res, ctx) => res( - req.headers.get('Backstage-Kubernetes-Authorization') === - 'Bearer k8-token' + req.headers.get('Backstage-Kubernetes-Authorization-aws') === + 'k8-token' ? ctx.json(nsResponse) : ctx.status(403), ), diff --git a/plugins/kubernetes-react/src/api/KubernetesBackendClient.ts b/plugins/kubernetes-react/src/api/KubernetesBackendClient.ts index b483c267d8..d4e11c43da 100644 --- a/plugins/kubernetes-react/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes-react/src/api/KubernetesBackendClient.ts @@ -75,9 +75,11 @@ export class KubernetesBackendClient implements KubernetesApi { return this.handleResponse(response); } - private async getCluster( - clusterName: string, - ): Promise<{ name: string; authProvider: string }> { + private async getCluster(clusterName: string): Promise<{ + name: string; + authProvider: string; + oidcTokenProvider?: string; + }> { const cluster = await this.getClusters().then(clusters => clusters.find(c => c.name === clusterName), ); @@ -140,23 +142,62 @@ export class KubernetesBackendClient implements KubernetesApi { path: string; init?: RequestInit; }): Promise { - const { authProvider } = await this.getCluster(options.clusterName); - const { token: k8sToken } = await this.getCredentials(authProvider); + const { authProvider, oidcTokenProvider } = await this.getCluster( + options.clusterName, + ); + const kubernetesCredentials = await this.getCredentials(authProvider); const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/proxy${ options.path }`; const identityResponse = await this.identityApi.getCredentials(); - const headers = { + const headers = KubernetesBackendClient.getKubernetesHeaders( + options, + kubernetesCredentials?.token, + identityResponse, + authProvider, + oidcTokenProvider, + ); + return await fetch(url, { ...options.init, headers }); + } + + private static getKubernetesHeaders( + options: { + clusterName: string; + path: string; + init?: RequestInit; + }, + k8sToken: string | undefined, + identityResponse: { token?: string }, + authProvider: string, + oidcTokenProvider: string | undefined, + ) { + const kubernetesAuthHeader = + KubernetesBackendClient.getKubernetesAuthHeaderByAuthProvider( + authProvider, + oidcTokenProvider, + ); + return { ...options.init?.headers, [`Backstage-Kubernetes-Cluster`]: options.clusterName, ...(k8sToken && { - [`Backstage-Kubernetes-Authorization`]: `Bearer ${k8sToken}`, + [kubernetesAuthHeader]: k8sToken, }), ...(identityResponse.token && { Authorization: `Bearer ${identityResponse.token}`, }), }; + } - return await fetch(url, { ...options.init, headers }); + private static getKubernetesAuthHeaderByAuthProvider( + authProvider: string, + oidcTokenProvider: string | undefined, + ): string { + let header: string = 'Backstage-Kubernetes-Authorization'; + + header = header.concat('-', authProvider); + + if (oidcTokenProvider) header = header.concat('-', oidcTokenProvider); + + return header; } }