Skip to content

Commit f8d9c7c

Browse files
fix: share aws credential fetching logic with aws KMS and only return expected fields
1 parent 458cf6d commit f8d9c7c

File tree

3 files changed

+105
-25
lines changed

3 files changed

+105
-25
lines changed
Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
import { getAwsCredentialProvider } from '../../deps';
22
import { type KMSProviders } from '.';
3+
import { AWSSDKCredentialProvider } from '../../cmap/auth/aws_temporary_credentials';
34

45
/**
56
* @internal
67
*/
78
export async function loadAWSCredentials(kmsProviders: KMSProviders): Promise<KMSProviders> {
8-
const credentialProvider = getAwsCredentialProvider();
9+
const credentialProvider = new AWSSDKCredentialProvider();
910

10-
if ('kModuleError' in credentialProvider) {
11-
return kmsProviders;
12-
}
11+
// We shouldn't ever receive a response from the AWS SDK that doesn't have a `SecretAccessKey`
12+
// or `AccessKeyId`. However, TS says these fields are optional. We provide empty strings
13+
// and let libmongocrypt error if we're unable to fetch the required keys.
14+
const {
15+
SecretAccessKey = '',
16+
AccessKeyId = '',
17+
Token
18+
} = await credentialProvider.getCredentials();
19+
const aws: NonNullable<KMSProviders['aws']> = {
20+
secretAccessKey: SecretAccessKey,
21+
accessKeyId: AccessKeyId
22+
};
23+
// the AWS session token is only required for temporary credentials so only attach it to the
24+
// result if it's present in the response from the aws sdk
25+
Token != null && (aws.sessionToken = Token);
1326

14-
const { fromNodeProviderChain } = credentialProvider;
15-
const provider = fromNodeProviderChain();
16-
// The state machine is the only place calling this so it will
17-
// catch if there is a rejection here.
18-
const aws = await provider();
1927
return { ...kmsProviders, aws };
2028
}

test/integration/auth/mongodb_aws.test.ts

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,26 @@ import * as http from 'http';
55
import { performance } from 'perf_hooks';
66
import * as sinon from 'sinon';
77

8+
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
9+
import { KMSCredentialProvider, refreshKMSCredentials } from '../../../src/client-side-encryption/providers';
810
import {
911
AWSTemporaryCredentialProvider,
1012
MongoAWSError,
1113
type MongoClient,
1214
MongoDBAWS,
1315
MongoMissingCredentialsError,
14-
MongoServerError
16+
MongoServerError,
17+
setDifference
1518
} from '../../mongodb';
1619

17-
function awsSdk() {
18-
try {
19-
return require('@aws-sdk/credential-providers');
20-
} catch {
21-
return null;
22-
}
23-
}
20+
const isMongoDBAWSAuthEnvironment = (process.env.MONGODB_URI ?? '').includes('MONGODB_AWS');
2421

2522
describe('MONGODB-AWS', function () {
2623
let awsSdkPresent;
2724
let client: MongoClient;
2825

2926
beforeEach(function () {
30-
const MONGODB_URI = process.env.MONGODB_URI;
31-
if (!MONGODB_URI || MONGODB_URI.indexOf('MONGODB-AWS') === -1) {
27+
if (!isMongoDBAWSAuthEnvironment) {
3228
this.currentTest.skipReason = 'requires MONGODB_URI to contain MONGODB-AWS auth mechanism';
3329
return this.skip();
3430
}
@@ -39,7 +35,7 @@ describe('MONGODB-AWS', function () {
3935
`Always inform the AWS tests if they run with or without the SDK (MONGODB_AWS_SDK=${MONGODB_AWS_SDK})`
4036
).to.include(MONGODB_AWS_SDK);
4137

42-
awsSdkPresent = !!awsSdk();
38+
awsSdkPresent = AWSTemporaryCredentialProvider.isAWSSDKInstalled;
4339
expect(
4440
awsSdkPresent,
4541
MONGODB_AWS_SDK === 'true'
@@ -244,8 +240,10 @@ describe('MONGODB-AWS', function () {
244240

245241
const envCheck = () => {
246242
const { AWS_WEB_IDENTITY_TOKEN_FILE = '' } = process.env;
247-
credentialProvider = awsSdk();
248-
return AWS_WEB_IDENTITY_TOKEN_FILE.length === 0 || credentialProvider == null;
243+
return (
244+
AWS_WEB_IDENTITY_TOKEN_FILE.length === 0 ||
245+
!AWSTemporaryCredentialProvider.isAWSSDKInstalled
246+
);
249247
};
250248

251249
beforeEach(function () {
@@ -255,6 +253,9 @@ describe('MONGODB-AWS', function () {
255253
return this.skip();
256254
}
257255

256+
// @ts-expect-error We intentionally access a protected variable.
257+
credentialProvider = AWSTemporaryCredentialProvider.awsSDK;
258+
258259
storedEnv = process.env;
259260
if (test.env.AWS_STS_REGIONAL_ENDPOINTS === undefined) {
260261
delete process.env.AWS_STS_REGIONAL_ENDPOINTS;
@@ -324,3 +325,49 @@ describe('MONGODB-AWS', function () {
324325
}
325326
});
326327
});
328+
329+
describe('AWS KMS Credential Fetching', function () {
330+
context('when the AWS SDK is not installed', function () {
331+
beforeEach(function () {
332+
this.currentTest.skipReason = !isMongoDBAWSAuthEnvironment
333+
? 'Test must run in an AWS auth testing environment'
334+
: AWSTemporaryCredentialProvider.isAWSSDKInstalled
335+
? 'This test must run in an environment where the AWS SDK is not installed.'
336+
: undefined;
337+
this.currentTest?.skipReason && this.skip();
338+
});
339+
it('fetching AWS KMS credentials throws an error', async function () {
340+
const error = await new KMSCredentialProvider({ aws: {} }).refreshCredentials().catch(e => e);
341+
expect(error).to.be.instanceOf(MongoAWSError);
342+
});
343+
});
344+
345+
context('when the AWS SDK is installed', function () {
346+
beforeEach(function () {
347+
this.currentTest.skipReason = !isMongoDBAWSAuthEnvironment
348+
? 'Test must run in an AWS auth testing environment'
349+
: AWSTemporaryCredentialProvider.isAWSSDKInstalled
350+
? 'This test must run in an environment where the AWS SDK is installed.'
351+
: undefined;
352+
this.currentTest?.skipReason && this.skip();
353+
});
354+
it('KMS credentials are successfully fetched.', async function () {
355+
const { aws } = await refreshKMSCredentials({ aws: {} });
356+
357+
expect(aws).to.have.property('accessKeyId');
358+
expect(aws).to.have.property('secretAccessKey');
359+
});
360+
361+
it('does not return any extra keys for the `aws` credential provider', async function () {
362+
const { aws } = await refreshKMSCredentials({ aws: {} });
363+
364+
const keys = new Set(Object.keys(aws ?? {}));
365+
const allowedKeys = ['accessKeyId', 'secretAccessKey', 'sessionToken'];
366+
367+
expect(
368+
setDifference(keys, allowedKeys),
369+
'received an unexpected key in the response refreshing KMS credentials'
370+
).to.deep.equal([]);
371+
});
372+
});
373+
});

test/unit/client-side-encryption/providers/credentialsProvider.test.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
2222
import * as utils from '../../../../src/client-side-encryption/providers/utils';
2323
import * as requirements from '../requirements.helper';
24+
import { AWSSDKCredentialProvider } from '../../../../src/cmap/auth/aws_temporary_credentials';
25+
import { MongoAWSError } from '../../../../src/error';
2426

2527
const originalAccessKeyId = process.env.AWS_ACCESS_KEY_ID;
2628
const originalSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
@@ -170,9 +172,32 @@ describe('#refreshKMSCredentials', function () {
170172
}
171173
});
172174

173-
it('does not refresh credentials', async function () {
174-
const providers = await refreshKMSCredentials(kmsProviders);
175-
expect(providers).to.deep.equal(kmsProviders);
175+
it('throws a MongoAWSError', async function () {
176+
const error = await refreshKMSCredentials(kmsProviders).catch(e => e);
177+
const expectedErrorMessage = 'Optional module `@aws-sdk/credential-providers` not found';
178+
expect(error).to.be.instanceOf(MongoAWSError).to.match(new RegExp(expectedErrorMessage, 'i'));
179+
});
180+
});
181+
182+
context('when the AWS SDK returns unknown fields', function () {
183+
beforeEach(() => {
184+
sinon.stub(AWSSDKCredentialProvider.prototype, 'getCredentials').resolves({
185+
Token: 'example',
186+
SecretAccessKey: 'example',
187+
AccessKeyId: 'example',
188+
Expiration: new Date()
189+
});
190+
});
191+
afterEach(() => sinon.restore());
192+
it('only returns fields libmongocrypt expects', async function () {
193+
const credentials = await refreshKMSCredentials({ aws: {} });
194+
expect(credentials).to.deep.equal({
195+
aws: {
196+
accessKeyId: accessKey,
197+
secretAccessKey: secretKey,
198+
sessionToken: sessionToken
199+
}
200+
});
176201
});
177202
});
178203
});

0 commit comments

Comments
 (0)