From c75db01052a9cb0c627044336c103bdc3976135d Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Fri, 24 Mar 2023 14:21:37 +0000 Subject: [PATCH 01/13] port k8s plugin from aws v2 to v3 client Signed-off-by: Brian Fletcher --- plugins/kubernetes-backend/package.json | 5 +- .../AwsIamKubernetesAuthTranslator.test.ts | 195 +++++++----------- .../AwsIamKubernetesAuthTranslator.ts | 135 +++++------- yarn.lock | 7 +- 4 files changed, 143 insertions(+), 199 deletions(-) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 8f7b071c9a..141bf56ad5 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -34,6 +34,9 @@ "clean": "backstage-cli package clean" }, "dependencies": { + "@aws-crypto/sha256-js": "^3.0.0", + "@aws-sdk/credential-providers": "^3.208.0", + "@aws-sdk/signature-v4": "^3.208.0", "@azure/identity": "^2.0.4", "@backstage/backend-common": "workspace:^", "@backstage/catalog-client": "workspace:^", @@ -49,7 +52,6 @@ "@kubernetes/client-node": "0.18.1", "@types/express": "^4.17.6", "@types/luxon": "^3.0.0", - "aws-sdk": "^2.840.0", "aws4": "^1.11.0", "compression": "^1.7.4", "cors": "^2.8.5", @@ -71,7 +73,6 @@ "@backstage/cli": "workspace:^", "@types/aws4": "^1.5.1", "@types/http-proxy-middleware": "^0.19.3", - "aws-sdk-mock": "^5.2.1", "mock-fs": "^5.2.0", "msw": "^1.0.0", "supertest": "^6.1.3" diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts index 1f8d525ea8..97272f9faa 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts @@ -13,148 +13,111 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import AWS from 'aws-sdk'; -import AWSMock from 'aws-sdk-mock'; import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator'; -const awsError: AWS.AWSError = { - code: '123', - message: 'no way', - name: 'nope', - time: new Date(), -}; +let presign = jest.fn(async () => ({ + hostname: 'https://example.com', + query: {}, + path: '/asdf', +})); + +const fromEnv = jest.fn(() => { + return { + AccessId: 'asdf', + }; +}); +const fromTemporaryCredentials = jest.fn(); +jest.mock('@aws-sdk/signature-v4', () => ({ + SignatureV4: jest.fn().mockImplementation(() => ({ + presign, + })), +})); + +jest.mock('@aws-sdk/credential-providers', () => ({ + fromEnv: () => fromEnv(), + fromTemporaryCredentials: (opts: any) => fromTemporaryCredentials(opts), +})); describe('AwsIamKubernetesAuthTranslator tests', () => { - let role: any = undefined; - const credentials: any = { - accessKeyId: 'bloop', - secretAccessKey: 'omg-so-secret', - sessionToken: 'token', - }; - - let assumeResponse: any = { - Credentials: { - AccessKeyId: credentials.accessKeyId, - SecretAccessKey: credentials.secretAccessKey, - SessionToken: credentials.sessionToken, - }, - }; - - let mockedCredentials: any = undefined; - - AWS.config.credentials = new AWS.Credentials(credentials); - - AWSMock.setSDKInstance(AWS); - - beforeEach(() => { - jest.resetAllMocks(); - }); - - afterAll(() => { - jest.resetAllMocks(); - }); - - function executeTranslation() { - AWSMock.mock('STS', 'assumeRole', (_params: any, callback: Function) => { - callback(null, assumeResponse); - }); - + beforeEach(() => {}); + it('returns a signed url for AWS credentials without assume role', async () => { const authTranslator = new AwsIamKubernetesAuthTranslator(); - if (mockedCredentials) { - jest - .spyOn(authTranslator, 'awsGetCredentials') - .mockImplementation(async () => mockedCredentials); - } - - const response = authTranslator.decorateClusterDetailsWithAuth({ - assumeRole: role, + const authPromise = authTranslator.decorateClusterDetailsWithAuth({ name: 'test-cluster', url: '', authProvider: 'aws', }); - - mockedCredentials = undefined; - - return response; - } - - it('returns a signed url for AWS credentials', async () => { - // These credentials are not real. - // Pulled from example in docs: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html - AWS.config.credentials = new AWS.Credentials( - 'AKIAIOSFODNN7EXAMPLE', - 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + expect(fromEnv).toHaveBeenCalledWith(); + expect((await authPromise).serviceAccountToken).toEqual( + 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', ); + }); - const response = await executeTranslation(); - expect(response.serviceAccountToken).toBeDefined(); + it('returns a signed url for AWS credentials with assume role', async () => { + const authTranslator = new AwsIamKubernetesAuthTranslator(); + + const authPromise = authTranslator.decorateClusterDetailsWithAuth({ + assumeRole: 'asdf', + name: 'test-cluster', + url: '', + authProvider: 'aws', + }); + expect(fromTemporaryCredentials).toHaveBeenCalledWith({ + clientConfig: { + region: 'us-east-1', + }, + masterCredentials: fromEnv(), + params: { + ExternalId: undefined, + RoleArn: 'asdf', + }, + }); + expect((await authPromise).serviceAccountToken).toEqual( + 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', + ); + }); + + it('returns a signed url for AWS credentials and passes the external id', async () => { + const authTranslator = new AwsIamKubernetesAuthTranslator(); + + const authPromise = authTranslator.decorateClusterDetailsWithAuth({ + assumeRole: 'SomeRole', + externalId: 'external-id', + name: 'test-cluster', + url: '', + authProvider: 'aws', + }); + expect(fromTemporaryCredentials).toHaveBeenCalledWith({ + clientConfig: { + region: 'us-east-1', + }, + masterCredentials: fromEnv(), + params: { + ExternalId: 'external-id', + RoleArn: 'SomeRole', + }, + }); + expect((await authPromise).serviceAccountToken).toEqual( + 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', + ); }); describe('When the credentials is failing', () => { beforeEach(() => { - jest.spyOn(AWS.config, 'getCredentials').mockImplementation(cb => { - cb(awsError, null); + presign = jest.fn(async () => { + throw new Error('no way'); }); }); it('throws the right error', async () => { const authTranslator = new AwsIamKubernetesAuthTranslator(); await expect( authTranslator.decorateClusterDetailsWithAuth({ - assumeRole: role, name: 'test-cluster', url: '', authProvider: 'aws', }), - ).rejects.toEqual(awsError); - }); - }); - - describe('When the role is assumed', () => { - // These credentials are not real. - // Pulled from example in docs: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html - AWS.config.credentials = new AWS.Credentials( - 'AKIAIOSFODNN7EXAMPLE', - 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', - ); - role = 'SomeRole'; - - describe('When the role is valid', () => { - it('returns a signed url for AWS credentials', async () => { - const response = await executeTranslation(); - expect(response.serviceAccountToken).toBeDefined(); - }); - }); - - describe('When the role is invalid', () => { - it('returns the original AWS credentials', async () => { - assumeResponse = undefined; - await expect(executeTranslation()).rejects.toThrow( - /Unable to assume role:/, - ); - }); - }); - }); - - describe('When no AWS creds are available', () => { - it('throws unable to get AWS credentials', async () => { - mockedCredentials = new Error(); - await expect(executeTranslation()).rejects.toThrow( - 'No AWS credentials found.', - ); - }); - }); - - describe('When invalid AWS creds are available', () => { - it('throws credentials are invalid to get AWS credentials', async () => { - const undefinedSecret: any = undefined; - AWS.config.credentials = new AWS.Credentials( - 'AKIAIOSFODNN7EXAMPLE', - undefinedSecret, - ); - await expect(executeTranslation()).rejects.toThrow( - 'Invalid AWS credentials found.', - ); + ).rejects.toThrow('no way'); }); }); }); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts index 84e60c9c69..40f74136ea 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts @@ -13,10 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import AWS, { Credentials } from 'aws-sdk'; -import { sign } from 'aws4'; import { AWSClusterDetails } from '../types/types'; import { KubernetesAuthTranslator } from './types'; +import { + fromEnv, + fromTemporaryCredentials, +} from '@aws-sdk/credential-providers'; +import { SignatureV4 } from '@aws-sdk/signature-v4'; +import { Sha256 } from '@aws-crypto/sha256-js'; /** * @@ -28,6 +32,8 @@ export type SigningCreds = { sessionToken: string | undefined; }; +const defaultRegion = 'us-east-1'; + /** * * @public @@ -35,90 +41,63 @@ export type SigningCreds = { export class AwsIamKubernetesAuthTranslator implements KubernetesAuthTranslator { - validCredentials(creds: SigningCreds): boolean { - return (creds?.accessKeyId && creds?.secretAccessKey) as unknown as boolean; - } - - awsGetCredentials = async (): Promise => { - return new Promise((resolve, reject) => { - AWS.config.getCredentials(err => { - if (err) { - return reject(err); - } - - return resolve(AWS.config.credentials as Credentials); - }); - }); - }; - - async getCredentials( - assumeRole?: string, - externalId?: string, - ): Promise { - const awsCreds = await this.awsGetCredentials(); - - if (!(awsCreds instanceof Credentials)) - throw new Error('No AWS credentials found.'); - - let creds: SigningCreds = { - accessKeyId: awsCreds.accessKeyId, - secretAccessKey: awsCreds.secretAccessKey, - sessionToken: awsCreds.sessionToken, - }; - - if (!this.validCredentials(creds)) - throw new Error('Invalid AWS credentials found.'); - if (!assumeRole) return creds; - - try { - const params: AWS.STS.Types.AssumeRoleRequest = { - RoleArn: assumeRole, - RoleSessionName: 'backstage-login', - }; - if (externalId) params.ExternalId = externalId; - - const assumedRole = await new AWS.STS().assumeRole(params).promise(); - - if (!assumedRole.Credentials) { - throw new Error(`No credentials returned for role ${assumeRole}`); - } - - creds = { - accessKeyId: assumedRole.Credentials.AccessKeyId, - secretAccessKey: assumedRole.Credentials.SecretAccessKey, - sessionToken: assumedRole.Credentials.SessionToken, - }; - } catch (e) { - console.warn(`There was an error assuming the role: ${e}`); - throw new Error(`Unable to assume role: ${e}`); - } - return creds; - } - async getBearerToken( + private async getBearerToken( clusterName: string, assumeRole?: string, externalId?: string, ): Promise { - const credentials = await this.getCredentials(assumeRole, externalId); + const region = process.env.AWS_REGION ?? defaultRegion; + let credentials = fromEnv(); + if (assumeRole) { + credentials = fromTemporaryCredentials({ + masterCredentials: credentials, + clientConfig: { + region, + }, + params: { + RoleArn: assumeRole, + ExternalId: externalId, + }, + }); + } - const request = { - host: `sts.amazonaws.com`, - path: `/?Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Expires=60`, - headers: { - 'x-k8s-aws-id': clusterName, + const signer = new SignatureV4({ + credentials, + region, + service: 'sts', + sha256: Sha256, + }); + + const request = await signer.presign( + { + headers: { + host: `sts.${region}.amazonaws.com`, + 'x-k8s-aws-id': clusterName, + }, + hostname: `sts.${region}.amazonaws.com`, + method: 'GET', + path: '/', + protocol: 'https:', + query: { + Action: 'GetCallerIdentity', + Version: '2011-06-15', + }, }, - signQuery: true, - }; + { expiresIn: 0 }, + ); - const signed = sign(request, credentials); - const url = `https://${signed.host}${signed.path}`; - const base64Url = Buffer.from(url, 'binary').toString('base64'); - const urlSafeBase64Url = base64Url - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/, ''); + const query = Object.keys(request?.query ?? {}) + .map( + q => + `${encodeURIComponent(q)}=${encodeURIComponent( + request.query?.[q] as string, + )}`, + ) + .join('&'); - return `k8s-aws-v1.${urlSafeBase64Url}`; + const url = `https://${request.hostname}${request.path}?${query}`; + + return `k8s-aws-v1.${Buffer.from(url).toString('base64url')}`; } async decorateClusterDetailsWithAuth( diff --git a/yarn.lock b/yarn.lock index 93a6661725..9d43cb0b84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1404,7 +1404,7 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/signature-v4@npm:3.296.0": +"@aws-sdk/signature-v4@npm:3.296.0, @aws-sdk/signature-v4@npm:^3.208.0": version: 3.296.0 resolution: "@aws-sdk/signature-v4@npm:3.296.0" dependencies: @@ -6881,6 +6881,9 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-kubernetes-backend@workspace:plugins/kubernetes-backend" dependencies: + "@aws-crypto/sha256-js": ^3.0.0 + "@aws-sdk/credential-providers": ^3.208.0 + "@aws-sdk/signature-v4": ^3.208.0 "@azure/identity": ^2.0.4 "@backstage/backend-common": "workspace:^" "@backstage/backend-test-utils": "workspace:^" @@ -6900,8 +6903,6 @@ __metadata: "@types/express": ^4.17.6 "@types/http-proxy-middleware": ^0.19.3 "@types/luxon": ^3.0.0 - aws-sdk: ^2.840.0 - aws-sdk-mock: ^5.2.1 aws4: ^1.11.0 compression: ^1.7.4 cors: ^2.8.5 From 890988341e921ba75fa4c0b9551fd5eb586dce9a Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Fri, 24 Mar 2023 14:24:55 +0000 Subject: [PATCH 02/13] add changeset Signed-off-by: Brian Fletcher --- .changeset/warm-lamps-tease.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/warm-lamps-tease.md diff --git a/.changeset/warm-lamps-tease.md b/.changeset/warm-lamps-tease.md new file mode 100644 index 0000000000..d6ebc0366e --- /dev/null +++ b/.changeset/warm-lamps-tease.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Update aws sdk client from v2 to v3. From 90b502cf5b3bd208ed83e03de39d2404aaeb5ee8 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Fri, 24 Mar 2023 14:30:09 +0000 Subject: [PATCH 03/13] fix typo Signed-off-by: Brian Fletcher --- .changeset/warm-lamps-tease.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/warm-lamps-tease.md b/.changeset/warm-lamps-tease.md index d6ebc0366e..f1dfed65e0 100644 --- a/.changeset/warm-lamps-tease.md +++ b/.changeset/warm-lamps-tease.md @@ -2,4 +2,4 @@ '@backstage/plugin-kubernetes-backend': patch --- -Update aws sdk client from v2 to v3. +Update `aws-sdk` client from v2 to v3. From 6636e74a88d4b8b8b7d0e3ff08733acec3d51cbc Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Fri, 24 Mar 2023 15:05:34 +0000 Subject: [PATCH 04/13] fix api-reports Signed-off-by: Brian Fletcher --- plugins/kubernetes-backend/api-report.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 449131a26e..2b6387701b 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -5,7 +5,6 @@ ```ts import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; -import { Credentials } from 'aws-sdk'; import type { CustomResourceMatcher } from '@backstage/plugin-kubernetes-common'; import { Duration } from 'luxon'; import { Entity } from '@backstage/catalog-model'; @@ -34,25 +33,10 @@ export interface AWSClusterDetails extends ClusterDetails { export class AwsIamKubernetesAuthTranslator implements KubernetesAuthTranslator { - // (undocumented) - awsGetCredentials: () => Promise; // (undocumented) decorateClusterDetailsWithAuth( clusterDetails: AWSClusterDetails, ): Promise; - // (undocumented) - getBearerToken( - clusterName: string, - assumeRole?: string, - externalId?: string, - ): Promise; - // (undocumented) - getCredentials( - assumeRole?: string, - externalId?: string, - ): Promise; - // (undocumented) - validCredentials(creds: SigningCreds): boolean; } // @public (undocumented) From 192d0173a0daef9fcc8d2fb8b8d4c05fc9363a33 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 27 Mar 2023 09:15:52 +0100 Subject: [PATCH 05/13] use aws credentials provider Signed-off-by: Brian Fletcher --- .../AwsIamKubernetesAuthTranslator.test.ts | 63 ++++++++++++------- .../AwsIamKubernetesAuthTranslator.ts | 20 ++++-- .../KubernetesAuthTranslatorGenerator.test.ts | 15 +++-- .../KubernetesAuthTranslatorGenerator.ts | 4 +- .../src/service/KubernetesBuilder.ts | 1 + .../src/service/KubernetesFanOutHandler.ts | 5 ++ plugins/kubernetes-backend/src/types/types.ts | 2 + 7 files changed, 76 insertions(+), 34 deletions(-) diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts index 97272f9faa..e72291664b 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator'; +import { ConfigReader } from '@backstage/config'; let presign = jest.fn(async () => ({ hostname: 'https://example.com', @@ -21,65 +22,79 @@ let presign = jest.fn(async () => ({ path: '/asdf', })); -const fromEnv = jest.fn(() => { - return { - AccessId: 'asdf', - }; -}); -const fromTemporaryCredentials = jest.fn(); +const credsManager = { + getCredentialProvider: async () => ({ + sdkCredentialProvider: { + AccessKeyId: 'asdf', + }, + }), +}; + +jest.mock('@backstage/integration-aws-node', () => ({ + DefaultAwsCredentialsManager: { + fromConfig: () => credsManager, + }, +})); + +const config = new ConfigReader({}); + jest.mock('@aws-sdk/signature-v4', () => ({ SignatureV4: jest.fn().mockImplementation(() => ({ presign, })), })); +const fromTemporaryCredentials = jest.fn(); jest.mock('@aws-sdk/credential-providers', () => ({ - fromEnv: () => fromEnv(), - fromTemporaryCredentials: (opts: any) => fromTemporaryCredentials(opts), + fromTemporaryCredentials: (opts: any) => { + console.log(`got: ${JSON.stringify(opts)}`); + return fromTemporaryCredentials(opts); + }, })); describe('AwsIamKubernetesAuthTranslator tests', () => { beforeEach(() => {}); it('returns a signed url for AWS credentials without assume role', async () => { - const authTranslator = new AwsIamKubernetesAuthTranslator(); + const authTranslator = new AwsIamKubernetesAuthTranslator({ config }); const authPromise = authTranslator.decorateClusterDetailsWithAuth({ name: 'test-cluster', url: '', authProvider: 'aws', }); - expect(fromEnv).toHaveBeenCalledWith(); expect((await authPromise).serviceAccountToken).toEqual( 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', ); }); it('returns a signed url for AWS credentials with assume role', async () => { - const authTranslator = new AwsIamKubernetesAuthTranslator(); + const authTranslator = new AwsIamKubernetesAuthTranslator({ config }); const authPromise = authTranslator.decorateClusterDetailsWithAuth({ - assumeRole: 'asdf', + assumeRole: 'SomeRole', name: 'test-cluster', url: '', authProvider: 'aws', }); + expect((await authPromise).serviceAccountToken).toEqual( + 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', + ); expect(fromTemporaryCredentials).toHaveBeenCalledWith({ clientConfig: { region: 'us-east-1', }, - masterCredentials: fromEnv(), + masterCredentials: { + AccessKeyId: 'asdf', + }, params: { ExternalId: undefined, - RoleArn: 'asdf', + RoleArn: 'SomeRole', }, }); - expect((await authPromise).serviceAccountToken).toEqual( - 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', - ); }); it('returns a signed url for AWS credentials and passes the external id', async () => { - const authTranslator = new AwsIamKubernetesAuthTranslator(); + const authTranslator = new AwsIamKubernetesAuthTranslator({ config }); const authPromise = authTranslator.decorateClusterDetailsWithAuth({ assumeRole: 'SomeRole', @@ -88,19 +103,21 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { url: '', authProvider: 'aws', }); + expect((await authPromise).serviceAccountToken).toEqual( + 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', + ); expect(fromTemporaryCredentials).toHaveBeenCalledWith({ clientConfig: { region: 'us-east-1', }, - masterCredentials: fromEnv(), + masterCredentials: { + AccessKeyId: 'asdf', + }, params: { ExternalId: 'external-id', RoleArn: 'SomeRole', }, }); - expect((await authPromise).serviceAccountToken).toEqual( - 'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_', - ); }); describe('When the credentials is failing', () => { @@ -110,7 +127,7 @@ describe('AwsIamKubernetesAuthTranslator tests', () => { }); }); it('throws the right error', async () => { - const authTranslator = new AwsIamKubernetesAuthTranslator(); + const authTranslator = new AwsIamKubernetesAuthTranslator({ config }); await expect( authTranslator.decorateClusterDetailsWithAuth({ name: 'test-cluster', diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts index 40f74136ea..f5985b6a93 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts @@ -15,12 +15,14 @@ */ import { AWSClusterDetails } from '../types/types'; import { KubernetesAuthTranslator } from './types'; -import { - fromEnv, - fromTemporaryCredentials, -} from '@aws-sdk/credential-providers'; +import { fromTemporaryCredentials } from '@aws-sdk/credential-providers'; import { SignatureV4 } from '@aws-sdk/signature-v4'; import { Sha256 } from '@aws-crypto/sha256-js'; +import { + AwsCredentialsManager, + DefaultAwsCredentialsManager, +} from '@backstage/integration-aws-node'; +import { Config } from '@backstage/config'; /** * @@ -41,13 +43,21 @@ const defaultRegion = 'us-east-1'; export class AwsIamKubernetesAuthTranslator implements KubernetesAuthTranslator { + private readonly credsManager: AwsCredentialsManager; + constructor(opts: { config: Config }) { + this.credsManager = DefaultAwsCredentialsManager.fromConfig(opts.config); + } + private async getBearerToken( clusterName: string, assumeRole?: string, externalId?: string, ): Promise { + console.log(`here ${clusterName}, ${assumeRole} ${externalId}`); const region = process.env.AWS_REGION ?? defaultRegion; - let credentials = fromEnv(); + + let credentials = (await this.credsManager.getCredentialProvider()) + .sdkCredentialProvider; if (assumeRole) { credentials = fromTemporaryCredentials({ masterCredentials: credentials, diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts index f8abdde9cf..a7e317e4d8 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts @@ -21,39 +21,44 @@ import { NoopKubernetesAuthTranslator } from './NoopKubernetesAuthTranslator'; import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator'; import { OidcKubernetesAuthTranslator } from './OidcKubernetesAuthTranslator'; import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; const logger = getVoidLogger(); +const config = new ConfigReader({}); describe('getKubernetesAuthTranslatorInstance', () => { const sut = KubernetesAuthTranslatorGenerator; it('can return an auth translator for google auth', () => { const authTranslator: KubernetesAuthTranslator = - sut.getKubernetesAuthTranslatorInstance('google', { logger }); + sut.getKubernetesAuthTranslatorInstance('google', { logger, config }); expect(authTranslator instanceof GoogleKubernetesAuthTranslator).toBe(true); }); it('can return an auth translator for aws auth', () => { const authTranslator: KubernetesAuthTranslator = - sut.getKubernetesAuthTranslatorInstance('aws', { logger }); + sut.getKubernetesAuthTranslatorInstance('aws', { logger, config }); expect(authTranslator instanceof AwsIamKubernetesAuthTranslator).toBe(true); }); it('can return an auth translator for serviceAccount auth', () => { const authTranslator: KubernetesAuthTranslator = - sut.getKubernetesAuthTranslatorInstance('serviceAccount', { logger }); + sut.getKubernetesAuthTranslatorInstance('serviceAccount', { + logger, + config, + }); expect(authTranslator instanceof NoopKubernetesAuthTranslator).toBe(true); }); it('can return an auth translator for oidc auth', () => { const authTranslator: KubernetesAuthTranslator = - sut.getKubernetesAuthTranslatorInstance('oidc', { logger }); + sut.getKubernetesAuthTranslatorInstance('oidc', { logger, config }); expect(authTranslator instanceof OidcKubernetesAuthTranslator).toBe(true); }); it('throws an error when asked for an auth translator for an unsupported auth type', () => { expect(() => - sut.getKubernetesAuthTranslatorInstance('linode', { logger }), + sut.getKubernetesAuthTranslatorInstance('linode', { logger, config }), ).toThrow( 'authProvider "linode" has no KubernetesAuthTranslator associated with it', ); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts index f54e46efab..834de28c84 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts @@ -22,6 +22,7 @@ import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator import { GoogleServiceAccountAuthTranslator } from './GoogleServiceAccountAuthProvider'; import { AzureIdentityKubernetesAuthTranslator } from './AzureIdentityKubernetesAuthTranslator'; import { OidcKubernetesAuthTranslator } from './OidcKubernetesAuthTranslator'; +import { Config } from '@backstage/config'; /** * @@ -32,6 +33,7 @@ export class KubernetesAuthTranslatorGenerator { authProvider: string, options: { logger: Logger; + config: Config; }, ): KubernetesAuthTranslator { switch (authProvider) { @@ -39,7 +41,7 @@ export class KubernetesAuthTranslatorGenerator { return new GoogleKubernetesAuthTranslator(); } case 'aws': { - return new AwsIamKubernetesAuthTranslator(); + return new AwsIamKubernetesAuthTranslator(options); } case 'azure': { return new AzureIdentityKubernetesAuthTranslator(options.logger); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 367e5edef3..f1d9f0bae1 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -121,6 +121,7 @@ export class KubernetesBuilder { const objectsProvider = this.getObjectsProvider({ logger, fetcher, + config, serviceLocator, customResources, objectTypesToFetch: this.getObjectTypesToFetch(), diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index f71c6506bd..75522c5887 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -48,6 +48,7 @@ import { CurrentResourceUsage, PodStatus, } from '@kubernetes/client-node'; +import { Config } from '@backstage/config'; const isRejected = ( input: PromiseSettledResult, @@ -196,14 +197,17 @@ export class KubernetesFanOutHandler { private readonly customResources: CustomResource[]; private readonly objectTypesToFetch: Set; private readonly authTranslators: Record; + private readonly config: Config; constructor({ + config, logger, fetcher, serviceLocator, customResources, objectTypesToFetch = DEFAULT_OBJECTS, }: KubernetesFanOutHandlerOptions) { + this.config = config; this.logger = logger; this.fetcher = fetcher; this.serviceLocator = serviceLocator; @@ -404,6 +408,7 @@ export class KubernetesFanOutHandler { provider, { logger: this.logger, + config: this.config, }, ); return this.authTranslators[provider]; diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 25c7abfd1f..31327b21ee 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -25,6 +25,7 @@ import type { KubernetesRequestBody, ObjectsByEntityResponse, } from '@backstage/plugin-kubernetes-common'; +import { Config } from '@backstage/config'; /** * @@ -240,6 +241,7 @@ export interface AWSClusterDetails extends ClusterDetails { */ export interface KubernetesObjectsProviderOptions { logger: Logger; + config: Config; fetcher: KubernetesFetcher; serviceLocator: KubernetesServiceLocator; customResources: CustomResource[]; From a5c3e3a7fceccf2321ca0f4f3d9539a2b450e02d Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 27 Mar 2023 09:18:46 +0100 Subject: [PATCH 06/13] signature version Signed-off-by: Brian Fletcher --- plugins/kubernetes-backend/package.json | 3 ++- yarn.lock | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 141bf56ad5..66f116f108 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -36,13 +36,14 @@ "dependencies": { "@aws-crypto/sha256-js": "^3.0.0", "@aws-sdk/credential-providers": "^3.208.0", - "@aws-sdk/signature-v4": "^3.208.0", + "@aws-sdk/signature-v4": "^3.296.0", "@azure/identity": "^2.0.4", "@backstage/backend-common": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/integration-aws-node": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 0c2363ac77..79cc1d2b40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1404,7 +1404,7 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/signature-v4@npm:3.296.0, @aws-sdk/signature-v4@npm:^3.208.0": +"@aws-sdk/signature-v4@npm:3.296.0": version: 3.296.0 resolution: "@aws-sdk/signature-v4@npm:3.296.0" dependencies: @@ -1419,6 +1419,21 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/signature-v4@npm:^3.296.0": + version: 3.299.0 + resolution: "@aws-sdk/signature-v4@npm:3.299.0" + dependencies: + "@aws-sdk/is-array-buffer": 3.295.0 + "@aws-sdk/types": 3.296.0 + "@aws-sdk/util-hex-encoding": 3.295.0 + "@aws-sdk/util-middleware": 3.296.0 + "@aws-sdk/util-uri-escape": 3.295.0 + "@aws-sdk/util-utf8": 3.295.0 + tslib: ^2.5.0 + checksum: 642e49ef1cb87649a0fdceb82c55f5153f9d662d5429d49fb18601c3ff8c12a1b050d8856fb18c041511be9d4d38fa741628c23f4be508cb816be696446139cd + languageName: node + linkType: hard + "@aws-sdk/smithy-client@npm:3.296.0": version: 3.296.0 resolution: "@aws-sdk/smithy-client@npm:3.296.0" @@ -6883,7 +6898,7 @@ __metadata: dependencies: "@aws-crypto/sha256-js": ^3.0.0 "@aws-sdk/credential-providers": ^3.208.0 - "@aws-sdk/signature-v4": ^3.208.0 + "@aws-sdk/signature-v4": ^3.296.0 "@azure/identity": ^2.0.4 "@backstage/backend-common": "workspace:^" "@backstage/backend-test-utils": "workspace:^" @@ -6892,6 +6907,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/integration-aws-node": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" From 904e858b49bc8646767e81da96e6473e4bdd996f Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 27 Mar 2023 09:37:37 +0100 Subject: [PATCH 07/13] fix deps and yarn lock Signed-off-by: Brian Fletcher --- plugins/kubernetes-backend/package.json | 2 +- yarn.lock | 19 ++----------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 66f116f108..796bf9fdeb 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -36,7 +36,7 @@ "dependencies": { "@aws-crypto/sha256-js": "^3.0.0", "@aws-sdk/credential-providers": "^3.208.0", - "@aws-sdk/signature-v4": "^3.296.0", + "@aws-sdk/signature-v4": "^3.299.0", "@azure/identity": "^2.0.4", "@backstage/backend-common": "workspace:^", "@backstage/catalog-client": "workspace:^", diff --git a/yarn.lock b/yarn.lock index d7ce664efb..96f12bf777 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1384,22 +1384,7 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/signature-v4@npm:3.299.0": - version: 3.299.0 - resolution: "@aws-sdk/signature-v4@npm:3.299.0" - dependencies: - "@aws-sdk/is-array-buffer": 3.295.0 - "@aws-sdk/types": 3.296.0 - "@aws-sdk/util-hex-encoding": 3.295.0 - "@aws-sdk/util-middleware": 3.296.0 - "@aws-sdk/util-uri-escape": 3.295.0 - "@aws-sdk/util-utf8": 3.295.0 - tslib: ^2.5.0 - checksum: 642e49ef1cb87649a0fdceb82c55f5153f9d662d5429d49fb18601c3ff8c12a1b050d8856fb18c041511be9d4d38fa741628c23f4be508cb816be696446139cd - languageName: node - linkType: hard - -"@aws-sdk/signature-v4@npm:^3.296.0": +"@aws-sdk/signature-v4@npm:3.299.0, @aws-sdk/signature-v4@npm:^3.299.0": version: 3.299.0 resolution: "@aws-sdk/signature-v4@npm:3.299.0" dependencies: @@ -6878,7 +6863,7 @@ __metadata: dependencies: "@aws-crypto/sha256-js": ^3.0.0 "@aws-sdk/credential-providers": ^3.208.0 - "@aws-sdk/signature-v4": ^3.296.0 + "@aws-sdk/signature-v4": ^3.299.0 "@azure/identity": ^2.0.4 "@backstage/backend-common": "workspace:^" "@backstage/backend-test-utils": "workspace:^" From 6ad2be9001b106898ec7cede2f04a7588da457a6 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 27 Mar 2023 09:46:44 +0100 Subject: [PATCH 08/13] fix more tests Signed-off-by: Brian Fletcher --- .../src/service/KubernetesFanOutHandler.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index 53aaa1ad3a..9cdd533c71 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -28,6 +28,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; +import { ConfigReader } from '@backstage/config'; const fetchObjectsForService = jest.fn(); const fetchPodMetricsByNamespaces = jest.fn(); @@ -158,6 +159,7 @@ function mockFetchAndGetKubernetesFanOutHandler( function getKubernetesFanOutHandler(customResources: CustomResource[]) { return new KubernetesFanOutHandler({ logger: getVoidLogger(), + config: new ConfigReader({}), fetcher: { fetchObjectsForService, fetchPodMetricsByNamespaces, @@ -863,6 +865,7 @@ describe('getKubernetesObjectsByEntity', () => { const sut = new KubernetesFanOutHandler({ logger, fetcher: new KubernetesClientBasedFetcher({ logger }), + config: new ConfigReader({}), serviceLocator: fleet, customResources: [], objectTypesToFetch: [ From bac70993b79d14082645d9694320eb434e26fbb4 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 27 Mar 2023 09:58:05 +0100 Subject: [PATCH 09/13] api reports Signed-off-by: Brian Fletcher --- plugins/kubernetes-backend/api-report.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 2b6387701b..0eac98f0a5 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -33,6 +33,7 @@ export interface AWSClusterDetails extends ClusterDetails { export class AwsIamKubernetesAuthTranslator implements KubernetesAuthTranslator { + constructor(opts: { config: Config }); // (undocumented) decorateClusterDetailsWithAuth( clusterDetails: AWSClusterDetails, @@ -148,6 +149,7 @@ export class KubernetesAuthTranslatorGenerator { authProvider: string, options: { logger: Logger; + config: Config; }, ): KubernetesAuthTranslator; } @@ -299,6 +301,8 @@ export interface KubernetesObjectsProvider { // @public (undocumented) export interface KubernetesObjectsProviderOptions { + // (undocumented) + config: Config; // (undocumented) customResources: CustomResource[]; // (undocumented) From ff23abfff2d502a90ef934b59fec42f2b23562ac Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 28 Mar 2023 09:04:37 +0100 Subject: [PATCH 10/13] remove console logs Signed-off-by: Brian Fletcher --- .../AwsIamKubernetesAuthTranslator.test.ts | 1 - .../kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts index e72291664b..b080aa332b 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts @@ -47,7 +47,6 @@ jest.mock('@aws-sdk/signature-v4', () => ({ const fromTemporaryCredentials = jest.fn(); jest.mock('@aws-sdk/credential-providers', () => ({ fromTemporaryCredentials: (opts: any) => { - console.log(`got: ${JSON.stringify(opts)}`); return fromTemporaryCredentials(opts); }, })); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts index f5985b6a93..cc46fd567a 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts @@ -53,7 +53,6 @@ export class AwsIamKubernetesAuthTranslator assumeRole?: string, externalId?: string, ): Promise { - console.log(`here ${clusterName}, ${assumeRole} ${externalId}`); const region = process.env.AWS_REGION ?? defaultRegion; let credentials = (await this.credsManager.getCredentialProvider()) From fec754357539806a58ef4bb1429ef274635cd7a9 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 29 Mar 2023 15:35:12 +0100 Subject: [PATCH 11/13] patch -> minor Signed-off-by: Brian Fletcher --- .changeset/warm-lamps-tease.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/warm-lamps-tease.md b/.changeset/warm-lamps-tease.md index f1dfed65e0..954197dd13 100644 --- a/.changeset/warm-lamps-tease.md +++ b/.changeset/warm-lamps-tease.md @@ -1,5 +1,7 @@ --- -'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes-backend': minor --- Update `aws-sdk` client from v2 to v3. + +**BREAKING**: The `AwsIamKubernetesAuthTranslator` class no longer exposes the following methods `awsGetCredentials`, `getBearerToken`, `getCredentials` and `validCredentials`. There is no replacement for these methods. From 4d38d41a855e308571d882d2bd0a2959a46e3563 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 13 Apr 2023 13:20:56 +0100 Subject: [PATCH 12/13] fix aws signature dep Signed-off-by: Brian Fletcher --- plugins/kubernetes-backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index bf9323eab5..99a7cf1870 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -51,7 +51,7 @@ "dependencies": { "@aws-crypto/sha256-js": "^3.0.0", "@aws-sdk/credential-providers": "^3.208.0", - "@aws-sdk/signature-v4": "^3.299.0", + "@aws-sdk/signature-v4": "^3.310.0", "@azure/identity": "^2.0.4", "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", From d24d78042486297935d054594d4b2a4dd4616be8 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 13 Apr 2023 13:22:36 +0100 Subject: [PATCH 13/13] yarn lock Signed-off-by: Brian Fletcher --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 29f6154c9d..28498a595d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1426,7 +1426,7 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/signature-v4@npm:3.310.0, @aws-sdk/signature-v4@npm:^3.299.0": +"@aws-sdk/signature-v4@npm:3.310.0, @aws-sdk/signature-v4@npm:^3.310.0": version: 3.310.0 resolution: "@aws-sdk/signature-v4@npm:3.310.0" dependencies: @@ -7181,7 +7181,7 @@ __metadata: dependencies: "@aws-crypto/sha256-js": ^3.0.0 "@aws-sdk/credential-providers": ^3.208.0 - "@aws-sdk/signature-v4": ^3.299.0 + "@aws-sdk/signature-v4": ^3.310.0 "@azure/identity": ^2.0.4 "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^"