Skip to content

Commit e54af10

Browse files
authored
fix(NODE-5053): enforce empty map for kmsProvider auto credentials (#565)
1 parent 4c4aa8b commit e54af10

File tree

3 files changed

+176
-127
lines changed

3 files changed

+176
-127
lines changed
+45-19
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
'use strict';
22

3-
let awsCredentialProviders = null;
4-
53
/**
6-
* Load cloud provider credentials for the user provided KMS providers.
7-
* Credentials will only attempt to get loaded if they do not exist
8-
* and no existing credentials will get overwritten.
4+
* @ignore
5+
* Auto credential fetching should only occur when the provider is defined on the kmsProviders map
6+
* and the settings are an empty object.
97
*
10-
* @param {Object} kmsProviders - The user provided KMS providers.
11-
* @returns {Promise} The new kms providers.
8+
* This is distinct from a nullish provider key.
129
*
13-
* @ignore
10+
* @param {string} provider
11+
* @param {object} kmsProviders
1412
*/
15-
async function loadCredentials(kmsProviders) {
13+
function isEmptyCredentials(provider, kmsProviders) {
14+
return (
15+
provider in kmsProviders &&
16+
kmsProviders[provider] != null &&
17+
typeof kmsProviders[provider] === 'object' &&
18+
Object.keys(kmsProviders[provider]).length === 0
19+
);
20+
}
21+
22+
let awsCredentialProviders = null;
23+
/** @ignore */
24+
async function loadAWSCredentials(kmsProviders) {
1625
if (awsCredentialProviders == null) {
1726
try {
1827
// Ensure you always wrap an optional require in the try block NODE-3199
@@ -22,18 +31,35 @@ async function loadCredentials(kmsProviders) {
2231
}
2332

2433
if (awsCredentialProviders != null) {
25-
const aws = kmsProviders.aws;
26-
if (!aws || Object.keys(aws).length === 0) {
27-
const { fromNodeProviderChain } = awsCredentialProviders;
28-
const provider = fromNodeProviderChain();
29-
// The state machine is the only place calling this so it will
30-
// catch if there is a rejection here.
31-
const awsCreds = await provider();
32-
return { ...kmsProviders, aws: awsCreds };
33-
}
34+
const { fromNodeProviderChain } = awsCredentialProviders;
35+
const provider = fromNodeProviderChain();
36+
// The state machine is the only place calling this so it will
37+
// catch if there is a rejection here.
38+
const aws = await provider();
39+
return { ...kmsProviders, aws };
3440
}
3541

3642
return kmsProviders;
3743
}
3844

39-
module.exports = { loadCredentials };
45+
/**
46+
* Load cloud provider credentials for the user provided KMS providers.
47+
* Credentials will only attempt to get loaded if they do not exist
48+
* and no existing credentials will get overwritten.
49+
*
50+
* @param {object} kmsProviders - The user provided KMS providers.
51+
* @returns {Promise} The new kms providers.
52+
*
53+
* @ignore
54+
*/
55+
async function loadCredentials(kmsProviders) {
56+
let finalKMSProviders = kmsProviders;
57+
58+
if (isEmptyCredentials('aws', kmsProviders)) {
59+
finalKMSProviders = await loadAWSCredentials(kmsProviders);
60+
}
61+
62+
return finalKMSProviders;
63+
}
64+
65+
module.exports = { loadCredentials, isEmptyCredentials };

bindings/node/test/credentialsProvider.test.js

+102-108
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,56 @@
22

33
const { expect } = require('chai');
44
const requirements = require('./requirements.helper');
5-
const { loadCredentials } = require('../lib/credentialsProvider');
5+
const { loadCredentials, isEmptyCredentials } = require('../lib/credentialsProvider');
66

77
const originalAccessKeyId = process.env.AWS_ACCESS_KEY_ID;
88
const originalSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
99
const originalSessionToken = process.env.AWS_SESSION_TOKEN;
1010

1111
describe('#loadCredentials', function () {
12-
const accessKey = 'example';
13-
const secretKey = 'example';
14-
const sessionToken = 'example';
15-
16-
after(function () {
17-
// After the entire suite runs, set the env back for the rest of the test run.
18-
process.env.AWS_ACCESS_KEY_ID = originalAccessKeyId;
19-
process.env.AWS_SECRET_ACCESS_KEY = originalSecretAccessKey;
20-
process.env.AWS_SESSION_TOKEN = originalSessionToken;
21-
});
12+
context('isEmptyCredentials()', () => {
13+
it('returns true for an empty object', () => {
14+
expect(isEmptyCredentials('rainyCloud', { rainyCloud: {} })).to.be.true;
15+
});
2216

23-
context('when the credential provider finds credentials', function () {
24-
before(function () {
25-
process.env.AWS_ACCESS_KEY_ID = accessKey;
26-
process.env.AWS_SECRET_ACCESS_KEY = secretKey;
27-
process.env.AWS_SESSION_TOKEN = sessionToken;
17+
it('returns false for an object with keys', () => {
18+
expect(isEmptyCredentials('rainyCloud', { rainyCloud: { password: 'secret' } })).to.be.false;
2819
});
2920

30-
context('when the credentials are empty', function () {
31-
const kmsProviders = {};
21+
it('returns false for an nullish credentials', () => {
22+
expect(isEmptyCredentials('rainyCloud', { rainyCloud: null })).to.be.false;
23+
expect(isEmptyCredentials('rainyCloud', { rainyCloud: undefined })).to.be.false;
24+
expect(isEmptyCredentials('rainyCloud', {})).to.be.false;
25+
});
3226

33-
before(function () {
34-
if (!requirements.credentialProvidersInstalled.aws) {
35-
this.currentTest.skipReason = 'Cannot refresh credentials without sdk provider';
36-
this.currentTest.skip();
37-
return;
38-
}
39-
});
27+
it('returns false for non object credentials', () => {
28+
expect(isEmptyCredentials('rainyCloud', { rainyCloud: 0 })).to.be.false;
29+
expect(isEmptyCredentials('rainyCloud', { rainyCloud: false })).to.be.false;
30+
expect(isEmptyCredentials('rainyCloud', { rainyCloud: Symbol('secret') })).to.be.false;
31+
});
32+
});
4033

41-
it('refreshes the aws credentials', async function () {
42-
const providers = await loadCredentials(kmsProviders);
43-
expect(providers).to.deep.equal({
44-
aws: {
45-
accessKeyId: accessKey,
46-
secretAccessKey: secretKey,
47-
sessionToken: sessionToken
48-
}
49-
});
50-
});
34+
context('when using aws', () => {
35+
const accessKey = 'example';
36+
const secretKey = 'example';
37+
const sessionToken = 'example';
38+
39+
after(function () {
40+
// After the entire suite runs, set the env back for the rest of the test run.
41+
process.env.AWS_ACCESS_KEY_ID = originalAccessKeyId;
42+
process.env.AWS_SECRET_ACCESS_KEY = originalSecretAccessKey;
43+
process.env.AWS_SESSION_TOKEN = originalSessionToken;
5144
});
5245

53-
context('when the credentials are not empty', function () {
54-
context('when aws is empty', function () {
55-
const kmsProviders = {
56-
local: {
57-
key: Buffer.alloc(96)
58-
},
59-
aws: {}
60-
};
46+
context('when the credential provider finds credentials', function () {
47+
before(function () {
48+
process.env.AWS_ACCESS_KEY_ID = accessKey;
49+
process.env.AWS_SECRET_ACCESS_KEY = secretKey;
50+
process.env.AWS_SESSION_TOKEN = sessionToken;
51+
});
52+
53+
context('when the credentials are empty', function () {
54+
const kmsProviders = { aws: {} };
6155

6256
before(function () {
6357
if (!requirements.credentialProvidersInstalled.aws) {
@@ -67,12 +61,9 @@ describe('#loadCredentials', function () {
6761
}
6862
});
6963

70-
it('refreshes only the aws credentials', async function () {
64+
it('refreshes the aws credentials', async function () {
7165
const providers = await loadCredentials(kmsProviders);
7266
expect(providers).to.deep.equal({
73-
local: {
74-
key: Buffer.alloc(96)
75-
},
7667
aws: {
7768
accessKeyId: accessKey,
7869
secretAccessKey: secretKey,
@@ -82,81 +73,84 @@ describe('#loadCredentials', function () {
8273
});
8374
});
8475

85-
context('when aws is not empty', function () {
86-
const kmsProviders = {
87-
local: {
88-
key: Buffer.alloc(96)
89-
},
90-
aws: {
91-
accessKeyId: 'example'
92-
}
93-
};
94-
95-
before(function () {
96-
if (!requirements.credentialProvidersInstalled.aws) {
97-
this.currentTest.skipReason = 'Cannot refresh credentials without sdk provider';
98-
this.currentTest.skip();
99-
return;
100-
}
101-
});
102-
103-
it('does not refresh credentials', async function () {
104-
const providers = await loadCredentials(kmsProviders);
105-
expect(providers).to.deep.equal(kmsProviders);
106-
});
107-
});
108-
109-
context('when aws does not exist', function () {
110-
const kmsProviders = {
111-
local: {
112-
key: Buffer.alloc(96)
113-
}
114-
};
76+
context('when the credentials are not empty', function () {
77+
context('when aws is empty', function () {
78+
const kmsProviders = {
79+
local: {
80+
key: Buffer.alloc(96)
81+
},
82+
aws: {}
83+
};
84+
85+
before(function () {
86+
if (!requirements.credentialProvidersInstalled.aws) {
87+
this.currentTest.skipReason = 'Cannot refresh credentials without sdk provider';
88+
this.currentTest.skip();
89+
return;
90+
}
91+
});
11592

116-
before(function () {
117-
if (!requirements.credentialProvidersInstalled.aws) {
118-
this.currentTest.skipReason = 'Cannot refresh credentials without sdk provider';
119-
this.currentTest.skip();
120-
return;
121-
}
93+
it('refreshes only the aws credentials', async function () {
94+
const providers = await loadCredentials(kmsProviders);
95+
expect(providers).to.deep.equal({
96+
local: {
97+
key: Buffer.alloc(96)
98+
},
99+
aws: {
100+
accessKeyId: accessKey,
101+
secretAccessKey: secretKey,
102+
sessionToken: sessionToken
103+
}
104+
});
105+
});
122106
});
123107

124-
it('refreshes ony the aws credentials', async function () {
125-
const providers = await loadCredentials(kmsProviders);
126-
expect(providers).to.deep.equal({
108+
context('when aws is not empty', function () {
109+
const kmsProviders = {
127110
local: {
128111
key: Buffer.alloc(96)
129112
},
130113
aws: {
131-
accessKeyId: accessKey,
132-
secretAccessKey: secretKey,
133-
sessionToken: sessionToken
114+
accessKeyId: 'example'
115+
}
116+
};
117+
118+
before(function () {
119+
if (!requirements.credentialProvidersInstalled.aws) {
120+
this.currentTest.skipReason = 'Cannot refresh credentials without sdk provider';
121+
this.currentTest.skip();
122+
return;
134123
}
135124
});
125+
126+
it('does not refresh credentials', async function () {
127+
const providers = await loadCredentials(kmsProviders);
128+
expect(providers).to.deep.equal(kmsProviders);
129+
});
136130
});
137131
});
138132
});
139-
});
140133

141-
context('when the sdk is not installed', function () {
142-
const kmsProviders = {
143-
local: {
144-
key: Buffer.alloc(96)
145-
},
146-
aws: {}
147-
};
148-
149-
before(function () {
150-
if (requirements.credentialProvidersInstalled.aws) {
151-
this.currentTest.skipReason = 'Credentials will be loaded when sdk present';
152-
this.currentTest.skip();
153-
return;
154-
}
155-
});
134+
context('when the sdk is not installed', function () {
135+
const kmsProviders = {
136+
local: {
137+
key: Buffer.alloc(96)
138+
},
139+
aws: {}
140+
};
156141

157-
it('does not refresh credentials', async function () {
158-
const providers = await loadCredentials(kmsProviders);
159-
expect(providers).to.deep.equal(kmsProviders);
142+
before(function () {
143+
if (requirements.credentialProvidersInstalled.aws) {
144+
this.currentTest.skipReason = 'Credentials will be loaded when sdk present';
145+
this.currentTest.skip();
146+
return;
147+
}
148+
});
149+
150+
it('does not refresh credentials', async function () {
151+
const providers = await loadCredentials(kmsProviders);
152+
expect(providers).to.deep.equal(kmsProviders);
153+
});
160154
});
161155
});
162156
});

bindings/node/test/index.test.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
3+
const { expect } = require('chai');
4+
const mongodbClientEncryption = require('../lib/index');
5+
6+
// Update this as you add exports, helps double check we don't accidentally remove something
7+
// since not all tests import from the root public export
8+
const EXPECTED_EXPORTS = [
9+
'extension',
10+
'MongoCryptError',
11+
'MongoCryptCreateEncryptedCollectionError',
12+
'MongoCryptCreateDataKeyError',
13+
'AutoEncrypter',
14+
'ClientEncryption'
15+
];
16+
17+
describe('mongodb-client-encryption entrypoint', () => {
18+
it('should export all and only the expected keys in expected_exports', () => {
19+
expect(mongodbClientEncryption).to.have.all.keys(EXPECTED_EXPORTS);
20+
});
21+
22+
it('extension returns an object equal in shape to the default except for extension', () => {
23+
const extensionResult = mongodbClientEncryption.extension(require('mongodb'));
24+
const expectedExports = EXPECTED_EXPORTS.filter(exp => exp !== 'extension');
25+
const exportsDefault = Object.keys(mongodbClientEncryption).filter(exp => exp !== 'extension');
26+
expect(extensionResult).to.have.all.keys(expectedExports);
27+
expect(extensionResult).to.have.all.keys(exportsDefault);
28+
});
29+
});

0 commit comments

Comments
 (0)