Skip to content

Commit 03d94d1

Browse files
authored
fix(middleware-user-agent): allow hash in userAgentAppId and propagate to inner clients (#7469)
1 parent 889767c commit 03d94d1

File tree

15 files changed

+112
-18
lines changed

15 files changed

+112
-18
lines changed

clients/client-sts/src/defaultStsRoleAssumers.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import type { STSClient, STSClientConfig, STSClientResolvedConfig } from "./STSC
1616
/**
1717
* @public
1818
*/
19-
export type STSRoleAssumerOptions = Pick<STSClientConfig, "logger" | "region" | "requestHandler" | "profile"> & {
19+
export type STSRoleAssumerOptions = Pick<
20+
STSClientConfig,
21+
"logger" | "region" | "requestHandler" | "profile" | "userAgentAppId"
22+
> & {
2023
credentialProviderLogger?: Logger;
2124
parentClientConfig?: CredentialProviderOptions["parentClientConfig"];
2225
};
@@ -98,6 +101,7 @@ export const getDefaultRoleAssumer = (
98101
region,
99102
requestHandler = stsOptions?.parentClientConfig?.requestHandler,
100103
credentialProviderLogger,
104+
userAgentAppId = stsOptions?.parentClientConfig?.userAgentAppId,
101105
} = stsOptions;
102106
const resolvedRegion = await resolveRegion(
103107
region,
@@ -112,6 +116,7 @@ export const getDefaultRoleAssumer = (
112116

113117
stsClient = new STSClient({
114118
...stsOptions,
119+
userAgentAppId,
115120
profile,
116121
// A hack to make sts client uses the credential in current closure.
117122
credentialDefaultProvider: () => async () => closureSourceCreds,
@@ -165,6 +170,7 @@ export const getDefaultRoleAssumerWithWebIdentity = (
165170
region,
166171
requestHandler = stsOptions?.parentClientConfig?.requestHandler,
167172
credentialProviderLogger,
173+
userAgentAppId = stsOptions?.parentClientConfig?.userAgentAppId,
168174
} = stsOptions;
169175
const resolvedRegion = await resolveRegion(
170176
region,
@@ -179,6 +185,7 @@ export const getDefaultRoleAssumerWithWebIdentity = (
179185

180186
stsClient = new STSClient({
181187
...stsOptions,
188+
userAgentAppId,
182189
profile,
183190
region: resolvedRegion,
184191
requestHandler: isCompatibleRequestHandler ? (requestHandler as any) : undefined,

codegen/smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/sts-client-defaultStsRoleAssumers.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import type { STSClient, STSClientConfig, STSClientResolvedConfig } from "./STSC
1313
/**
1414
* @public
1515
*/
16-
export type STSRoleAssumerOptions = Pick<STSClientConfig, "logger" | "region" | "requestHandler" | "profile"> & {
16+
export type STSRoleAssumerOptions = Pick<
17+
STSClientConfig,
18+
"logger" | "region" | "requestHandler" | "profile" | "userAgentAppId"
19+
> & {
1720
credentialProviderLogger?: Logger;
1821
parentClientConfig?: CredentialProviderOptions["parentClientConfig"];
1922
};
@@ -95,6 +98,7 @@ export const getDefaultRoleAssumer = (
9598
region,
9699
requestHandler = stsOptions?.parentClientConfig?.requestHandler,
97100
credentialProviderLogger,
101+
userAgentAppId = stsOptions?.parentClientConfig?.userAgentAppId,
98102
} = stsOptions;
99103
const resolvedRegion = await resolveRegion(
100104
region,
@@ -109,6 +113,7 @@ export const getDefaultRoleAssumer = (
109113

110114
stsClient = new STSClient({
111115
...stsOptions,
116+
userAgentAppId,
112117
profile,
113118
// A hack to make sts client uses the credential in current closure.
114119
credentialDefaultProvider: () => async () => closureSourceCreds,
@@ -162,6 +167,7 @@ export const getDefaultRoleAssumerWithWebIdentity = (
162167
region,
163168
requestHandler = stsOptions?.parentClientConfig?.requestHandler,
164169
credentialProviderLogger,
170+
userAgentAppId = stsOptions?.parentClientConfig?.userAgentAppId,
165171
} = stsOptions;
166172
const resolvedRegion = await resolveRegion(
167173
region,
@@ -176,6 +182,7 @@ export const getDefaultRoleAssumerWithWebIdentity = (
176182

177183
stsClient = new STSClient({
178184
...stsOptions,
185+
userAgentAppId,
179186
profile,
180187
region: resolvedRegion,
181188
requestHandler: isCompatibleRequestHandler ? (requestHandler as any) : undefined,

packages/credential-provider-cognito-identity/src/fromCognitoIdentity.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function fromCognitoIdentity(parameters: FromCognitoIdentityParameters):
3333
parameters.logger?.debug("@aws-sdk/credential-provider-cognito-identity - fromCognitoIdentity");
3434
const { GetCredentialsForIdentityCommand, CognitoIdentityClient } = await import("./loadCognitoIdentity");
3535

36-
const fromConfigs = (property: "region" | "profile"): any =>
36+
const fromConfigs = (property: "region" | "profile" | "userAgentAppId"): any =>
3737
parameters.clientConfig?.[property] ??
3838
parameters.parentClientConfig?.[property] ??
3939
awsIdentityProperties?.callerClientConfig?.[property];
@@ -51,6 +51,7 @@ export function fromCognitoIdentity(parameters: FromCognitoIdentityParameters):
5151
Object.assign({}, parameters.clientConfig ?? {}, {
5252
region: fromConfigs("region"),
5353
profile: fromConfigs("profile"),
54+
userAgentAppId: fromConfigs("userAgentAppId"),
5455
})
5556
)
5657
).send(

packages/credential-provider-cognito-identity/src/fromCognitoIdentityPool.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function fromCognitoIdentityPool({
3838
let provider: CognitoIdentityCredentialProvider = async (awsIdentityProperties?: AwsIdentityProperties) => {
3939
const { GetIdCommand, CognitoIdentityClient } = await import("./loadCognitoIdentity");
4040

41-
const fromConfigs = (property: "region" | "profile"): any =>
41+
const fromConfigs = (property: "region" | "profile" | "userAgentAppId"): any =>
4242
clientConfig?.[property] ??
4343
parentClientConfig?.[property] ??
4444
awsIdentityProperties?.callerClientConfig?.[property];
@@ -49,6 +49,7 @@ export function fromCognitoIdentityPool({
4949
Object.assign({}, clientConfig ?? {}, {
5050
region: fromConfigs("region"),
5151
profile: fromConfigs("profile"),
52+
userAgentAppId: fromConfigs("userAgentAppId"),
5253
})
5354
);
5455

packages/credential-provider-sso/src/resolveSSOCredentials.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export const resolveSSOCredentials = async ({
7676
Object.assign({}, clientConfig ?? {}, {
7777
logger: clientConfig?.logger ?? parentClientConfig?.logger,
7878
region: clientConfig?.region ?? ssoRegion,
79+
userAgentAppId: clientConfig?.userAgentAppId ?? parentClientConfig?.userAgentAppId,
7980
})
8081
);
8182
let ssoResp: GetRoleCredentialsCommandOutput;

packages/credential-providers/src/fromTemporaryCredentials.base.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export const fromTemporaryCredentials = (
117117
);
118118

119119
stsClient = new STSClient({
120+
userAgentAppId: callerClientConfig?.userAgentAppId,
120121
...options.clientConfig,
121122
credentials: coalesce(credentialSources),
122123
logger,

packages/middleware-user-agent/src/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ export const SPACE = " ";
66

77
export const UA_NAME_SEPARATOR = "/";
88

9-
export const UA_NAME_ESCAPE_REGEX = /[^\!\$\%\&\'\*\+\-\.\^\_\`\|\~\d\w]/g;
9+
export const UA_NAME_ESCAPE_REGEX = /[^!$%&'*+\-.^_`|~\w]/g;
1010

11-
export const UA_VALUE_ESCAPE_REGEX = /[^\!\$\%\&\'\*\+\-\.\^\_\`\|\~\d\w\#]/g;
11+
export const UA_VALUE_ESCAPE_REGEX = /[^!$%&'*+\-.^_`|~\w#]/g;
1212

1313
export const UA_ESCAPE_CHAR = "-";

packages/middleware-user-agent/src/middleware-user-agent.integ.spec.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { requireRequestsFrom } from "@aws-sdk/aws-util-test/src";
22
import { CodeCatalyst } from "@aws-sdk/client-codecatalyst";
33
import { DynamoDB } from "@aws-sdk/client-dynamodb";
4+
import { S3 } from "@aws-sdk/client-s3";
5+
import { fromTemporaryCredentials } from "@aws-sdk/credential-providers";
46
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
7+
import { STSClient } from "@aws-sdk/nested-clients/sts";
58
import { AwsSdkFeatures } from "@aws-sdk/types";
6-
import { describe, expect, test as it } from "vitest";
9+
import { describe, expect, test as it, vi } from "vitest";
710

811
describe("middleware-user-agent", () => {
912
describe(CodeCatalyst.name, () => {
@@ -17,7 +20,7 @@ describe("middleware-user-agent", () => {
1720
requireRequestsFrom(client).toMatch({
1821
headers: {
1922
"x-amz-user-agent": /aws-sdk-js\/[\d\.]+/,
20-
"user-agent": /aws-sdk-js\/[\d\.]+ (.*?)lang\/js md\/nodejs\#[\d\.]+ (.*?)api\/(.+)\#[\d\.]+ (.*?)m\//,
23+
"user-agent": /aws-sdk-js\/[\d.]+ (.*?)lang\/js md\/nodejs#[\d.]+ (.*?)api\/(.+)#[\d.]+ (.*?)m\//,
2124
},
2225
});
2326
await client.getUserDetails({
@@ -27,6 +30,65 @@ describe("middleware-user-agent", () => {
2730
});
2831
});
2932

33+
describe("user agent customization", () => {
34+
it("should propagate the application id configuration to inner clients", async () => {
35+
const s3 = new S3({
36+
region: "us-west-2",
37+
credentials: fromTemporaryCredentials({
38+
masterCredentials: {
39+
accessKeyId: "my-access-key",
40+
secretAccessKey: "my-secretKey",
41+
},
42+
params: {
43+
RoleArn: "arn:aws:iam::1234567890:role/Rigmarole",
44+
},
45+
}),
46+
userAgentAppId: "widget-factory",
47+
});
48+
49+
requireRequestsFrom(s3).toMatch({
50+
headers: {
51+
"user-agent": /app\/widget-factory$/,
52+
},
53+
});
54+
55+
const actual = STSClient.prototype.send;
56+
vi.spyOn(STSClient.prototype, "send").mockImplementation(async function (this: STSClient, ...args) {
57+
if (this instanceof STSClient) {
58+
expect(await this.config.userAgentAppId()).toEqual("widget-factory");
59+
return {
60+
Credentials: {
61+
AccessKeyId: "A",
62+
SecretAccessKey: "S",
63+
},
64+
};
65+
}
66+
return actual.bind(this)(...args);
67+
});
68+
69+
await s3.listBuckets();
70+
71+
expect.assertions(2);
72+
});
73+
74+
it("should allow characters from the set !#$%&'*+-.^_`|~[0-9][A-Za-z]", async () => {
75+
const s3 = new S3({
76+
region: "us-west-2",
77+
userAgentAppId: "!#$%&'*+-.^_`|~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
78+
});
79+
80+
requireRequestsFrom(s3).toMatch({
81+
headers: {
82+
"user-agent": /app\/!#\$%&'\*\+-\.\^_`\|~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$/,
83+
},
84+
});
85+
86+
await s3.listBuckets();
87+
88+
expect.hasAssertions();
89+
});
90+
});
91+
3092
describe("features", () => {
3193
it("should detect DDB mapper, account id, and account id mode", async () => {
3294
const client = new DynamoDB({

packages/middleware-user-agent/src/user-agent-middleware.spec.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ describe("userAgentMiddleware", () => {
133133
{ ua: ["api/Service", "1.0.0"], expected: "api/service#1.0.0" },
134134
{ ua: ["#name#", "1.0.0#blah"], expected: "-name-/1.0.0#blah" },
135135
{ ua: ["#prefix#/#name#", "1.0.0#blah"], expected: "-prefix-/-name-#1.0.0#blah" },
136+
{
137+
ua: ["app", "!#$%&'*+-.^_`|~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"],
138+
expected: "app/!#$%&'*+-.^_`|~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
139+
},
136140
];
137141
[
138142
{ runtime: "node", sdkUserAgentKey: USER_AGENT },
@@ -148,9 +152,7 @@ describe("userAgentMiddleware", () => {
148152
});
149153
const handler = middleware(mockNextHandler, {});
150154
await handler({ input: {}, request: new HttpRequest({ headers: {} }) });
151-
expect(mockNextHandler.mock.calls[0][0].request.headers[sdkUserAgentKey]).toEqual(
152-
expect.stringContaining(expected)
153-
);
155+
expect(mockNextHandler.mock.calls[0][0].request.headers[sdkUserAgentKey]).toContain(expected);
154156
});
155157

156158
it(`should include internal metadata, user agent ${ua} customization: ${expected}`, async () => {
@@ -164,8 +166,8 @@ describe("userAgentMiddleware", () => {
164166
setPartitionInfo({} as any, "a-test-prefix");
165167
const handler = middleware(mockInternalNextHandler, {});
166168
await handler({ input: {}, request: new HttpRequest({ headers: {} }) });
167-
expect(mockInternalNextHandler.mock.calls[0][0].request.headers[sdkUserAgentKey]).toEqual(
168-
expect.stringContaining("a-test-prefix " + expected)
169+
expect(mockInternalNextHandler.mock.calls[0][0].request.headers[sdkUserAgentKey]).toContain(
170+
"a-test-prefix " + expected
169171
);
170172
});
171173
}

packages/middleware-user-agent/src/user-agent-middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export const userAgentMiddleware =
6464
const customUserAgent = options?.customUserAgent?.map(escapeUserAgent) || [];
6565
const appId = await options.userAgentAppId();
6666
if (appId) {
67-
defaultUserAgent.push(escapeUserAgent([`app/${appId}`]));
67+
defaultUserAgent.push(escapeUserAgent([`app`, `${appId}`]));
6868
}
6969
const prefix = getUserAgentPrefix();
7070

0 commit comments

Comments
 (0)