Skip to content

Commit d410602

Browse files
committed
MONGOSH-523 - AutoEncryption only run on enterprise
1 parent 4a64db7 commit d410602

File tree

2 files changed

+111
-11
lines changed

2 files changed

+111
-11
lines changed

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

Lines changed: 73 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,78 @@ 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).to.include('automatic encryption');
104+
}
105+
expect.fail('Failed to throw expected error');
106+
});
107+
});
108+
37109
describe('#constructor', () => {
38110
const mongoClient: any = sinon.spy();
39111
serviceProvider = new CliServiceProvider(mongoClient);

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

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ import {
7373
bson as BSON
7474
} from '@mongosh/service-provider-core';
7575

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

7878
const bsonlib = {
7979
Binary,
@@ -119,7 +119,35 @@ const DEFAULT_BASE_OPTIONS = Object.freeze({
119119
});
120120

121121
/**
122-
* Encapsulates logic for the service provider for the mongosh CLI.
122+
* Connect a MongoClient. If AutoEncryption is requested, first connect without the encryption options and verify that
123+
* the connection is to an enterprise cluster. If not, then error, otherwise close the connection and reconnect with the
124+
* options the user initially specified. Provide the client class as an additional argument in order to test.
125+
* @param uri {String}
126+
* @param clientOptions {MongoClientOptions}
127+
* @param mClient {MongoClient}
128+
*/
129+
export async function connectMongoClient(uri: string, clientOptions: MongoClientOptions, mClient = MongoClient): Promise<MongoClient | void> {
130+
if (clientOptions.autoEncryption !== undefined &&
131+
!clientOptions.autoEncryption.bypassAutoEncryption) {
132+
// connect first without autoEncryptionOptions
133+
const optionsWithoutFLE = { ...clientOptions };
134+
delete optionsWithoutFLE.autoEncryption;
135+
const client = await mClient.connect(uri, optionsWithoutFLE);
136+
const buildInfo = await client.db('admin').admin().command({ buildInfo: 1 });
137+
if (
138+
!(buildInfo.gitVersion && buildInfo.gitVersion.match(/enterprise/)) &&
139+
!(buildInfo.modules && buildInfo.modules.indexOf('enterprise') !== -1)
140+
) {
141+
await client.close();
142+
throw new MongoshRuntimeError('Cannot turn on automatic encryption for connections to non-enterprise hosts');
143+
}
144+
await client.close();
145+
}
146+
return mClient.connect(uri, clientOptions);
147+
}
148+
149+
/**
150+
* Encapsulates logic for the service provider for the mongosh CLI.
123151
*/
124152
class CliServiceProvider extends ServiceProviderCore implements ServiceProvider {
125153
/**
@@ -142,10 +170,10 @@ class CliServiceProvider extends ServiceProviderCore implements ServiceProvider
142170
};
143171

144172
const mongoClient = !cliOptions.nodb ?
145-
await MongoClient.connect(
173+
await connectMongoClient(
146174
uri,
147175
clientOptions
148-
) :
176+
) as MongoClient :
149177
new MongoClient(uri || 'mongodb://nodb/', clientOptions);
150178

151179
return new CliServiceProvider(mongoClient, clientOptions, uri);
@@ -192,10 +220,10 @@ class CliServiceProvider extends ServiceProviderCore implements ServiceProvider
192220
...options
193221
};
194222

195-
const mongoClient = await MongoClient.connect(
223+
const mongoClient = await connectMongoClient(
196224
uri,
197225
clientOptions
198-
);
226+
) as MongoClient;
199227
return new CliServiceProvider(mongoClient, uri);
200228
}
201229

@@ -1023,10 +1051,10 @@ class CliServiceProvider extends ServiceProviderCore implements ServiceProvider
10231051
};
10241052
if (authDoc.mechanism) clientOptions.authMechanism = authDoc.mechanism as AuthMechanismId;
10251053
if (authDoc.authDb) clientOptions.authSource = authDoc.authDb;
1026-
const mc = await MongoClient.connect(
1054+
const mc = await connectMongoClient(
10271055
this.uri as string,
10281056
clientOptions
1029-
);
1057+
) as MongoClient;
10301058
try {
10311059
await this.mongoClient.close();
10321060
// eslint-disable-next-line no-empty
@@ -1092,10 +1120,10 @@ class CliServiceProvider extends ServiceProviderCore implements ServiceProvider
10921120
...this.initialOptions,
10931121
...options
10941122
};
1095-
const mc = await MongoClient.connect(
1123+
const mc = await connectMongoClient(
10961124
this.uri as string,
10971125
clientOptions
1098-
);
1126+
) as MongoClient;
10991127
try {
11001128
await this.mongoClient.close();
11011129
// eslint-disable-next-line no-empty

0 commit comments

Comments
 (0)