Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(shell-api): adds createEncryptedCollection on Database and ClientEncryption #1416

Merged
merged 13 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/cli-repl/test/e2e-fle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,41 @@ describe('FLE tests', () => {
const compactResult = await shell.executeLine(`autoMongo.getDB('${dbname}').test.compactStructuredEncryptionData()`);
expect(compactResult).to.include('ok: 1');
});

it('creates an encrypted collection and generates data encryption keys automatically per encrypted fields', async() => {
const shell = TestShell.start({ args: ['--nodb', `--cryptSharedLibPath=${cryptLibrary}`] });
himanshusinghs marked this conversation as resolved.
Show resolved Hide resolved
const uri = JSON.stringify(await testServer.connectionString());
await shell.waitForPrompt();
await shell.executeLine('local = { key: BinData(0, "kh4Gv2N8qopZQMQYMEtww/AkPsIrXNmEMxTrs3tUoTQZbZu4msdRUaR8U5fXD7A7QXYHcEvuu4WctJLoT+NvvV3eeIg3MD+K8H9SR794m/safgRHdIfy6PD+rFpvmFbY") }');
await shell.executeLine(`keyMongo = Mongo(${uri}, { \
keyVaultNamespace: '${dbname}.keyVault', \
kmsProviders: { local } \
});`);
await shell.executeLine(`secretDB = keyMongo.getDB('${dbname}')`);
await shell.executeLine(`var { collection, encryptedFields } = secretDB.createEncryptedCollection('secretCollection', {
provider: 'local',
createCollectionOptions: {
encryptedFields: {
fields: [{
keyId: null,
path: 'secretField',
bsonType: 'string'
}]
}
}
});`);

await shell.executeLine(`plainMongo = Mongo(${uri});`);
const collections = await shell.executeLine(`plainMongo.getDB('${dbname}').getCollectionNames()`);
expect(collections).to.include('enxcol_.secretCollection.ecc');
expect(collections).to.include('enxcol_.secretCollection.esc');
expect(collections).to.include('enxcol_.secretCollection.ecoc');
expect(collections).to.include('secretCollection');

const dekCount = await shell.executeLine(`plainMongo.getDB('${dbname}').getCollection('keyVault').countDocuments()`);
// Since there is only one field to be encrypted hence there would only be one DEK in our keyvault collection
expect(parseInt(dekCount.trim(), 10)).to.equal(1);
});
});

context('6.2+', () => {
Expand Down
12 changes: 11 additions & 1 deletion packages/i18n/src/locales/en_US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,11 @@ const translations: Catalog = {
description: 'Create new collection',
example: 'db.createCollection(\'collName\')'
},
createEncryptedCollection: {
link: 'https://docs.mongodb.com/manual/reference/method/db.createEncryptedCollection/',
description: 'Creates a new collection with a list of encrypted fields each with unique and auto-created data encryption keys (DEKs). This is a utility function that internally utilises ClientEnryption.createEncryptedCollection.',
example: 'db.createEncryptedCollection( "collName", { "provider": "<kmsProvider>", "createCollectionOptions": { "encryptedFields": { ... }, ...<otherOptions> } })'
},
createView: {
link: 'https://docs.mongodb.com/manual/reference/method/db.createView/',
description: 'Create new view',
Expand Down Expand Up @@ -2202,7 +2207,12 @@ const translations: Catalog = {
decrypt: {
link: 'https://docs.mongodb.com/manual/reference/method/ClientEncryption.decrypt/#ClientEncryption.decrypt',
description: 'decrypts the encryptionValue if the current database connection was configured with access to the Key Management Service (KMS) and key vault used to encrypt encryptionValue.'
}
},
createEncryptedCollection: {
link: 'https://docs.mongodb.com/manual/reference/method/ClientEncryption.createEncryptedCollection/#ClientEncryption.createEncryptedCollection',
description: 'Creates a new collection with a list of encrypted fields each with unique and auto-created data encryption keys (DEKs). This method should be invoked on a connection instantiated with queryable encryption options.',
example: 'db.getMongo().getClientEncryption().createEncryptedCollection( "dbName", "collName", { "provider": "<kmsProvider>", "createCollectionOptions": { "encryptedFields": { ... }, ...<otherOptions> } })'
},
}
}
}
Expand Down
14 changes: 7 additions & 7 deletions packages/service-provider-core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/service-provider-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"mongodb-build-info": "^1.5.0"
},
"optionalDependencies": {
"mongodb-client-encryption": "^2.4.0"
"mongodb-client-encryption": "^2.5.0"
},
"dependency-check": {
"entries": [
Expand Down
29 changes: 26 additions & 3 deletions packages/service-provider-core/src/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,25 @@ import type {
DbOptions,
ClientSessionOptions,
ListDatabasesOptions,
AutoEncryptionOptions
AutoEncryptionOptions,
Collection
} from './all-transport-types';
import type { bson as BSON } from './index';
import type { ReplPlatform } from './platform';
import { FLE } from './all-fle-types';

import {
AWSEncryptionKeyOptions,
AzureEncryptionKeyOptions,
ClientEncryption as MongoCryptClientEncryption,
ClientEncryptionDataKeyProvider,
FLE,
GCPEncryptionKeyOptions
} from './all-fle-types';

export interface CreateEncryptedCollectionOptions {
provider: ClientEncryptionDataKeyProvider,
createCollectionOptions: Omit<CreateCollectionOptions, 'encryptedFields'> & { encryptedFields: Document },
masterKey?: AWSEncryptionKeyOptions | AzureEncryptionKeyOptions | GCPEncryptionKeyOptions;
}

export default interface Admin {
/**
Expand Down Expand Up @@ -116,4 +129,14 @@ export default interface Admin {
* The FLE options passed to the client, if any.
*/
getFleOptions?: () => AutoEncryptionOptions | undefined;

/**
* The helper method to correctly access FLE implementation's createEncryptedCollection
*/
createEncryptedCollection?(
dbName: string,
collName: string,
options: CreateEncryptedCollectionOptions,
libmongocrypt: MongoCryptClientEncryption
addaleax marked this conversation as resolved.
Show resolved Hide resolved
): Promise<{ collection: Collection, encryptedFields: Document }>
}
2 changes: 2 additions & 0 deletions packages/service-provider-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export * from './all-fle-types';

export { MapReduceOptions, FinalizeFunction } from './map-reduce-options';

export { CreateEncryptedCollectionOptions } from './admin';

const bson = {
ObjectId,
DBRef,
Expand Down
14 changes: 7 additions & 7 deletions packages/service-provider-server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/service-provider-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"saslprep": "mongodb-js/saslprep#v1.0.4"
},
"optionalDependencies": {
"mongodb-client-encryption": "^2.4.0",
"mongodb-client-encryption": "^2.5.0",
"kerberos": "^2.0.0"
}
}
42 changes: 42 additions & 0 deletions packages/service-provider-server/src/cli-service-provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import sinon, { StubbedInstance, stubInterface } from 'ts-sinon';
import CliServiceProvider from './cli-service-provider';
import ConnectionString from 'mongodb-connection-string-url';
import { EventEmitter } from 'events';
import type { ClientEncryption, ClientEncryptionDataKeyProvider } from '@mongosh/service-provider-core';

chai.use(sinonChai);

Expand Down Expand Up @@ -600,6 +601,47 @@ describe('CliServiceProvider', () => {
});
});

describe('#createEncryptedCollection', () => {
let dbStub: StubbedInstance<Db>;
let clientStub: StubbedInstance<MongoClient>;
let libmongoc: StubbedInstance<ClientEncryption>;
const createCollOptions = {
provider: 'local' as ClientEncryptionDataKeyProvider,
createCollectionOptions: {
encryptedFields: {
fields: [{
path: 'ssn',
bsonType: 'string'
}]
}
}
};

beforeEach(() => {
dbStub = stubInterface<Db>();
clientStub = stubInterface<MongoClient>();
clientStub.db.returns(dbStub);
serviceProvider = new CliServiceProvider(clientStub, bus);
libmongoc = stubInterface<ClientEncryption>();
});

it('calls calls libmongocrypt.createEncryptedCollection', async() => {
await serviceProvider.createEncryptedCollection('db1', 'coll1', createCollOptions, libmongoc);
expect(libmongoc.createEncryptedCollection).calledOnceWithExactly(
dbStub,
'coll1',
createCollOptions
);
});

it('returns whatever libmongocrypt.createEncryptedCollection returns', async() => {
const resolvedValue = { collection: { name: 'secretCol' }, encryptedFields: [] } as any;
libmongoc.createEncryptedCollection.resolves(resolvedValue);
const returnValue = await serviceProvider.createEncryptedCollection('db1', 'coll1', createCollOptions, libmongoc);
expect(returnValue).to.deep.equal(resolvedValue);
});
});

describe('sessions', () => {
let clientStub: StubbedInstance<MongoClient>;
let serviceProvider: CliServiceProvider;
Expand Down
17 changes: 16 additions & 1 deletion packages/service-provider-server/src/cli-service-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ import {
ChangeStream,
bson as BSON,
FLE,
AutoEncryptionOptions
AutoEncryptionOptions,
ClientEncryption as MongoCryptClientEncryption
} from '@mongosh/service-provider-core';

import { connectMongoClient, DevtoolsConnectOptions } from '@mongodb-js/devtools-connect';
Expand All @@ -84,6 +85,7 @@ import type { MongoshBus } from '@mongosh/types';
import { forceCloseMongoClient } from './mongodb-patches';
import ConnectionString from 'mongodb-connection-string-url';
import { EventEmitter } from 'events';
import { CreateEncryptedCollectionOptions } from '@mongosh/service-provider-core';

const bsonlib = {
Binary,
Expand Down Expand Up @@ -1038,6 +1040,19 @@ class CliServiceProvider extends ServiceProviderCore implements ServiceProvider
return { ok: 1 };
}

async createEncryptedCollection(
dbName: string,
collName: string,
options: CreateEncryptedCollectionOptions,
libmongocrypt: MongoCryptClientEncryption
): Promise<{ collection: Collection, encryptedFields: Document }> {
return await libmongocrypt.createEncryptedCollection(
this.db(dbName),
collName,
options
);
}

// eslint-disable-next-line @typescript-eslint/require-await
async initializeBulkOp(
dbName: string,
Expand Down
33 changes: 33 additions & 0 deletions packages/shell-api/src/database.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import {
bson,
ClientSession as ServiceProviderSession,
Document,
ClientEncryptionDataKeyProvider,
} from '@mongosh/service-provider-core';
import ShellInstanceState from './shell-instance-state';
import crypto from 'crypto';
import { ADMIN_DB } from './enums';
import ChangeStreamCursor from './change-stream-cursor';
import { CommonErrors, MongoshDeprecatedError, MongoshInvalidInputError, MongoshRuntimeError, MongoshUnimplementedError } from '@mongosh/errors';
import { ClientEncryption } from './field-level-encryption';
chai.use(sinonChai);

describe('Database', () => {
Expand Down Expand Up @@ -1157,6 +1159,36 @@ describe('Database', () => {
expect(caughtError).to.equal(expectedError);
});
});
describe('createEncryptedCollection', () => {
let clientEncryption: StubbedInstance<ClientEncryption>;
const createCollectionOptions = {
provider: 'local' as ClientEncryptionDataKeyProvider,
createCollectionOptions: {
encryptedFields: {
fields: []
}
}
};
beforeEach(() => {
clientEncryption = stubInterface<ClientEncryption>();
sinon.stub(database._mongo, 'getClientEncryption').returns(clientEncryption);
});
it('calls ClientEncryption.createEncryptedCollection with the provided options', async() => {
await database.createEncryptedCollection('secretCollection', createCollectionOptions);
expect(clientEncryption.createEncryptedCollection).calledOnceWithExactly(
database._name,
'secretCollection',
createCollectionOptions
);
});

it('returns whatever ClientEncryption.createEncryptedCollection returns', async() => {
const resolvedValue = { collection: { name: 'secretCol' }, encryptedFields: [] } as any;
clientEncryption.createEncryptedCollection.resolves(resolvedValue);
const returnValue = await database.createEncryptedCollection('secretCollection', createCollectionOptions);
expect(returnValue).to.deep.equal(resolvedValue);
});
});
describe('createView', () => {
it('calls serviceProvider.createCollection on the database without options', async() => {
await database.createView('newcoll', 'sourcecoll', [{ $match: { x: 1 } }]);
Expand Down Expand Up @@ -2763,6 +2795,7 @@ describe('Database', () => {
'copyDatabase',
'getReplicationInfo',
'setSecondaryOk',
'createEncryptedCollection',
'sql'
];
const args = [ {}, {}, {} ];
Expand Down
12 changes: 12 additions & 0 deletions packages/shell-api/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { HIDDEN_COMMANDS } from '@mongosh/history';
import Session from './session';
import ChangeStreamCursor from './change-stream-cursor';
import { ShellApiErrors } from './error-codes';
import { CreateEncryptedCollectionOptions } from '@mongosh/service-provider-core';

export type CollectionNamesWithTypes = {
name: string;
Expand Down Expand Up @@ -617,6 +618,17 @@ export default class Database extends ShellApiWithMongoClass {
}
}

@returnsPromise
@apiVersions([1])
async createEncryptedCollection(name: string, options: CreateEncryptedCollectionOptions): Promise<{ collection: Collection, encryptedFields: Document }> {
this._emitDatabaseApiCall('createEncryptedCollection', { name: name, options: options });
return this._mongo.getClientEncryption().createEncryptedCollection(
this._name,
name,
options
);
}

async _improveErrorMessageForLowServerVersionForQE(err: Error): Promise<void> {
try {
const serverVersion = await this.version();
Expand Down
Loading