diff --git a/clients/client-sts/src/defaultStsRoleAssumers.ts b/clients/client-sts/src/defaultStsRoleAssumers.ts index 84970eaf6a24..fbadb5bafff5 100644 --- a/clients/client-sts/src/defaultStsRoleAssumers.ts +++ b/clients/client-sts/src/defaultStsRoleAssumers.ts @@ -16,7 +16,10 @@ import type { STSClient, STSClientConfig, STSClientResolvedConfig } from "./STSC /** * @public */ -export type STSRoleAssumerOptions = Pick & { +export type STSRoleAssumerOptions = Pick< + STSClientConfig, + "logger" | "region" | "requestHandler" | "profile" | "userAgentAppId" +> & { credentialProviderLogger?: Logger; parentClientConfig?: CredentialProviderOptions["parentClientConfig"]; }; @@ -98,6 +101,7 @@ export const getDefaultRoleAssumer = ( region, requestHandler = stsOptions?.parentClientConfig?.requestHandler, credentialProviderLogger, + userAgentAppId = stsOptions?.parentClientConfig?.userAgentAppId, } = stsOptions; const resolvedRegion = await resolveRegion( region, @@ -112,6 +116,7 @@ export const getDefaultRoleAssumer = ( stsClient = new STSClient({ ...stsOptions, + userAgentAppId, profile, // A hack to make sts client uses the credential in current closure. credentialDefaultProvider: () => async () => closureSourceCreds, @@ -165,6 +170,7 @@ export const getDefaultRoleAssumerWithWebIdentity = ( region, requestHandler = stsOptions?.parentClientConfig?.requestHandler, credentialProviderLogger, + userAgentAppId = stsOptions?.parentClientConfig?.userAgentAppId, } = stsOptions; const resolvedRegion = await resolveRegion( region, @@ -179,6 +185,7 @@ export const getDefaultRoleAssumerWithWebIdentity = ( stsClient = new STSClient({ ...stsOptions, + userAgentAppId, profile, region: resolvedRegion, requestHandler: isCompatibleRequestHandler ? (requestHandler as any) : undefined, 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 d8edd0789e34..aa3bc6df600e 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 @@ -13,7 +13,10 @@ import type { STSClient, STSClientConfig, STSClientResolvedConfig } from "./STSC /** * @public */ -export type STSRoleAssumerOptions = Pick & { +export type STSRoleAssumerOptions = Pick< + STSClientConfig, + "logger" | "region" | "requestHandler" | "profile" | "userAgentAppId" +> & { credentialProviderLogger?: Logger; parentClientConfig?: CredentialProviderOptions["parentClientConfig"]; }; @@ -95,6 +98,7 @@ export const getDefaultRoleAssumer = ( region, requestHandler = stsOptions?.parentClientConfig?.requestHandler, credentialProviderLogger, + userAgentAppId = stsOptions?.parentClientConfig?.userAgentAppId, } = stsOptions; const resolvedRegion = await resolveRegion( region, @@ -109,6 +113,7 @@ export const getDefaultRoleAssumer = ( stsClient = new STSClient({ ...stsOptions, + userAgentAppId, profile, // A hack to make sts client uses the credential in current closure. credentialDefaultProvider: () => async () => closureSourceCreds, @@ -162,6 +167,7 @@ export const getDefaultRoleAssumerWithWebIdentity = ( region, requestHandler = stsOptions?.parentClientConfig?.requestHandler, credentialProviderLogger, + userAgentAppId = stsOptions?.parentClientConfig?.userAgentAppId, } = stsOptions; const resolvedRegion = await resolveRegion( region, @@ -176,6 +182,7 @@ export const getDefaultRoleAssumerWithWebIdentity = ( stsClient = new STSClient({ ...stsOptions, + userAgentAppId, profile, region: resolvedRegion, requestHandler: isCompatibleRequestHandler ? (requestHandler as any) : undefined, diff --git a/packages/credential-provider-cognito-identity/src/fromCognitoIdentity.ts b/packages/credential-provider-cognito-identity/src/fromCognitoIdentity.ts index ddc82a9b7eaf..0e82e97e8429 100644 --- a/packages/credential-provider-cognito-identity/src/fromCognitoIdentity.ts +++ b/packages/credential-provider-cognito-identity/src/fromCognitoIdentity.ts @@ -33,7 +33,7 @@ export function fromCognitoIdentity(parameters: FromCognitoIdentityParameters): parameters.logger?.debug("@aws-sdk/credential-provider-cognito-identity - fromCognitoIdentity"); const { GetCredentialsForIdentityCommand, CognitoIdentityClient } = await import("./loadCognitoIdentity"); - const fromConfigs = (property: "region" | "profile"): any => + const fromConfigs = (property: "region" | "profile" | "userAgentAppId"): any => parameters.clientConfig?.[property] ?? parameters.parentClientConfig?.[property] ?? awsIdentityProperties?.callerClientConfig?.[property]; @@ -51,6 +51,7 @@ export function fromCognitoIdentity(parameters: FromCognitoIdentityParameters): Object.assign({}, parameters.clientConfig ?? {}, { region: fromConfigs("region"), profile: fromConfigs("profile"), + userAgentAppId: fromConfigs("userAgentAppId"), }) ) ).send( diff --git a/packages/credential-provider-cognito-identity/src/fromCognitoIdentityPool.ts b/packages/credential-provider-cognito-identity/src/fromCognitoIdentityPool.ts index b1ff0dbba3da..9b954bc9c7d0 100644 --- a/packages/credential-provider-cognito-identity/src/fromCognitoIdentityPool.ts +++ b/packages/credential-provider-cognito-identity/src/fromCognitoIdentityPool.ts @@ -38,7 +38,7 @@ export function fromCognitoIdentityPool({ let provider: CognitoIdentityCredentialProvider = async (awsIdentityProperties?: AwsIdentityProperties) => { const { GetIdCommand, CognitoIdentityClient } = await import("./loadCognitoIdentity"); - const fromConfigs = (property: "region" | "profile"): any => + const fromConfigs = (property: "region" | "profile" | "userAgentAppId"): any => clientConfig?.[property] ?? parentClientConfig?.[property] ?? awsIdentityProperties?.callerClientConfig?.[property]; @@ -49,6 +49,7 @@ export function fromCognitoIdentityPool({ Object.assign({}, clientConfig ?? {}, { region: fromConfigs("region"), profile: fromConfigs("profile"), + userAgentAppId: fromConfigs("userAgentAppId"), }) ); diff --git a/packages/credential-provider-sso/src/resolveSSOCredentials.ts b/packages/credential-provider-sso/src/resolveSSOCredentials.ts index c1e31d4e1226..ccccf4c4cb35 100644 --- a/packages/credential-provider-sso/src/resolveSSOCredentials.ts +++ b/packages/credential-provider-sso/src/resolveSSOCredentials.ts @@ -76,6 +76,7 @@ export const resolveSSOCredentials = async ({ Object.assign({}, clientConfig ?? {}, { logger: clientConfig?.logger ?? parentClientConfig?.logger, region: clientConfig?.region ?? ssoRegion, + userAgentAppId: clientConfig?.userAgentAppId ?? parentClientConfig?.userAgentAppId, }) ); let ssoResp: GetRoleCredentialsCommandOutput; diff --git a/packages/credential-providers/src/fromTemporaryCredentials.base.ts b/packages/credential-providers/src/fromTemporaryCredentials.base.ts index 434617d6688a..19461bb0ab9a 100644 --- a/packages/credential-providers/src/fromTemporaryCredentials.base.ts +++ b/packages/credential-providers/src/fromTemporaryCredentials.base.ts @@ -117,6 +117,7 @@ export const fromTemporaryCredentials = ( ); stsClient = new STSClient({ + userAgentAppId: callerClientConfig?.userAgentAppId, ...options.clientConfig, credentials: coalesce(credentialSources), logger, diff --git a/packages/middleware-user-agent/src/constants.ts b/packages/middleware-user-agent/src/constants.ts index 88de049d5bed..fa17fdb91212 100644 --- a/packages/middleware-user-agent/src/constants.ts +++ b/packages/middleware-user-agent/src/constants.ts @@ -6,8 +6,8 @@ export const SPACE = " "; export const UA_NAME_SEPARATOR = "/"; -export const UA_NAME_ESCAPE_REGEX = /[^\!\$\%\&\'\*\+\-\.\^\_\`\|\~\d\w]/g; +export const UA_NAME_ESCAPE_REGEX = /[^!$%&'*+\-.^_`|~\w]/g; -export const UA_VALUE_ESCAPE_REGEX = /[^\!\$\%\&\'\*\+\-\.\^\_\`\|\~\d\w\#]/g; +export const UA_VALUE_ESCAPE_REGEX = /[^!$%&'*+\-.^_`|~\w#]/g; export const UA_ESCAPE_CHAR = "-"; diff --git a/packages/middleware-user-agent/src/middleware-user-agent.integ.spec.ts b/packages/middleware-user-agent/src/middleware-user-agent.integ.spec.ts index d7a9d5afa65b..e6c52be0e514 100644 --- a/packages/middleware-user-agent/src/middleware-user-agent.integ.spec.ts +++ b/packages/middleware-user-agent/src/middleware-user-agent.integ.spec.ts @@ -1,9 +1,12 @@ import { requireRequestsFrom } from "@aws-sdk/aws-util-test/src"; import { CodeCatalyst } from "@aws-sdk/client-codecatalyst"; import { DynamoDB } from "@aws-sdk/client-dynamodb"; +import { S3 } from "@aws-sdk/client-s3"; +import { fromTemporaryCredentials } from "@aws-sdk/credential-providers"; import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb"; +import { STSClient } from "@aws-sdk/nested-clients/sts"; import { AwsSdkFeatures } from "@aws-sdk/types"; -import { describe, expect, test as it } from "vitest"; +import { describe, expect, test as it, vi } from "vitest"; describe("middleware-user-agent", () => { describe(CodeCatalyst.name, () => { @@ -17,7 +20,7 @@ describe("middleware-user-agent", () => { requireRequestsFrom(client).toMatch({ headers: { "x-amz-user-agent": /aws-sdk-js\/[\d\.]+/, - "user-agent": /aws-sdk-js\/[\d\.]+ (.*?)lang\/js md\/nodejs\#[\d\.]+ (.*?)api\/(.+)\#[\d\.]+ (.*?)m\//, + "user-agent": /aws-sdk-js\/[\d.]+ (.*?)lang\/js md\/nodejs#[\d.]+ (.*?)api\/(.+)#[\d.]+ (.*?)m\//, }, }); await client.getUserDetails({ @@ -27,6 +30,65 @@ describe("middleware-user-agent", () => { }); }); + describe("user agent customization", () => { + it("should propagate the application id configuration to inner clients", async () => { + const s3 = new S3({ + region: "us-west-2", + credentials: fromTemporaryCredentials({ + masterCredentials: { + accessKeyId: "my-access-key", + secretAccessKey: "my-secretKey", + }, + params: { + RoleArn: "arn:aws:iam::1234567890:role/Rigmarole", + }, + }), + userAgentAppId: "widget-factory", + }); + + requireRequestsFrom(s3).toMatch({ + headers: { + "user-agent": /app\/widget-factory$/, + }, + }); + + const actual = STSClient.prototype.send; + vi.spyOn(STSClient.prototype, "send").mockImplementation(async function (this: STSClient, ...args) { + if (this instanceof STSClient) { + expect(await this.config.userAgentAppId()).toEqual("widget-factory"); + return { + Credentials: { + AccessKeyId: "A", + SecretAccessKey: "S", + }, + }; + } + return actual.bind(this)(...args); + }); + + await s3.listBuckets(); + + expect.assertions(2); + }); + + it("should allow characters from the set !#$%&'*+-.^_`|~[0-9][A-Za-z]", async () => { + const s3 = new S3({ + region: "us-west-2", + userAgentAppId: "!#$%&'*+-.^_`|~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", + }); + + requireRequestsFrom(s3).toMatch({ + headers: { + "user-agent": /app\/!#\$%&'\*\+-\.\^_`\|~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$/, + }, + }); + + await s3.listBuckets(); + + expect.hasAssertions(); + }); + }); + describe("features", () => { it("should detect DDB mapper, account id, and account id mode", async () => { const client = new DynamoDB({ diff --git a/packages/middleware-user-agent/src/user-agent-middleware.spec.ts b/packages/middleware-user-agent/src/user-agent-middleware.spec.ts index 355146df8d51..16882d05acc0 100644 --- a/packages/middleware-user-agent/src/user-agent-middleware.spec.ts +++ b/packages/middleware-user-agent/src/user-agent-middleware.spec.ts @@ -133,6 +133,10 @@ describe("userAgentMiddleware", () => { { ua: ["api/Service", "1.0.0"], expected: "api/service#1.0.0" }, { ua: ["#name#", "1.0.0#blah"], expected: "-name-/1.0.0#blah" }, { ua: ["#prefix#/#name#", "1.0.0#blah"], expected: "-prefix-/-name-#1.0.0#blah" }, + { + ua: ["app", "!#$%&'*+-.^_`|~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"], + expected: "app/!#$%&'*+-.^_`|~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", + }, ]; [ { runtime: "node", sdkUserAgentKey: USER_AGENT }, @@ -148,9 +152,7 @@ describe("userAgentMiddleware", () => { }); const handler = middleware(mockNextHandler, {}); await handler({ input: {}, request: new HttpRequest({ headers: {} }) }); - expect(mockNextHandler.mock.calls[0][0].request.headers[sdkUserAgentKey]).toEqual( - expect.stringContaining(expected) - ); + expect(mockNextHandler.mock.calls[0][0].request.headers[sdkUserAgentKey]).toContain(expected); }); it(`should include internal metadata, user agent ${ua} customization: ${expected}`, async () => { @@ -164,8 +166,8 @@ describe("userAgentMiddleware", () => { setPartitionInfo({} as any, "a-test-prefix"); const handler = middleware(mockInternalNextHandler, {}); await handler({ input: {}, request: new HttpRequest({ headers: {} }) }); - expect(mockInternalNextHandler.mock.calls[0][0].request.headers[sdkUserAgentKey]).toEqual( - expect.stringContaining("a-test-prefix " + expected) + expect(mockInternalNextHandler.mock.calls[0][0].request.headers[sdkUserAgentKey]).toContain( + "a-test-prefix " + expected ); }); } diff --git a/packages/middleware-user-agent/src/user-agent-middleware.ts b/packages/middleware-user-agent/src/user-agent-middleware.ts index 8af5dc0e8838..4c03993b82e4 100644 --- a/packages/middleware-user-agent/src/user-agent-middleware.ts +++ b/packages/middleware-user-agent/src/user-agent-middleware.ts @@ -64,7 +64,7 @@ export const userAgentMiddleware = const customUserAgent = options?.customUserAgent?.map(escapeUserAgent) || []; const appId = await options.userAgentAppId(); if (appId) { - defaultUserAgent.push(escapeUserAgent([`app/${appId}`])); + defaultUserAgent.push(escapeUserAgent([`app`, `${appId}`])); } const prefix = getUserAgentPrefix(); diff --git a/packages/nested-clients/src/submodules/sts/defaultStsRoleAssumers.ts b/packages/nested-clients/src/submodules/sts/defaultStsRoleAssumers.ts index 84970eaf6a24..fbadb5bafff5 100644 --- a/packages/nested-clients/src/submodules/sts/defaultStsRoleAssumers.ts +++ b/packages/nested-clients/src/submodules/sts/defaultStsRoleAssumers.ts @@ -16,7 +16,10 @@ import type { STSClient, STSClientConfig, STSClientResolvedConfig } from "./STSC /** * @public */ -export type STSRoleAssumerOptions = Pick & { +export type STSRoleAssumerOptions = Pick< + STSClientConfig, + "logger" | "region" | "requestHandler" | "profile" | "userAgentAppId" +> & { credentialProviderLogger?: Logger; parentClientConfig?: CredentialProviderOptions["parentClientConfig"]; }; @@ -98,6 +101,7 @@ export const getDefaultRoleAssumer = ( region, requestHandler = stsOptions?.parentClientConfig?.requestHandler, credentialProviderLogger, + userAgentAppId = stsOptions?.parentClientConfig?.userAgentAppId, } = stsOptions; const resolvedRegion = await resolveRegion( region, @@ -112,6 +116,7 @@ export const getDefaultRoleAssumer = ( stsClient = new STSClient({ ...stsOptions, + userAgentAppId, profile, // A hack to make sts client uses the credential in current closure. credentialDefaultProvider: () => async () => closureSourceCreds, @@ -165,6 +170,7 @@ export const getDefaultRoleAssumerWithWebIdentity = ( region, requestHandler = stsOptions?.parentClientConfig?.requestHandler, credentialProviderLogger, + userAgentAppId = stsOptions?.parentClientConfig?.userAgentAppId, } = stsOptions; const resolvedRegion = await resolveRegion( region, @@ -179,6 +185,7 @@ export const getDefaultRoleAssumerWithWebIdentity = ( stsClient = new STSClient({ ...stsOptions, + userAgentAppId, profile, region: resolvedRegion, requestHandler: isCompatibleRequestHandler ? (requestHandler as any) : undefined, diff --git a/packages/nested-clients/src/submodules/sts/endpoint/ruleset.ts b/packages/nested-clients/src/submodules/sts/endpoint/ruleset.ts index d78ef4811151..dcab43409c7d 100644 --- a/packages/nested-clients/src/submodules/sts/endpoint/ruleset.ts +++ b/packages/nested-clients/src/submodules/sts/endpoint/ruleset.ts @@ -23,8 +23,8 @@ i="https://sts.{Region}.{PartitionResult#dnsSuffix}", j="tree", k="error", l="getAttr", -m={[F]:false,[G]:"String"}, -n={[F]:true,"default":false,[G]:"Boolean"}, +m={[F]:false,[G]:"string"}, +n={[F]:true,"default":false,[G]:"boolean"}, o={[J]:"Endpoint"}, p={[H]:"isSet",[I]:[{[J]:"Region"}]}, q={[J]:"Region"}, diff --git a/packages/token-providers/src/getSsoOidcClient.ts b/packages/token-providers/src/getSsoOidcClient.ts index 58e7f10cf3d2..98bc11a9ed9c 100644 --- a/packages/token-providers/src/getSsoOidcClient.ts +++ b/packages/token-providers/src/getSsoOidcClient.ts @@ -7,10 +7,13 @@ import { FromSsoInit } from "./fromSso"; export const getSsoOidcClient = async (ssoRegion: string, init: FromSsoInit = {}) => { const { SSOOIDCClient } = await import("@aws-sdk/nested-clients/sso-oidc"); + const coalesce = (prop: string) => init.clientConfig?.[prop] ?? init.parentClientConfig?.[prop]; + const ssoOidcClient = new SSOOIDCClient( Object.assign({}, init.clientConfig ?? {}, { region: ssoRegion ?? init.clientConfig?.region, - logger: init.clientConfig?.logger ?? init.parentClientConfig?.logger, + logger: coalesce("logger"), + userAgentAppId: coalesce("userAgentAppId"), }) ); return ssoOidcClient; diff --git a/packages/types/src/credentials.ts b/packages/types/src/credentials.ts index a9c175ff3252..be2ecf6064de 100644 --- a/packages/types/src/credentials.ts +++ b/packages/types/src/credentials.ts @@ -50,6 +50,7 @@ export type CredentialProviderOptions = { region?: string | Provider; profile?: string; logger?: Logger; + userAgentAppId?(): Promise; [key: string]: unknown; }; }; diff --git a/packages/types/src/identity/AwsCredentialIdentity.ts b/packages/types/src/identity/AwsCredentialIdentity.ts index 6fe23d7071cc..405635a348fb 100644 --- a/packages/types/src/identity/AwsCredentialIdentity.ts +++ b/packages/types/src/identity/AwsCredentialIdentity.ts @@ -28,6 +28,7 @@ export interface AwsIdentityProperties { profile?: string; region(): Promise; requestHandler?: RequestHandler; + userAgentAppId?(): Promise; }; }