Skip to content

Commit 2fab06b

Browse files
authored
fix(NODE-5550): set AWS region from environment variable for STSClient (#3851)
1 parent 435f88b commit 2fab06b

File tree

3 files changed

+217
-14
lines changed

3 files changed

+217
-14
lines changed

src/cmap/auth/mongodb_aws.ts

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as crypto from 'crypto';
2+
import * as process from 'process';
23
import { promisify } from 'util';
34

45
import type { Binary, BSONSerializeOptions } from '../../bson';
@@ -15,6 +16,28 @@ import { type AuthContext, AuthProvider } from './auth_provider';
1516
import { MongoCredentials } from './mongo_credentials';
1617
import { AuthMechanism } from './providers';
1718

19+
/**
20+
* The following regions use the global AWS STS endpoint, sts.amazonaws.com, by default
21+
* https://docs.aws.amazon.com/sdkref/latest/guide/feature-sts-regionalized-endpoints.html
22+
*/
23+
const LEGACY_REGIONS = new Set([
24+
'ap-northeast-1',
25+
'ap-south-1',
26+
'ap-southeast-1',
27+
'ap-southeast-2',
28+
'aws-global',
29+
'ca-central-1',
30+
'eu-central-1',
31+
'eu-north-1',
32+
'eu-west-1',
33+
'eu-west-2',
34+
'eu-west-3',
35+
'sa-east-1',
36+
'us-east-1',
37+
'us-east-2',
38+
'us-west-1',
39+
'us-west-2'
40+
]);
1841
const ASCII_N = 110;
1942
const AWS_RELATIVE_URI = 'http://169.254.170.2';
2043
const AWS_EC2_URI = 'http://169.254.169.254';
@@ -34,6 +57,7 @@ interface AWSSaslContinuePayload {
3457
}
3558

3659
export class MongoDBAWS extends AuthProvider {
60+
static credentialProvider: ReturnType<typeof getAwsCredentialProvider> | null = null;
3761
randomBytesAsync: (size: number) => Promise<Buffer>;
3862

3963
constructor() {
@@ -157,14 +181,6 @@ interface AWSTempCredentials {
157181
Expiration?: Date;
158182
}
159183

160-
/* @internal */
161-
export interface AWSCredentials {
162-
accessKeyId?: string;
163-
secretAccessKey?: string;
164-
sessionToken?: string;
165-
expiration?: Date;
166-
}
167-
168184
async function makeTempCredentials(credentials: MongoCredentials): Promise<MongoCredentials> {
169185
function makeMongoCredentialsFromAWSTemp(creds: AWSTempCredentials) {
170186
if (!creds.AccessKeyId || !creds.SecretAccessKey || !creds.Token) {
@@ -182,11 +198,11 @@ async function makeTempCredentials(credentials: MongoCredentials): Promise<Mongo
182198
});
183199
}
184200

185-
const credentialProvider = getAwsCredentialProvider();
201+
MongoDBAWS.credentialProvider ??= getAwsCredentialProvider();
186202

187203
// Check if the AWS credential provider from the SDK is present. If not,
188204
// use the old method.
189-
if ('kModuleError' in credentialProvider) {
205+
if ('kModuleError' in MongoDBAWS.credentialProvider) {
190206
// If the environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
191207
// is set then drivers MUST assume that it was set by an AWS ECS agent
192208
if (process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) {
@@ -217,6 +233,32 @@ async function makeTempCredentials(credentials: MongoCredentials): Promise<Mongo
217233

218234
return makeMongoCredentialsFromAWSTemp(creds);
219235
} else {
236+
let { AWS_STS_REGIONAL_ENDPOINTS = '', AWS_REGION = '' } = process.env;
237+
AWS_STS_REGIONAL_ENDPOINTS = AWS_STS_REGIONAL_ENDPOINTS.toLowerCase();
238+
AWS_REGION = AWS_REGION.toLowerCase();
239+
240+
/** The option setting should work only for users who have explicit settings in their environment, the driver should not encode "defaults" */
241+
const awsRegionSettingsExist =
242+
AWS_REGION.length !== 0 && AWS_STS_REGIONAL_ENDPOINTS.length !== 0;
243+
244+
/**
245+
* If AWS_STS_REGIONAL_ENDPOINTS is set to regional, users are opting into the new behavior of respecting the region settings
246+
*
247+
* If AWS_STS_REGIONAL_ENDPOINTS is set to legacy, then "old" regions need to keep using the global setting.
248+
* Technically the SDK gets this wrong, it reaches out to 'sts.us-east-1.amazonaws.com' when it should be 'sts.amazonaws.com'.
249+
* That is not our bug to fix here. We leave that up to the SDK.
250+
*/
251+
const useRegionalSts =
252+
AWS_STS_REGIONAL_ENDPOINTS === 'regional' ||
253+
(AWS_STS_REGIONAL_ENDPOINTS === 'legacy' && !LEGACY_REGIONS.has(AWS_REGION));
254+
255+
const provider =
256+
awsRegionSettingsExist && useRegionalSts
257+
? MongoDBAWS.credentialProvider.fromNodeProviderChain({
258+
clientConfig: { region: AWS_REGION }
259+
})
260+
: MongoDBAWS.credentialProvider.fromNodeProviderChain();
261+
220262
/*
221263
* Creates a credential provider that will attempt to find credentials from the
222264
* following sources (listed in order of precedence):
@@ -227,8 +269,6 @@ async function makeTempCredentials(credentials: MongoCredentials): Promise<Mongo
227269
* - Shared credentials and config ini files
228270
* - The EC2/ECS Instance Metadata Service
229271
*/
230-
const { fromNodeProviderChain } = credentialProvider;
231-
const provider = fromNodeProviderChain();
232272
try {
233273
const creds = await provider();
234274
return makeMongoCredentialsFromAWSTemp({

src/deps.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-var-requires */
22
import type { Document } from './bson';
3-
import type { AWSCredentials } from './cmap/auth/mongodb_aws';
43
import type { ProxyOptions } from './cmap/connection';
54
import { MongoMissingDependencyError } from './error';
65
import type { MongoClient } from './mongo_client';
@@ -76,7 +75,23 @@ export function getZstdLibrary(): typeof ZStandard | { kModuleError: MongoMissin
7675
}
7776
}
7877

78+
/**
79+
* @internal
80+
* Copy of the AwsCredentialIdentityProvider interface from [`smithy/types`](https://socket.dev/npm/package/\@smithy/types/files/1.1.1/dist-types/identity/awsCredentialIdentity.d.ts),
81+
* the return type of the aws-sdk's `fromNodeProviderChain().provider()`.
82+
*/
83+
export interface AWSCredentials {
84+
accessKeyId: string;
85+
secretAccessKey: string;
86+
sessionToken: string;
87+
expiration?: Date;
88+
}
89+
7990
type CredentialProvider = {
91+
fromNodeProviderChain(
92+
this: void,
93+
options: { clientConfig: { region: string } }
94+
): () => Promise<AWSCredentials>;
8095
fromNodeProviderChain(this: void): () => Promise<AWSCredentials>;
8196
};
8297

test/integration/auth/mongodb_aws.test.ts

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as http from 'http';
33
import { performance } from 'perf_hooks';
44
import * as sinon from 'sinon';
55

6-
import { MongoAWSError, type MongoClient, MongoServerError } from '../../mongodb';
6+
import { MongoAWSError, type MongoClient, MongoDBAWS, MongoServerError } from '../../mongodb';
77

88
describe('MONGODB-AWS', function () {
99
let client: MongoClient;
@@ -88,4 +88,152 @@ describe('MONGODB-AWS', function () {
8888
expect(timeTaken).to.be.below(12000);
8989
});
9090
});
91+
92+
describe('when using AssumeRoleWithWebIdentity', () => {
93+
const tests = [
94+
{
95+
ctx: 'when no AWS region settings are set',
96+
title: 'uses the default region',
97+
env: {
98+
AWS_STS_REGIONAL_ENDPOINTS: undefined,
99+
AWS_REGION: undefined
100+
},
101+
calledWith: []
102+
},
103+
{
104+
ctx: 'when only AWS_STS_REGIONAL_ENDPOINTS is set',
105+
title: 'uses the default region',
106+
env: {
107+
AWS_STS_REGIONAL_ENDPOINTS: 'regional',
108+
AWS_REGION: undefined
109+
},
110+
calledWith: []
111+
},
112+
{
113+
ctx: 'when only AWS_REGION is set',
114+
title: 'uses the default region',
115+
env: {
116+
AWS_STS_REGIONAL_ENDPOINTS: undefined,
117+
AWS_REGION: 'us-west-2'
118+
},
119+
calledWith: []
120+
},
121+
122+
{
123+
ctx: 'when AWS_STS_REGIONAL_ENDPOINTS is set to regional and region is legacy',
124+
title: 'uses the region from the environment',
125+
env: {
126+
AWS_STS_REGIONAL_ENDPOINTS: 'regional',
127+
AWS_REGION: 'us-west-2'
128+
},
129+
calledWith: [{ clientConfig: { region: 'us-west-2' } }]
130+
},
131+
{
132+
ctx: 'when AWS_STS_REGIONAL_ENDPOINTS is set to regional and region is new',
133+
title: 'uses the region from the environment',
134+
env: {
135+
AWS_STS_REGIONAL_ENDPOINTS: 'regional',
136+
AWS_REGION: 'sa-east-1'
137+
},
138+
calledWith: [{ clientConfig: { region: 'sa-east-1' } }]
139+
},
140+
141+
{
142+
ctx: 'when AWS_STS_REGIONAL_ENDPOINTS is set to legacy and region is legacy',
143+
title: 'uses the region from the environment',
144+
env: {
145+
AWS_STS_REGIONAL_ENDPOINTS: 'legacy',
146+
AWS_REGION: 'us-west-2'
147+
},
148+
calledWith: []
149+
},
150+
{
151+
ctx: 'when AWS_STS_REGIONAL_ENDPOINTS is set to legacy and region is new',
152+
title: 'uses the default region',
153+
env: {
154+
AWS_STS_REGIONAL_ENDPOINTS: 'legacy',
155+
AWS_REGION: 'sa-east-1'
156+
},
157+
calledWith: []
158+
}
159+
];
160+
161+
for (const test of tests) {
162+
context(test.ctx, () => {
163+
let credentialProvider;
164+
let storedEnv;
165+
let calledArguments;
166+
let shouldSkip = false;
167+
168+
const envCheck = () => {
169+
const { AWS_WEB_IDENTITY_TOKEN_FILE = '' } = process.env;
170+
credentialProvider = (() => {
171+
try {
172+
return require('@aws-sdk/credential-providers');
173+
} catch {
174+
return null;
175+
}
176+
})();
177+
return AWS_WEB_IDENTITY_TOKEN_FILE.length === 0 || credentialProvider == null;
178+
};
179+
180+
beforeEach(function () {
181+
shouldSkip = envCheck();
182+
if (shouldSkip) {
183+
this.skipReason = 'only relevant to AssumeRoleWithWebIdentity with SDK installed';
184+
return this.skip();
185+
}
186+
187+
client = this.configuration.newClient(process.env.MONGODB_URI);
188+
189+
storedEnv = process.env;
190+
if (test.env.AWS_STS_REGIONAL_ENDPOINTS === undefined) {
191+
delete process.env.AWS_STS_REGIONAL_ENDPOINTS;
192+
} else {
193+
process.env.AWS_STS_REGIONAL_ENDPOINTS = test.env.AWS_STS_REGIONAL_ENDPOINTS;
194+
}
195+
if (test.env.AWS_REGION === undefined) {
196+
delete process.env.AWS_REGION;
197+
} else {
198+
process.env.AWS_REGION = test.env.AWS_REGION;
199+
}
200+
201+
calledArguments = [];
202+
MongoDBAWS.credentialProvider = {
203+
fromNodeProviderChain(...args) {
204+
calledArguments = args;
205+
return credentialProvider.fromNodeProviderChain(...args);
206+
}
207+
};
208+
});
209+
210+
afterEach(() => {
211+
if (shouldSkip) {
212+
return;
213+
}
214+
if (typeof storedEnv.AWS_STS_REGIONAL_ENDPOINTS === 'string') {
215+
process.env.AWS_STS_REGIONAL_ENDPOINTS = storedEnv.AWS_STS_REGIONAL_ENDPOINTS;
216+
}
217+
if (typeof storedEnv.AWS_STS_REGIONAL_ENDPOINTS === 'string') {
218+
process.env.AWS_REGION = storedEnv.AWS_REGION;
219+
}
220+
MongoDBAWS.credentialProvider = credentialProvider;
221+
calledArguments = [];
222+
});
223+
224+
it(test.title, async function () {
225+
const result = await client
226+
.db('aws')
227+
.collection('aws_test')
228+
.estimatedDocumentCount()
229+
.catch(error => error);
230+
231+
expect(result).to.not.be.instanceOf(MongoServerError);
232+
expect(result).to.be.a('number');
233+
234+
expect(calledArguments).to.deep.equal(test.calledWith);
235+
});
236+
});
237+
}
238+
});
91239
});

0 commit comments

Comments
 (0)