Backstage-Kubernetes-Authorization-X-X headers are provided when it gets invoke kubernetes endpoints on backstage

Signed-off-by: Andres Mauricio Gomez P <andmagom@outlook.com>
This commit is contained in:
Andres Mauricio Gomez P
2023-09-28 10:52:45 -05:00
parent e4a83c1b85
commit 5dac12e435
3 changed files with 126 additions and 14 deletions
+6
View File
@@ -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
@@ -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),
),
@@ -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<Response> {
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;
}
}