Skip to content

Commit 583947a

Browse files
authored
MONGOSH-523 - AutoEncryption only run on enterprise (#545)
1 parent d2f0887 commit 583947a

File tree

2 files changed

+127
-7
lines changed

2 files changed

+127
-7
lines changed

packages/service-provider-server/src/cli-service-provider.spec.ts

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import chai, { expect } from 'chai';
33
import { Collection, Db, MongoClient } from 'mongodb';
44
import sinonChai from 'sinon-chai';
55
import sinon, { StubbedInstance, stubInterface } from 'ts-sinon';
6-
import CliServiceProvider from './cli-service-provider';
6+
import CliServiceProvider, { connectMongoClient } from './cli-service-provider';
77

88
chai.use(sinonChai);
99

@@ -34,6 +34,98 @@ describe('CliServiceProvider', () => {
3434
let serviceProvider: CliServiceProvider;
3535
let collectionStub: StubbedInstance<Collection>;
3636

37+
describe('connectMongoClient', () => {
38+
it('connects once when no AutoEncryption set', async() => {
39+
const uri = 'localhost:27017';
40+
const mClientType = stubInterface<typeof MongoClient>();
41+
const mClient = stubInterface<MongoClient>();
42+
mClientType.connect.onFirstCall().resolves(mClient);
43+
const result = await connectMongoClient(uri, {}, mClientType);
44+
const calls = mClientType.connect.getCalls();
45+
expect(calls.length).to.equal(1);
46+
expect(calls[0].args).to.deep.equal([
47+
uri, {}
48+
]);
49+
expect(result).to.equal(mClient);
50+
});
51+
it('connects once when bypassAutoEncryption is true', async() => {
52+
const uri = 'localhost:27017';
53+
const opts = { autoEncryption: { bypassAutoEncryption: true } };
54+
const mClientType = stubInterface<typeof MongoClient>();
55+
const mClient = stubInterface<MongoClient>();
56+
mClientType.connect.onFirstCall().resolves(mClient);
57+
const result = await connectMongoClient(uri, opts, mClientType);
58+
const calls = mClientType.connect.getCalls();
59+
expect(calls.length).to.equal(1);
60+
expect(calls[0].args).to.deep.equal([
61+
uri, opts
62+
]);
63+
expect(result).to.equal(mClient);
64+
});
65+
it('connects twice when bypassAutoEncryption is false and enterprise via modules', async() => {
66+
const uri = 'localhost:27017';
67+
const opts = { autoEncryption: { bypassAutoEncryption: false } };
68+
const mClientType = stubInterface<typeof MongoClient>();
69+
const mClientFirst = stubInterface<MongoClient>();
70+
const commandSpy = sinon.spy();
71+
mClientFirst.db.returns({ admin: () => ({ command: (...args) => {
72+
commandSpy(...args);
73+
return { modules: [ 'enterprise' ] };
74+
} } as any) } as any);
75+
const mClientSecond = stubInterface<MongoClient>();
76+
mClientType.connect.onFirstCall().resolves(mClientFirst);
77+
mClientType.connect.onSecondCall().resolves(mClientSecond);
78+
const result = await connectMongoClient(uri, opts, mClientType);
79+
const calls = mClientType.connect.getCalls();
80+
expect(calls.length).to.equal(2);
81+
expect(calls[0].args).to.deep.equal([
82+
uri, {}
83+
]);
84+
expect(commandSpy).to.have.been.calledOnceWithExactly({ buildInfo: 1 });
85+
expect(result).to.equal(mClientSecond);
86+
});
87+
it('errors when bypassAutoEncryption is falsy and not enterprise', async() => {
88+
const uri = 'localhost:27017';
89+
const opts = { autoEncryption: {} };
90+
const mClientType = stubInterface<typeof MongoClient>();
91+
const mClientFirst = stubInterface<MongoClient>();
92+
const commandSpy = sinon.spy();
93+
mClientFirst.db.returns({ admin: () => ({ command: (...args) => {
94+
commandSpy(...args);
95+
return { modules: [] };
96+
} } as any) } as any);
97+
const mClientSecond = stubInterface<MongoClient>();
98+
mClientType.connect.onFirstCall().resolves(mClientFirst);
99+
mClientType.connect.onSecondCall().resolves(mClientSecond);
100+
try {
101+
await connectMongoClient(uri, opts, mClientType);
102+
} catch (e) {
103+
return expect(e.message.toLowerCase()).to.include('automatic encryption');
104+
}
105+
expect.fail('Failed to throw expected error');
106+
});
107+
it('errors when bypassAutoEncryption is falsy, missing modules', async() => {
108+
const uri = 'localhost:27017';
109+
const opts = { autoEncryption: {} };
110+
const mClientType = stubInterface<typeof MongoClient>();
111+
const mClientFirst = stubInterface<MongoClient>();
112+
const commandSpy = sinon.spy();
113+
mClientFirst.db.returns({ admin: () => ({ command: (...args) => {
114+
commandSpy(...args);
115+
return {};
116+
} } as any) } as any);
117+
const mClientSecond = stubInterface<MongoClient>();
118+
mClientType.connect.onFirstCall().resolves(mClientFirst);
119+
mClientType.connect.onSecondCall().resolves(mClientSecond);
120+
try {
121+
await connectMongoClient(uri, opts, mClientType);
122+
} catch (e) {
123+
return expect(e.message.toLowerCase()).to.include('automatic encryption');
124+
}
125+
expect.fail('Failed to throw expected error');
126+
});
127+
});
128+
37129
describe('#constructor', () => {
38130
const mongoClient: any = sinon.spy();
39131
serviceProvider = new CliServiceProvider(mongoClient);

packages/service-provider-server/src/cli-service-provider.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ import {
7474
ConnectionString
7575
} from '@mongosh/service-provider-core';
7676

77-
import { MongoshCommandFailed, MongoshInternalError } from '@mongosh/errors';
77+
import { MongoshCommandFailed, MongoshInternalError, MongoshRuntimeError } from '@mongosh/errors';
7878

7979
const bsonlib = {
8080
Binary,
@@ -127,7 +127,35 @@ const DEFAULT_BASE_OPTIONS = Object.freeze({
127127
});
128128

129129
/**
130-
* Encapsulates logic for the service provider for the mongosh CLI.
130+
* Connect a MongoClient. If AutoEncryption is requested, first connect without the encryption options and verify that
131+
* the connection is to an enterprise cluster. If not, then error, otherwise close the connection and reconnect with the
132+
* options the user initially specified. Provide the client class as an additional argument in order to test.
133+
* @param uri {String}
134+
* @param clientOptions {MongoClientOptions}
135+
* @param mClient {MongoClient}
136+
*/
137+
export async function connectMongoClient(uri: string, clientOptions: MongoClientOptions, mClient = MongoClient): Promise<MongoClient> {
138+
if (clientOptions.autoEncryption !== undefined &&
139+
!clientOptions.autoEncryption.bypassAutoEncryption) {
140+
// connect first without autoEncryptionOptions
141+
const optionsWithoutFLE = { ...clientOptions };
142+
delete optionsWithoutFLE.autoEncryption;
143+
const client = await mClient.connect(uri, optionsWithoutFLE);
144+
const buildInfo = await client.db('admin').admin().command({ buildInfo: 1 });
145+
if (
146+
!(buildInfo.modules?.includes('enterprise')) &&
147+
!(buildInfo.gitVersion?.match(/enterprise/))
148+
) {
149+
await client.close();
150+
throw new MongoshRuntimeError('Automatic encryption is only available with Atlas and MongoDB Enterprise');
151+
}
152+
await client.close();
153+
}
154+
return mClient.connect(uri, clientOptions);
155+
}
156+
157+
/**
158+
* Encapsulates logic for the service provider for the mongosh CLI.
131159
*/
132160
class CliServiceProvider extends ServiceProviderCore implements ServiceProvider {
133161
/**
@@ -148,7 +176,7 @@ class CliServiceProvider extends ServiceProviderCore implements ServiceProvider
148176
const clientOptions = processDriverOptions(driverOptions);
149177

150178
const mongoClient = !cliOptions.nodb ?
151-
await MongoClient.connect(
179+
await connectMongoClient(
152180
connectionString.toString(),
153181
clientOptions
154182
) :
@@ -196,7 +224,7 @@ class CliServiceProvider extends ServiceProviderCore implements ServiceProvider
196224
const connectionString = new ConnectionString(uri);
197225
const clientOptions = processDriverOptions(options);
198226

199-
const mongoClient = await MongoClient.connect(
227+
const mongoClient = await connectMongoClient(
200228
connectionString.toString(),
201229
clientOptions
202230
);
@@ -1026,7 +1054,7 @@ class CliServiceProvider extends ServiceProviderCore implements ServiceProvider
10261054
});
10271055
if (authDoc.mechanism) clientOptions.authMechanism = authDoc.mechanism as AuthMechanismId;
10281056
if (authDoc.authDb) clientOptions.authSource = authDoc.authDb;
1029-
const mc = await MongoClient.connect(
1057+
const mc = await connectMongoClient(
10301058
Object.assign((this.uri as ConnectionString).clone(), {
10311059
username: '', password: ''
10321060
}).toString(),
@@ -1096,7 +1124,7 @@ class CliServiceProvider extends ServiceProviderCore implements ServiceProvider
10961124
...this.initialOptions,
10971125
...options
10981126
});
1099-
const mc = await MongoClient.connect(
1127+
const mc = await connectMongoClient(
11001128
// TODO This seems to potentially undo a previous db.auth(), MONGOSH-529
11011129
(this.uri as ConnectionString).toString(),
11021130
clientOptions

0 commit comments

Comments
 (0)