fix:addressing PR comments

Signed-off-by: Ruben Vallejo <rvallejo@vmware.com>
This commit is contained in:
Ruben Vallejo
2023-03-29 17:08:41 -04:00
parent c159ab64a6
commit 6bea150df5
8 changed files with 384 additions and 102 deletions
+12 -4
View File
@@ -185,7 +185,9 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider {
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody>;
// (undocumented)
getBearerToken(): Promise<string>;
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<KubernetesRequestBody>;
// (undocumented)
getBearerToken(authProvider: string): Promise<string>;
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<KubernetesRequestBody>;
// (undocumented)
getBearerToken(authProvider: string): Promise<string>;
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<KubernetesRequestBody>;
// (undocumented)
getBearerToken(): Promise<string>;
getCredentials(): Promise<{
token: string;
}>;
}
// Warning: (ae-forgotten-export) The symbol "ServicesAccordionsProps" needs to be exported by the entry point index.d.ts
@@ -32,7 +32,7 @@ describe('KubernetesBackendClient', () => {
let backendClient: KubernetesBackendClient;
const kubernetesAuthProvidersApi: jest.Mocked<KubernetesAuthProvidersApi> = {
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);
});
});
});
@@ -87,8 +87,10 @@ export class KubernetesBackendClient implements KubernetesApi {
return cluster;
}
private async getBearerToken(authProvider: string): Promise<string> {
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<Response> {
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 });
}
}
@@ -28,7 +28,7 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider {
async decorateRequestBodyForAuth(
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody> {
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<string> {
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',
),
};
}
}
@@ -92,12 +92,12 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi {
);
}
async getBearerToken(authProvider: string): Promise<string> {
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.')) {
@@ -30,7 +30,7 @@ export class OidcKubernetesAuthProvider implements KubernetesAuthProvider {
async decorateRequestBodyForAuth(
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody> {
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<string> {
return await this.authProvider.getIdToken();
async getCredentials(): Promise<{ token: string }> {
return {
token: await this.authProvider.getIdToken(),
};
}
}
@@ -32,7 +32,7 @@ export class ServerSideKubernetesAuthProvider
return requestBody;
}
async getBearerToken(): Promise<string> {
return '';
async getCredentials(): Promise<{ token: string }> {
return { token: '' };
}
}
@@ -21,7 +21,7 @@ export interface KubernetesAuthProvider {
decorateRequestBodyForAuth(
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody>;
getBearerToken(): Promise<string>;
getCredentials(): Promise<{ token: string }>;
}
export const kubernetesAuthProvidersApiRef =
@@ -34,5 +34,5 @@ export interface KubernetesAuthProvidersApi {
authProvider: string,
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody>;
getBearerToken(authProvider: string): Promise<string>;
getCredentials(authProvider: string): Promise<{ token: string }>;
}