diff --git a/clients/client-sts/src/defaultStsRoleAssumers.ts b/clients/client-sts/src/defaultStsRoleAssumers.ts index 98b34dafbe86..f68235ce8eac 100644 --- a/clients/client-sts/src/defaultStsRoleAssumers.ts +++ b/clients/client-sts/src/defaultStsRoleAssumers.ts @@ -29,6 +29,31 @@ export type RoleAssumer = ( const ASSUME_ROLE_DEFAULT_REGION = "us-east-1"; +interface AssumedRoleUser { + /** + * The ARN of the temporary security credentials that are returned from the AssumeRole action. + */ + Arn?: string; + + /** + * A unique identifier that contains the role ID and the role session name of the role that is being assumed. + */ + AssumedRoleId?: string; +} + +/** + * @internal + */ +const getAccountIdFromAssumedRoleUser = (assumedRoleUser?: AssumedRoleUser) => { + if (typeof assumedRoleUser?.Arn === "string") { + const arnComponents = assumedRoleUser.Arn.split(":"); + if (arnComponents.length > 4 && arnComponents[4] !== "") { + return arnComponents[4]; + } + } + return undefined; +}; + /** * @internal * @@ -84,17 +109,21 @@ export const getDefaultRoleAssumer = ( logger: logger as any, }); } - const { Credentials } = await stsClient.send(new AssumeRoleCommand(params)); + const { Credentials, AssumedRoleUser } = await stsClient.send(new AssumeRoleCommand(params)); if (!Credentials || !Credentials.AccessKeyId || !Credentials.SecretAccessKey) { throw new Error(`Invalid response from STS.assumeRole call with role ${params.RoleArn}`); } + + const accountId = getAccountIdFromAssumedRoleUser(AssumedRoleUser); + return { accessKeyId: Credentials.AccessKeyId, secretAccessKey: Credentials.SecretAccessKey, sessionToken: Credentials.SessionToken, expiration: Credentials.Expiration, // TODO(credentialScope): access normally when shape is updated. - credentialScope: (Credentials as any).CredentialScope, + ...((Credentials as any).CredentialScope && { credentialScope: (Credentials as any).CredentialScope }), + ...(accountId && { accountId }), }; }; }; @@ -134,17 +163,21 @@ export const getDefaultRoleAssumerWithWebIdentity = ( logger: logger as any, }); } - const { Credentials } = await stsClient.send(new AssumeRoleWithWebIdentityCommand(params)); + const { Credentials, AssumedRoleUser } = await stsClient.send(new AssumeRoleWithWebIdentityCommand(params)); if (!Credentials || !Credentials.AccessKeyId || !Credentials.SecretAccessKey) { throw new Error(`Invalid response from STS.assumeRoleWithWebIdentity call with role ${params.RoleArn}`); } + + const accountId = getAccountIdFromAssumedRoleUser(AssumedRoleUser); + return { accessKeyId: Credentials.AccessKeyId, secretAccessKey: Credentials.SecretAccessKey, sessionToken: Credentials.SessionToken, expiration: Credentials.Expiration, // TODO(credentialScope): access normally when shape is updated. - credentialScope: (Credentials as any).CredentialScope, + ...((Credentials as any).CredentialScope && { credentialScope: (Credentials as any).CredentialScope }), + ...(accountId && { accountId }), }; }; }; diff --git a/clients/client-sts/test/defaultRoleAssumers.spec.ts b/clients/client-sts/test/defaultRoleAssumers.spec.ts index 2be61122a3c1..7cae7a622b12 100644 --- a/clients/client-sts/test/defaultRoleAssumers.spec.ts +++ b/clients/client-sts/test/defaultRoleAssumers.spec.ts @@ -89,6 +89,17 @@ describe("getDefaultRoleAssumer", () => { ); }); + it("should return accountId in the credentials", async () => { + const roleAssumer = getDefaultRoleAssumer(); + const params: AssumeRoleCommandInput = { + RoleArn: "arn:aws:foo", + RoleSessionName: "session", + }; + const sourceCred = { accessKeyId: "key", secretAccessKey: "secrete" }; + const assumedRole = await roleAssumer(sourceCred, params); + expect(assumedRole.accountId).toEqual("123"); + }); + it("should use the STS client config", async () => { const logger = console; const region = "some-region"; @@ -169,6 +180,10 @@ describe("getDefaultRoleAssumer", () => { describe("getDefaultRoleAssumerWithWebIdentity", () => { const assumeRoleResponse = ` + + AROAZOX2IL27GNRBJHWC2:session + arn:aws:sts::123456789012:assumed-role/assume-role-test/session + key secrete @@ -209,6 +224,17 @@ describe("getDefaultRoleAssumerWithWebIdentity", () => { }); }); + it("should return accountId in the credentials", async () => { + const roleAssumerWithWebIdentity = getDefaultRoleAssumerWithWebIdentity(); + const params: AssumeRoleWithWebIdentityCommandInput = { + RoleArn: "arn:aws:foo", + RoleSessionName: "session", + WebIdentityToken: "token", + }; + const assumedRole = await roleAssumerWithWebIdentity(params); + expect(assumedRole.accountId).toEqual("123456789012"); + }); + it("should use the STS client middleware", async () => { const customMiddlewareFunction = jest.fn(); const roleAssumerWithWebIdentity = getDefaultRoleAssumerWithWebIdentity({}, [ diff --git a/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultRoleAssumers.spec.ts b/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultRoleAssumers.spec.ts index 072f80a3c4ce..d08478e213ad 100644 --- a/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultRoleAssumers.spec.ts +++ b/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultRoleAssumers.spec.ts @@ -87,6 +87,17 @@ describe("getDefaultRoleAssumer", () => { ); }); + it("should return accountId in the credentials", async () => { + const roleAssumer = getDefaultRoleAssumer(); + const params: AssumeRoleCommandInput = { + RoleArn: "arn:aws:foo", + RoleSessionName: "session", + }; + const sourceCred = { accessKeyId: "key", secretAccessKey: "secrete" }; + const assumedRole = await roleAssumer(sourceCred, params); + expect(assumedRole.accountId).toEqual("123"); + }); + it("should use the STS client config", async () => { const logger = console; const region = "some-region"; @@ -167,6 +178,10 @@ describe("getDefaultRoleAssumer", () => { describe("getDefaultRoleAssumerWithWebIdentity", () => { const assumeRoleResponse = ` + + AROAZOX2IL27GNRBJHWC2:session + arn:aws:sts::123456789012:assumed-role/assume-role-test/session + key secrete @@ -207,6 +222,17 @@ describe("getDefaultRoleAssumerWithWebIdentity", () => { }); }); + it("should return accountId in the credentials", async () => { + const roleAssumerWithWebIdentity = getDefaultRoleAssumerWithWebIdentity(); + const params: AssumeRoleWithWebIdentityCommandInput = { + RoleArn: "arn:aws:foo", + RoleSessionName: "session", + WebIdentityToken: "token", + }; + const assumedRole = await roleAssumerWithWebIdentity(params); + expect(assumedRole.accountId).toEqual("123456789012"); + }); + it("should use the STS client middleware", async () => { const customMiddlewareFunction = jest.fn(); const roleAssumerWithWebIdentity = getDefaultRoleAssumerWithWebIdentity({}, [ diff --git a/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts b/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts index 0a3d3fb26400..1f1d66f01dc3 100644 --- a/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts +++ b/codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts @@ -26,6 +26,31 @@ export type RoleAssumer = ( const ASSUME_ROLE_DEFAULT_REGION = "us-east-1"; +interface AssumedRoleUser { + /** + * The ARN of the temporary security credentials that are returned from the AssumeRole action. + */ + Arn?: string; + + /** + * A unique identifier that contains the role ID and the role session name of the role that is being assumed. + */ + AssumedRoleId?: string; +} + +/** + * @internal + */ +const getAccountIdFromAssumedRoleUser = (assumedRoleUser?: AssumedRoleUser) => { + if (typeof assumedRoleUser?.Arn === "string") { + const arnComponents = assumedRoleUser.Arn.split(":"); + if (arnComponents.length > 4 && arnComponents[4] !== "") { + return arnComponents[4]; + } + } + return undefined; +}; + /** * @internal * @@ -81,17 +106,21 @@ export const getDefaultRoleAssumer = ( logger: logger as any, }); } - const { Credentials } = await stsClient.send(new AssumeRoleCommand(params)); + const { Credentials, AssumedRoleUser } = await stsClient.send(new AssumeRoleCommand(params)); if (!Credentials || !Credentials.AccessKeyId || !Credentials.SecretAccessKey) { throw new Error(`Invalid response from STS.assumeRole call with role ${params.RoleArn}`); } + + const accountId = getAccountIdFromAssumedRoleUser(AssumedRoleUser); + return { accessKeyId: Credentials.AccessKeyId, secretAccessKey: Credentials.SecretAccessKey, sessionToken: Credentials.SessionToken, expiration: Credentials.Expiration, // TODO(credentialScope): access normally when shape is updated. - credentialScope: (Credentials as any).CredentialScope, + ...((Credentials as any).CredentialScope && { credentialScope: (Credentials as any).CredentialScope }), + ...(accountId && { accountId }), }; }; }; @@ -131,17 +160,21 @@ export const getDefaultRoleAssumerWithWebIdentity = ( logger: logger as any, }); } - const { Credentials } = await stsClient.send(new AssumeRoleWithWebIdentityCommand(params)); + const { Credentials, AssumedRoleUser } = await stsClient.send(new AssumeRoleWithWebIdentityCommand(params)); if (!Credentials || !Credentials.AccessKeyId || !Credentials.SecretAccessKey) { throw new Error(`Invalid response from STS.assumeRoleWithWebIdentity call with role ${params.RoleArn}`); } + + const accountId = getAccountIdFromAssumedRoleUser(AssumedRoleUser); + return { accessKeyId: Credentials.AccessKeyId, secretAccessKey: Credentials.SecretAccessKey, sessionToken: Credentials.SessionToken, expiration: Credentials.Expiration, // TODO(credentialScope): access normally when shape is updated. - credentialScope: (Credentials as any).CredentialScope, + ...((Credentials as any).CredentialScope && { credentialScope: (Credentials as any).CredentialScope }), + ...(accountId && { accountId }), }; }; }; diff --git a/packages/credential-provider-env/src/fromEnv.spec.ts b/packages/credential-provider-env/src/fromEnv.spec.ts index 18c6b9a19992..596b9234bb4e 100644 --- a/packages/credential-provider-env/src/fromEnv.spec.ts +++ b/packages/credential-provider-env/src/fromEnv.spec.ts @@ -1,6 +1,6 @@ import { CredentialsProviderError } from "@smithy/property-provider"; -import { ENV_EXPIRATION, ENV_KEY, ENV_SECRET, ENV_SESSION, fromEnv } from "./fromEnv"; +import { ENV_ACCOUNT_ID, ENV_EXPIRATION, ENV_KEY, ENV_SECRET, ENV_SESSION, fromEnv } from "./fromEnv"; describe(fromEnv.name, () => { const ORIGINAL_ENV = process.env; @@ -8,6 +8,7 @@ describe(fromEnv.name, () => { const mockSecretAccessKey = "mockSecretAccessKey"; const mockSessionToken = "mockSessionToken"; const mockExpiration = new Date().toISOString(); + const mockAccountId = "123456789012"; beforeEach(() => { process.env = { @@ -16,6 +17,7 @@ describe(fromEnv.name, () => { [ENV_SECRET]: mockSecretAccessKey, [ENV_SESSION]: mockSessionToken, [ENV_EXPIRATION]: mockExpiration, + [ENV_ACCOUNT_ID]: mockAccountId, }; }); @@ -30,12 +32,14 @@ describe(fromEnv.name, () => { secretAccessKey: mockSecretAccessKey, sessionToken: mockSessionToken, expiration: new Date(mockExpiration), + accountId: mockAccountId, }); }); - it("can create credentials without a session token or expiration", async () => { + it("can create credentials without a session token, accountId, or expiration", async () => { delete process.env[ENV_SESSION]; delete process.env[ENV_EXPIRATION]; + delete process.env[ENV_ACCOUNT_ID]; const receivedCreds = await fromEnv()(); expect(receivedCreds).toStrictEqual({ accessKeyId: mockAccessKeyId, @@ -43,6 +47,18 @@ describe(fromEnv.name, () => { }); }); + it("should include accountId when it is provided in environment variables", async () => { + process.env[ENV_ACCOUNT_ID] = mockAccountId; + const receivedCreds = await fromEnv()(); + expect(receivedCreds).toHaveProperty("accountId", mockAccountId); + }); + + it("should not include accountId when it is not provided in environment variables", async () => { + delete process.env[ENV_ACCOUNT_ID]; // Ensure accountId is not set + const receivedCreds = await fromEnv()(); + expect(receivedCreds).not.toHaveProperty("accountId"); + }); + it.each([ENV_KEY, ENV_SECRET])("throws if env['%s'] is not found", async (key) => { delete process.env[key]; const expectedError = new CredentialsProviderError("Unable to find environment variable credentials."); diff --git a/packages/credential-provider-env/src/fromEnv.ts b/packages/credential-provider-env/src/fromEnv.ts index 0b9aeeb58eef..71215e3f83f0 100644 --- a/packages/credential-provider-env/src/fromEnv.ts +++ b/packages/credential-provider-env/src/fromEnv.ts @@ -24,6 +24,10 @@ export const ENV_EXPIRATION = "AWS_CREDENTIAL_EXPIRATION"; * @internal */ export const ENV_CREDENTIAL_SCOPE = "AWS_CREDENTIAL_SCOPE"; +/** + * @internal + */ +export const ENV_ACCOUNT_ID = "AWS_ACCOUNT_ID"; /** * @internal @@ -41,6 +45,7 @@ export const fromEnv = const sessionToken: string | undefined = process.env[ENV_SESSION]; const expiry: string | undefined = process.env[ENV_EXPIRATION]; const credentialScope: string | undefined = process.env[ENV_CREDENTIAL_SCOPE]; + const accountId: string | undefined = process.env[ENV_ACCOUNT_ID]; if (accessKeyId && secretAccessKey) { return { @@ -49,6 +54,7 @@ export const fromEnv = ...(sessionToken && { sessionToken }), ...(expiry && { expiration: new Date(expiry) }), ...(credentialScope && { credentialScope }), + ...(accountId && { accountId }), }; } diff --git a/packages/credential-provider-ini/src/resolveStaticCredentials.spec.ts b/packages/credential-provider-ini/src/resolveStaticCredentials.spec.ts index b075357d6dd3..00da15e3144f 100644 --- a/packages/credential-provider-ini/src/resolveStaticCredentials.spec.ts +++ b/packages/credential-provider-ini/src/resolveStaticCredentials.spec.ts @@ -5,6 +5,7 @@ const getMockStaticCredsProfile = () => ({ aws_secret_access_key: "mock_aws_secret_access_key", aws_session_token: "mock_aws_session_token", aws_credential_scope: "mock_aws_credential_scope", + aws_account_id: "mock_aws_account_id", }); describe(isStaticCredsProfile.name, () => { @@ -32,6 +33,12 @@ describe(isStaticCredsProfile.name, () => { }); }); + it.each(["aws_account_id"])("value at '%s' is not of type string | undefined", (key) => { + [true, null, 1, NaN, {}].forEach((value) => { + expect(isStaticCredsProfile({ ...getMockStaticCredsProfile(), [key]: value })).toEqual(false); + }); + }); + it("returns true for StaticCredentialsProfile", () => { expect(isStaticCredsProfile(getMockStaticCredsProfile())).toEqual(true); }); @@ -46,6 +53,7 @@ describe(resolveStaticCredentials.name, () => { secretAccessKey: mockProfile.aws_secret_access_key, sessionToken: mockProfile.aws_session_token, credentialScope: mockProfile.aws_credential_scope, + accountId: mockProfile.aws_account_id, }); }); }); diff --git a/packages/credential-provider-ini/src/resolveStaticCredentials.ts b/packages/credential-provider-ini/src/resolveStaticCredentials.ts index c9ced1a540c3..a778252f1d95 100644 --- a/packages/credential-provider-ini/src/resolveStaticCredentials.ts +++ b/packages/credential-provider-ini/src/resolveStaticCredentials.ts @@ -10,6 +10,7 @@ export interface StaticCredsProfile extends Profile { aws_secret_access_key: string; aws_session_token?: string; aws_credential_scope?: string; + aws_account_id?: string; } /** @@ -20,7 +21,8 @@ export const isStaticCredsProfile = (arg: any): arg is StaticCredsProfile => typeof arg === "object" && typeof arg.aws_access_key_id === "string" && typeof arg.aws_secret_access_key === "string" && - ["undefined", "string"].indexOf(typeof arg.aws_session_token) > -1; + ["undefined", "string"].indexOf(typeof arg.aws_session_token) > -1 && + ["undefined", "string"].indexOf(typeof arg.aws_account_id) > -1; /** * @internal @@ -34,6 +36,7 @@ export const resolveStaticCredentials = ( accessKeyId: profile.aws_access_key_id, secretAccessKey: profile.aws_secret_access_key, sessionToken: profile.aws_session_token, - credentialScope: profile.aws_credential_scope, + ...(profile.aws_credential_scope && { credentialScope: profile.aws_credential_scope }), + ...(profile.aws_account_id && { accountId: profile.aws_account_id }), }); }; diff --git a/packages/credential-provider-process/src/ProcessCredentials.ts b/packages/credential-provider-process/src/ProcessCredentials.ts index 0580e63897bf..c675a9013210 100644 --- a/packages/credential-provider-process/src/ProcessCredentials.ts +++ b/packages/credential-provider-process/src/ProcessCredentials.ts @@ -8,4 +8,5 @@ export type ProcessCredentials = { SessionToken?: string; Expiration?: number; CredentialScope?: string; + AccountId?: string; }; diff --git a/packages/credential-provider-process/src/getValidatedProcessCredentials.spec.ts b/packages/credential-provider-process/src/getValidatedProcessCredentials.spec.ts index b72d91ed14a6..cc82de4d9c2d 100644 --- a/packages/credential-provider-process/src/getValidatedProcessCredentials.spec.ts +++ b/packages/credential-provider-process/src/getValidatedProcessCredentials.spec.ts @@ -1,4 +1,4 @@ -import { AwsCredentialIdentity } from "@smithy/types"; +import { AwsCredentialIdentity, ParsedIniData } from "@smithy/types"; import { getValidatedProcessCredentials } from "./getValidatedProcessCredentials"; import { ProcessCredentials } from "./ProcessCredentials"; @@ -9,6 +9,13 @@ describe(getValidatedProcessCredentials.name, () => { const mockSecretAccessKey = "mockSecretAccessKey"; const mockSessionToken = "mockSessionToken"; const mockExpiration = Date.now() + 24 * 60 * 60 * 1000; + const mockAccountId = "123456789012"; + + const mockProfiles: ParsedIniData = { + [mockProfileName]: { + aws_account_id: mockAccountId, + }, + }; const getMockProcessCreds = (): ProcessCredentials => ({ Version: 1, @@ -16,33 +23,46 @@ describe(getValidatedProcessCredentials.name, () => { SecretAccessKey: mockSecretAccessKey, SessionToken: mockSessionToken, Expiration: mockExpiration, + AccountId: mockAccountId, }); it.each([2])("throws Error when Version is %s", (Version) => { expect(() => { - getValidatedProcessCredentials(mockProfileName, { - ...getMockProcessCreds(), - Version, - }); + getValidatedProcessCredentials( + mockProfileName, + { + ...getMockProcessCreds(), + Version, + }, + mockProfiles + ); }).toThrow(`Profile ${mockProfileName} credential_process did not return Version 1.`); }); it.each(["AccessKeyId", "SecretAccessKey"])("throws Error when '%s' is not defined", (key) => { expect(() => { - getValidatedProcessCredentials(mockProfileName, { - ...getMockProcessCreds(), - [key]: undefined, - }); + getValidatedProcessCredentials( + mockProfileName, + { + ...getMockProcessCreds(), + [key]: undefined, + }, + mockProfiles + ); }).toThrow(`Profile ${mockProfileName} credential_process returned invalid credentials.`); }); it("throws error when credentials are expired", () => { const expirationDayBefore = Date.now() - 24 * 60 * 60 * 1000; expect(() => { - getValidatedProcessCredentials(mockProfileName, { - ...getMockProcessCreds(), - Expiration: expirationDayBefore, - }); + getValidatedProcessCredentials( + mockProfileName, + { + ...getMockProcessCreds(), + Expiration: expirationDayBefore, + }, + mockProfiles + ); }).toThrow(`Profile ${mockProfileName} credential_process returned expired credentials.`); }); @@ -52,18 +72,23 @@ describe(getValidatedProcessCredentials.name, () => { secretAccessKey: data.SecretAccessKey, ...(data.SessionToken && { sessionToken: data.SessionToken }), ...(data.Expiration && { expiration: new Date(data.Expiration) }), + ...(data.AccountId && { accountId: data.AccountId }), }); it("with all values", () => { const mockProcessCreds = getMockProcessCreds(); const mockOutputCreds = getValidatedCredentials(mockProcessCreds); - expect(getValidatedProcessCredentials(mockProfileName, mockProcessCreds)).toStrictEqual(mockOutputCreds); + expect(getValidatedProcessCredentials(mockProfileName, mockProcessCreds, mockProfiles)).toStrictEqual( + mockOutputCreds + ); }); it.each(["SessionToken", "Expiration"])("without '%s'", (key) => { const mockProcessCreds = { ...getMockProcessCreds(), [key]: undefined }; const mockOutputCreds = getValidatedCredentials(mockProcessCreds); - expect(getValidatedProcessCredentials(mockProfileName, mockProcessCreds)).toStrictEqual(mockOutputCreds); + expect(getValidatedProcessCredentials(mockProfileName, mockProcessCreds, mockProfiles)).toStrictEqual( + mockOutputCreds + ); }); }); }); diff --git a/packages/credential-provider-process/src/getValidatedProcessCredentials.ts b/packages/credential-provider-process/src/getValidatedProcessCredentials.ts index c8ba5a59fc47..6b0284972eca 100644 --- a/packages/credential-provider-process/src/getValidatedProcessCredentials.ts +++ b/packages/credential-provider-process/src/getValidatedProcessCredentials.ts @@ -1,4 +1,4 @@ -import { AwsCredentialIdentity } from "@smithy/types"; +import { AwsCredentialIdentity, ParsedIniData } from "@smithy/types"; import { ProcessCredentials } from "./ProcessCredentials"; @@ -7,7 +7,8 @@ import { ProcessCredentials } from "./ProcessCredentials"; */ export const getValidatedProcessCredentials = ( profileName: string, - data: ProcessCredentials + data: ProcessCredentials, + profiles: ParsedIniData ): AwsCredentialIdentity => { if (data.Version !== 1) { throw Error(`Profile ${profileName} credential_process did not return Version 1.`); @@ -25,11 +26,17 @@ export const getValidatedProcessCredentials = ( } } + let accountId = data.AccountId; + if (!accountId && profiles?.[profileName]?.aws_account_id) { + accountId = profiles[profileName].aws_account_id; + } + return { accessKeyId: data.AccessKeyId, secretAccessKey: data.SecretAccessKey, ...(data.SessionToken && { sessionToken: data.SessionToken }), ...(data.Expiration && { expiration: new Date(data.Expiration) }), ...(data.CredentialScope && { credentialScope: data.CredentialScope }), + ...(accountId && { accountId }), }; }; diff --git a/packages/credential-provider-process/src/resolveProcessCredentials.spec.ts b/packages/credential-provider-process/src/resolveProcessCredentials.spec.ts index c0595660d981..73915255581e 100644 --- a/packages/credential-provider-process/src/resolveProcessCredentials.spec.ts +++ b/packages/credential-provider-process/src/resolveProcessCredentials.spec.ts @@ -99,7 +99,11 @@ describe(resolveProcessCredentials.name, () => { expect(error).toEqual(expectedError); } expect(mockExecPromise).toHaveBeenCalledWith(mockCredentialProcess); - expect(getValidatedProcessCredentials).toHaveBeenCalledWith(mockProfileName, JSON.parse(mockExecPromiseOutput)); + expect(getValidatedProcessCredentials).toHaveBeenCalledWith( + mockProfileName, + JSON.parse(mockExecPromiseOutput), + getMockProfiles() + ); }); it("returns data from getValidatedProcessCredentials", async () => { @@ -111,6 +115,10 @@ describe(resolveProcessCredentials.name, () => { const receivedCreds = await resolveProcessCredentials(mockProfileName, getMockProfiles()); expect(receivedCreds).toStrictEqual(expectedCreds); expect(mockExecPromise).toHaveBeenCalledWith(mockCredentialProcess); - expect(getValidatedProcessCredentials).toHaveBeenCalledWith(mockProfileName, JSON.parse(mockExecPromiseOutput)); + expect(getValidatedProcessCredentials).toHaveBeenCalledWith( + mockProfileName, + JSON.parse(mockExecPromiseOutput), + getMockProfiles() + ); }); }); diff --git a/packages/credential-provider-process/src/resolveProcessCredentials.ts b/packages/credential-provider-process/src/resolveProcessCredentials.ts index c0df0c38da10..4313c13a6d0b 100644 --- a/packages/credential-provider-process/src/resolveProcessCredentials.ts +++ b/packages/credential-provider-process/src/resolveProcessCredentials.ts @@ -28,7 +28,7 @@ export const resolveProcessCredentials = async ( } catch { throw Error(`Profile ${profileName} credential_process returned invalid JSON.`); } - return getValidatedProcessCredentials(profileName, data as ProcessCredentials); + return getValidatedProcessCredentials(profileName, data as ProcessCredentials, profiles); } catch (error) { throw new CredentialsProviderError(error.message, { logger }); } diff --git a/packages/credential-provider-sso/src/resolveSSOCredentials.spec.ts b/packages/credential-provider-sso/src/resolveSSOCredentials.spec.ts index 3431dd19863f..b2ad2ff8c28c 100644 --- a/packages/credential-provider-sso/src/resolveSSOCredentials.spec.ts +++ b/packages/credential-provider-sso/src/resolveSSOCredentials.spec.ts @@ -42,6 +42,7 @@ describe(resolveSSOCredentials.name, () => { secretAccessKey: "mockSecretAccessKey", sessionToken: "mockSessionToken", expiration: Date.now(), + accountId: "mock_sso_account_id", }; beforeEach(() => { @@ -164,4 +165,21 @@ describe(resolveSSOCredentials.name, () => { expect(mockCustomSsoSend).toHaveBeenCalledTimes(1); }); }); + + describe("returns valid credentials including accountId", () => { + it("returns valid credentials with accountId from sso.getRoleCredentials", async () => { + const result = await resolveSSOCredentials(mockOptions); + expect(result).toHaveProperty("accountId", mockOptions.ssoAccountId); + expect(mockSsoSend).toHaveBeenCalledTimes(1); + }); + + it("creates SSO client with provided region, if client is not passed, and includes accountId", async () => { + const mockCustomSsoSend = jest.fn().mockResolvedValue({ roleCredentials: mockCreds }); + (SSOClient as jest.Mock).mockReturnValue({ send: mockCustomSsoSend }); + + const result = await resolveSSOCredentials({ ...mockOptions, ssoClient: undefined }); + expect(result).toHaveProperty("accountId", mockOptions.ssoAccountId); + expect(mockCustomSsoSend).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/credential-provider-sso/src/resolveSSOCredentials.ts b/packages/credential-provider-sso/src/resolveSSOCredentials.ts index b1c7a4692a64..48b598bd092e 100644 --- a/packages/credential-provider-sso/src/resolveSSOCredentials.ts +++ b/packages/credential-provider-sso/src/resolveSSOCredentials.ts @@ -83,16 +83,18 @@ export const resolveSSOCredentials = async ({ }); } - const { roleCredentials: { accessKeyId, secretAccessKey, sessionToken, expiration, credentialScope } = {} } = - ssoResp as unknown as { - roleCredentials: { - accessKeyId?: string; - secretAccessKey?: string; - sessionToken?: string; - expiration?: Date | string; - credentialScope?: string; - }; + const { + roleCredentials: { accessKeyId, secretAccessKey, sessionToken, expiration, credentialScope, accountId } = {}, + } = ssoResp as unknown as { + roleCredentials: { + accessKeyId?: string; + secretAccessKey?: string; + sessionToken?: string; + expiration?: Date | string; + credentialScope?: string; + accountId?: string; }; + }; if (!accessKeyId || !secretAccessKey || !sessionToken || !expiration) { throw new CredentialsProviderError("SSO returns an invalid temporary credential.", { @@ -101,5 +103,12 @@ export const resolveSSOCredentials = async ({ }); } - return { accessKeyId, secretAccessKey, sessionToken, expiration: new Date(expiration), credentialScope }; + return { + accessKeyId, + secretAccessKey, + sessionToken, + expiration: new Date(expiration), + ...(credentialScope && { credentialScope }), + ...(accountId && { accountId }), + }; };