Skip to content

Commit e9a5079

Browse files
authored
fix(NODE-5551): set AWS region from environment variable for STSClient (#3831)
1 parent 87d172d commit e9a5079

File tree

3 files changed

+205
-5
lines changed

3 files changed

+205
-5
lines changed

src/cmap/auth/mongodb_aws.ts

Lines changed: 52 additions & 4 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() {
@@ -174,11 +198,11 @@ async function makeTempCredentials(credentials: MongoCredentials): Promise<Mongo
174198
});
175199
}
176200

177-
const credentialProvider = getAwsCredentialProvider();
201+
MongoDBAWS.credentialProvider ??= getAwsCredentialProvider();
178202

179203
// Check if the AWS credential provider from the SDK is present. If not,
180204
// use the old method.
181-
if ('kModuleError' in credentialProvider) {
205+
if ('kModuleError' in MongoDBAWS.credentialProvider) {
182206
// If the environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
183207
// is set then drivers MUST assume that it was set by an AWS ECS agent
184208
if (process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) {
@@ -209,6 +233,32 @@ async function makeTempCredentials(credentials: MongoCredentials): Promise<Mongo
209233

210234
return makeMongoCredentialsFromAWSTemp(creds);
211235
} 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+
212262
/*
213263
* Creates a credential provider that will attempt to find credentials from the
214264
* following sources (listed in order of precedence):
@@ -219,8 +269,6 @@ async function makeTempCredentials(credentials: MongoCredentials): Promise<Mongo
219269
* - Shared credentials and config ini files
220270
* - The EC2/ECS Instance Metadata Service
221271
*/
222-
const { fromNodeProviderChain } = credentialProvider;
223-
const provider = fromNodeProviderChain();
224272
try {
225273
const creds = await provider();
226274
return makeMongoCredentialsFromAWSTemp({

src/deps.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ export interface AWSCredentials {
8686
}
8787

8888
type CredentialProvider = {
89+
fromNodeProviderChain(
90+
this: void,
91+
options: { clientConfig: { region: string } }
92+
): () => Promise<AWSCredentials>;
8993
fromNodeProviderChain(this: void): () => Promise<AWSCredentials>;
9094
};
9195

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)