From aa069f1b351d531ae7377fa53fd0ab98b97cddee Mon Sep 17 00:00:00 2001 From: Daria Pardue Date: Wed, 16 Feb 2022 16:54:57 -0500 Subject: [PATCH] chore(NODE-3719): spec compliance review wrap up (#3145) --- .evergreen/config.yml | 10 +- .evergreen/config.yml.in | 4 +- .evergreen/generate_evergreen_tasks.js | 31 +- .evergreen/run-custom-csfle-tests.sh | 3 +- .evergreen/run-ocsp-tests.sh | 7 +- .evergreen/run-tests.sh | 15 +- ...ient_side_encryption.prose.corpus.test.js} | 2 +- .../client_side_encryption.prose.test.js | 1669 +++++++++-------- .../client_side_encryption.spec.test.js | 34 +- .../command_monitoring.spec.test.js | 2 +- test/manual/mocharc.json | 1 + .../filters/client_encryption_filter.js | 15 +- test/tools/spec-runner/index.js | 21 +- 13 files changed, 967 insertions(+), 847 deletions(-) rename test/integration/client-side-encryption/{client_side_encryption.corpus.spec.test.js => client_side_encryption.prose.corpus.test.js} (99%) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 967c691a5f..4107cc9bde 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -143,7 +143,7 @@ functions: fi MONGODB_URI="${MONGODB_URI}" \ - AUTH=${AUTH} SSL=${SSL} UNIFIED=${UNIFIED} \ + AUTH=${AUTH} SSL=${SSL} TEST_CSFLE=true \ MONGODB_API_VERSION="${MONGODB_API_VERSION}" \ NODE_VERSION=${NODE_VERSION} SKIP_DEPS=${SKIP_DEPS|1} NO_EXIT=${NO_EXIT|1} \ bash ${PROJECT_DIRECTORY}/.evergreen/run-tests.sh @@ -204,7 +204,6 @@ functions: MONGODB_URI="${MONGODB_URI}" \ AUTH=${AUTH} \ SSL=${SSL} \ - UNIFIED=${UNIFIED} \ MONGODB_API_VERSION="${MONGODB_API_VERSION}" \ NODE_VERSION=${NODE_VERSION} \ SINGLE_MONGOS_LB_URI="${SINGLE_MONGOS_LB_URI}" \ @@ -595,7 +594,6 @@ functions: script: | ${PREPARE_SHELL} - UNIFIED=${UNIFIED} \ CA_FILE="$DRIVERS_TOOLS/.evergreen/ocsp/rsa/ca.pem" \ OCSP_TLS_SHOULD_SUCCEED="${OCSP_TLS_SHOULD_SUCCEED}" \ bash ${PROJECT_DIRECTORY}/.evergreen/run-ocsp-tests.sh @@ -2353,9 +2351,11 @@ buildvariants: NODE_LTS_NAME: erbium tasks: - serverless_task_group - - name: no-auth-tests + - name: ubuntu1804-no-auth-tests display_name: No Auth Tests - run_on: ubuntu1804-test + run_on: ubuntu1804-large + expansions: + CLIENT_ENCRYPTION: true tasks: - test-latest-server-noauth - test-latest-replica_set-noauth diff --git a/.evergreen/config.yml.in b/.evergreen/config.yml.in index 184bac4402..95a79be9c2 100644 --- a/.evergreen/config.yml.in +++ b/.evergreen/config.yml.in @@ -163,7 +163,7 @@ functions: fi MONGODB_URI="${MONGODB_URI}" \ - AUTH=${AUTH} SSL=${SSL} UNIFIED=${UNIFIED} \ + AUTH=${AUTH} SSL=${SSL} TEST_CSFLE=true \ MONGODB_API_VERSION="${MONGODB_API_VERSION}" \ NODE_VERSION=${NODE_VERSION} SKIP_DEPS=${SKIP_DEPS|1} NO_EXIT=${NO_EXIT|1} \ bash ${PROJECT_DIRECTORY}/.evergreen/run-tests.sh @@ -228,7 +228,6 @@ functions: MONGODB_URI="${MONGODB_URI}" \ AUTH=${AUTH} \ SSL=${SSL} \ - UNIFIED=${UNIFIED} \ MONGODB_API_VERSION="${MONGODB_API_VERSION}" \ NODE_VERSION=${NODE_VERSION} \ SINGLE_MONGOS_LB_URI="${SINGLE_MONGOS_LB_URI}" \ @@ -629,7 +628,6 @@ functions: script: | ${PREPARE_SHELL} - UNIFIED=${UNIFIED} \ CA_FILE="$DRIVERS_TOOLS/.evergreen/ocsp/rsa/ca.pem" \ OCSP_TLS_SHOULD_SUCCEED="${OCSP_TLS_SHOULD_SUCCEED}" \ bash ${PROJECT_DIRECTORY}/.evergreen/run-ocsp-tests.sh diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index f2e244429a..ba736ea029 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -72,7 +72,7 @@ function generateVersionTopologyMatrix() { function* _generate() { for (const mongoVersion of MONGODB_VERSIONS) { for (const topology of TOPOLOGIES) { - yield { mongoVersion, topology} + yield { mongoVersion, topology }; } } } @@ -80,8 +80,10 @@ function generateVersionTopologyMatrix() { return Array.from(_generate()); } -const BASE_TASKS = generateVersionTopologyMatrix().map(makeTask) -const AUTH_DISABLED_TASKS = generateVersionTopologyMatrix().map((test) => makeTask({ ...test, auth: 'noauth', tags: ['noauth'] })) +const BASE_TASKS = generateVersionTopologyMatrix().map(makeTask); +const AUTH_DISABLED_TASKS = generateVersionTopologyMatrix().map(test => + makeTask({ ...test, auth: 'noauth', tags: ['noauth'] }) +); BASE_TASKS.push({ name: `test-latest-server-v1-api`, @@ -323,7 +325,7 @@ TLS_VERSIONS.forEach(VERSION => { vars: { VERSION, SSL: 'ssl', - TOPOLOGY: 'server', + TOPOLOGY: 'server' // TODO: NODE-3891 - fix tests broken when AUTH enabled // AUTH: 'auth' } @@ -702,10 +704,8 @@ const coverageTask = { func: 'download and merge coverage' } ], - depends_on: [ - { name: '*', variant: '*', status: '*', patch_optional: true } - ] -} + depends_on: [{ name: '*', variant: '*', status: '*', patch_optional: true }] +}; SINGLETON_TASKS.push(...oneOffFuncAsTasks); @@ -728,14 +728,21 @@ BUILD_VARIANTS.push({ }); BUILD_VARIANTS.push({ - name: 'no-auth-tests', + name: 'ubuntu1804-no-auth-tests', display_name: 'No Auth Tests', - run_on: 'ubuntu1804-test', + run_on: DEFAULT_OS, + expansions: { + CLIENT_ENCRYPTION: true + }, tasks: AUTH_DISABLED_TASKS.map(({ name }) => name) -}) +}); const fileData = yaml.load(fs.readFileSync(`${__dirname}/config.yml.in`, 'utf8')); -fileData.tasks = (fileData.tasks || []).concat(BASE_TASKS).concat(TASKS).concat(SINGLETON_TASKS).concat(AUTH_DISABLED_TASKS); +fileData.tasks = (fileData.tasks || []) + .concat(BASE_TASKS) + .concat(TASKS) + .concat(SINGLETON_TASKS) + .concat(AUTH_DISABLED_TASKS); fileData.buildvariants = (fileData.buildvariants || []).concat(BUILD_VARIANTS); fs.writeFileSync(`${__dirname}/config.yml`, yaml.dump(fileData, { lineWidth: 120 }), 'utf8'); diff --git a/.evergreen/run-custom-csfle-tests.sh b/.evergreen/run-custom-csfle-tests.sh index f15c1b2f94..c71aab7f2b 100644 --- a/.evergreen/run-custom-csfle-tests.sh +++ b/.evergreen/run-custom-csfle-tests.sh @@ -19,7 +19,7 @@ set -o errexit # Exit the script with error if any of the commands fail # Get access to the AWS temporary credentials: echo "adding temporary AWS credentials to environment" # CSFLE_AWS_TEMP_ACCESS_KEY_ID, CSFLE_AWS_TEMP_SECRET_ACCESS_KEY, CSFLE_AWS_TEMP_SESSION_TOKEN -. $DRIVERS_TOOLS/.evergreen/csfle/set-temp-creds.sh +. "$DRIVERS_TOOLS"/.evergreen/csfle/set-temp-creds.sh ABS_PATH_TO_PATCH=$(pwd) @@ -65,6 +65,7 @@ cp -R ../csfle-deps-tmp/libmongocrypt/bindings/node node_modules/mongodb-client- export MONGODB_URI=${MONGODB_URI} export KMIP_TLS_CA_FILE="${DRIVERS_TOOLS}/.evergreen/x509gen/ca.pem" export KMIP_TLS_CERT_FILE="${DRIVERS_TOOLS}/.evergreen/x509gen/client.pem" +export TEST_CSFLE=true set +o errexit # We want to run both test suites even if the first fails npm run check:csfle DRIVER_CSFLE_TEST_RESULT=$? diff --git a/.evergreen/run-ocsp-tests.sh b/.evergreen/run-ocsp-tests.sh index 4c61215c74..4f96b1e86b 100644 --- a/.evergreen/run-ocsp-tests.sh +++ b/.evergreen/run-ocsp-tests.sh @@ -2,8 +2,6 @@ set -o xtrace set -o errexit -UNIFIED=${UNIFIED:-} - # load node.js environment source "${PROJECT_DIRECTORY}/.evergreen/init-nvm.sh" @@ -14,7 +12,6 @@ source "${PROJECT_DIRECTORY}/.evergreen/init-nvm.sh" # PYTHON=python # NOTE: `--opts {}` is used below to revert mocha to normal behavior (without mongodb specific plugins) -MONGODB_UNIFIED_TOPOLOGY=${UNIFIED} \ -OCSP_TLS_SHOULD_SUCCEED=${OCSP_TLS_SHOULD_SUCCEED} \ -CA_FILE=${CA_FILE} \ +export OCSP_TLS_SHOULD_SUCCEED=${OCSP_TLS_SHOULD_SUCCEED} +export CA_FILE=${CA_FILE} npm run check:ocsp diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 138aa7e41a..a6a349e4f3 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -5,15 +5,14 @@ set -o errexit # Exit the script with error if any of the commands fail # Supported/used environment variables: # AUTH Set to enable authentication. Defaults to "noauth" # SSL Set to enable SSL. Defaults to "nossl" -# UNIFIED Set to enable the Unified SDAM topology for the node driver # MONGODB_URI Set the suggested connection MONGODB_URI (including credentials and topology info) # MARCH Machine Architecture. Defaults to lowercase uname -m # TEST_NPM_SCRIPT Script to npm run. Defaults to "integration-coverage" # SKIP_DEPS Skip installing dependencies # NO_EXIT Don't exit early from tests that leak resources +# TEST_CSFLE Set to enforce running csfle tests AUTH=${AUTH:-noauth} -UNIFIED=${UNIFIED:-} MONGODB_URI=${MONGODB_URI:-} TEST_NPM_SCRIPT=${TEST_NPM_SCRIPT:-check:integration-coverage} if [[ -z "${NO_EXIT}" ]]; then @@ -45,9 +44,17 @@ else # Get access to the AWS temporary credentials: echo "adding temporary AWS credentials to environment" # CSFLE_AWS_TEMP_ACCESS_KEY_ID, CSFLE_AWS_TEMP_SECRET_ACCESS_KEY, CSFLE_AWS_TEMP_SESSION_TOKEN - . $DRIVERS_TOOLS/.evergreen/csfle/set-temp-creds.sh + source "$DRIVERS_TOOLS"/.evergreen/csfle/set-temp-creds.sh fi npm install mongodb-client-encryption@">=2.0.0-beta.4" -AUTH=$AUTH SINGLE_MONGOS_LB_URI=${SINGLE_MONGOS_LB_URI} MULTI_MONGOS_LB_URI=${MULTI_MONGOS_LB_URI} MONGODB_API_VERSION=${MONGODB_API_VERSION} MONGODB_UNIFIED_TOPOLOGY=${UNIFIED} MONGODB_URI=${MONGODB_URI} LOAD_BALANCER=${LOAD_BALANCER} npm run ${TEST_NPM_SCRIPT} +export AUTH=$AUTH +export SINGLE_MONGOS_LB_URI=${SINGLE_MONGOS_LB_URI} +export MULTI_MONGOS_LB_URI=${MULTI_MONGOS_LB_URI} +export MONGODB_API_VERSION=${MONGODB_API_VERSION} +export MONGODB_URI=${MONGODB_URI} +export LOAD_BALANCER=${LOAD_BALANCER} +export TEST_CSFLE=${TEST_CSFLE} +# Do not add quotes, due to the way NO_EXIT is handled +npm run ${TEST_NPM_SCRIPT} diff --git a/test/integration/client-side-encryption/client_side_encryption.corpus.spec.test.js b/test/integration/client-side-encryption/client_side_encryption.prose.corpus.test.js similarity index 99% rename from test/integration/client-side-encryption/client_side_encryption.corpus.spec.test.js rename to test/integration/client-side-encryption/client_side_encryption.prose.corpus.test.js index c01f3d7143..9bc9ef7e47 100644 --- a/test/integration/client-side-encryption/client_side_encryption.corpus.spec.test.js +++ b/test/integration/client-side-encryption/client_side_encryption.prose.corpus.test.js @@ -8,7 +8,7 @@ const BSON = require('bson'); const { EJSON } = require('bson'); const { expect } = require('chai'); -describe('Client Side Encryption Corpus', function () { +describe('Client Side Encryption Prose Corpus Test', function () { const metadata = { requires: { mongodb: '>=4.2.0', diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.test.js b/test/integration/client-side-encryption/client_side_encryption.prose.test.js index 6dab38aa89..d33ee1c2b0 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.test.js +++ b/test/integration/client-side-encryption/client_side_encryption.prose.test.js @@ -1,6 +1,8 @@ 'use strict'; const BSON = require('bson'); const chai = require('chai'); +const fs = require('fs'); +const path = require('path'); const { deadlockTests } = require('./client_side_encryption.prose.deadlock'); const expect = chai.expect; @@ -60,275 +62,6 @@ describe('Client Side Encryption Prose Tests', metadata, function () { 'base64' ); - /** - * - Create client encryption no tls - * - Create client encryption with tls - * - Create client encryption expired - * - Create client encryption invalid hostname - */ - context('KMS TLS Options Tests', metadata, function () { - let tlsCaOptions; - let clientNoTlsOptions; - let clientWithTlsOptions; - let clientWithTlsExpiredOptions; - let clientWithInvalidHostnameOptions; - let clientNoTls; - let clientWithTls; - let clientWithTlsExpired; - let clientWithInvalidHostname; - let clientEncryptionNoTls; - let clientEncryptionWithTls; - let clientEncryptionWithTlsExpired; - let clientEncryptionWithInvalidHostname; - - before(function () { - tlsCaOptions = { - aws: { - tlsCAFile: process.env.KMIP_TLS_CA_FILE - }, - azure: { - tlsCAFile: process.env.KMIP_TLS_CA_FILE - }, - gcp: { - tlsCAFile: process.env.KMIP_TLS_CA_FILE - }, - kmip: { - tlsCAFile: process.env.KMIP_TLS_CA_FILE - } - }; - clientNoTlsOptions = { - keyVaultNamespace, - kmsProviders: getKmsProviders(null, null, '127.0.0.1:8002', '127.0.0.1:8002'), - tlsOptions: tlsCaOptions - }; - clientWithTlsOptions = { - keyVaultNamespace, - kmsProviders: getKmsProviders(null, null, '127.0.0.1:8002', '127.0.0.1:8002'), - tlsOptions: { - aws: { - tlsCAFile: process.env.KMIP_TLS_CA_FILE, - tlsCertificateKeyFile: process.env.KMIP_TLS_CERT_FILE - }, - azure: { - tlsCAFile: process.env.KMIP_TLS_CA_FILE, - tlsCertificateKeyFile: process.env.KMIP_TLS_CERT_FILE - }, - gcp: { - tlsCAFile: process.env.KMIP_TLS_CA_FILE, - tlsCertificateKeyFile: process.env.KMIP_TLS_CERT_FILE - }, - kmip: { - tlsCAFile: process.env.KMIP_TLS_CA_FILE, - tlsCertificateKeyFile: process.env.KMIP_TLS_CERT_FILE - } - } - }; - clientWithTlsExpiredOptions = { - keyVaultNamespace, - kmsProviders: getKmsProviders(null, '127.0.0.1:8000', '127.0.0.1:8000', '127.0.0.1:8000'), - tlsOptions: tlsCaOptions - }; - clientWithInvalidHostnameOptions = { - keyVaultNamespace, - kmsProviders: getKmsProviders(null, '127.0.0.1:8001', '127.0.0.1:8001', '127.0.0.1:8001'), - tlsOptions: tlsCaOptions - }; - clientNoTls = this.configuration.newClient({}, { autoEncryption: clientNoTlsOptions }); - clientWithTls = this.configuration.newClient({}, { autoEncryption: clientWithTlsOptions }); - clientWithTlsExpired = this.configuration.newClient( - {}, - { autoEncryption: clientWithTlsExpiredOptions } - ); - clientWithInvalidHostname = this.configuration.newClient( - {}, - { autoEncryption: clientWithInvalidHostnameOptions } - ); - const mongodbClientEncryption = this.configuration.mongodbClientEncryption; - clientEncryptionNoTls = new mongodbClientEncryption.ClientEncryption(clientNoTls, { - ...clientNoTlsOptions, - bson: BSON - }); - clientEncryptionWithTls = new mongodbClientEncryption.ClientEncryption(clientWithTls, { - ...clientWithTlsOptions, - bson: BSON - }); - clientEncryptionWithTlsExpired = new mongodbClientEncryption.ClientEncryption( - clientWithTlsExpired, - { ...clientWithTlsExpiredOptions, bson: BSON } - ); - clientEncryptionWithInvalidHostname = new mongodbClientEncryption.ClientEncryption( - clientWithInvalidHostname, - { ...clientWithInvalidHostnameOptions, bson: BSON } - ); - }); - - // Case 1. - context('Case 1: AWS', metadata, function () { - const masterKey = { - region: 'us-east-1', - key: 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0', - endpoint: '127.0.0.1:8002' - }; - const masterKeyExpired = { ...masterKey, endpoint: '127.0.0.1:8000' }; - const masterKeyInvalidHostname = { ...masterKey, endpoint: '127.0.0.1:8001' }; - - it('fails with various invalid tls options', metadata, async function () { - try { - await clientNoTls.connect(); - await clientEncryptionNoTls.createDataKey('aws', { masterKey }); - expect.fail('it must fail with no tls'); - } catch (e) { - expect(e.originalError.message).to.include('certificate required'); - await clientNoTls.close(); - } - try { - await clientWithTls.connect(); - await clientEncryptionWithTls.createDataKey('aws', { masterKey }); - expect.fail('it must fail to parse response'); - } catch (e) { - await clientWithTls.close(); - expect(e.message).to.include('parse error'); - } - try { - await clientWithTlsExpired.connect(); - await clientEncryptionWithTlsExpired.createDataKey('aws', { masterKeyExpired }); - expect.fail('it must fail with invalid certificate'); - } catch (e) { - await clientWithTlsExpired.close(); - expect(e.message).to.include('expected UTF-8 key'); - } - try { - await clientWithInvalidHostname.connect(); - await clientEncryptionWithInvalidHostname.createDataKey('aws', { - masterKeyInvalidHostname - }); - expect.fail('it must fail with invalid hostnames'); - } catch (e) { - await clientWithInvalidHostname.close(); - expect(e.message).to.include('expected UTF-8 key'); - } - }); - }); - - // Case 2. - context('Case 2: Azure', metadata, function () { - const masterKey = { - keyVaultEndpoint: 'doesnotexist.local', - keyName: 'foo' - }; - - it('fails with various invalid tls options', metadata, async function () { - try { - await clientNoTls.connect(); - await clientEncryptionNoTls.createDataKey('azure', { masterKey }); - expect.fail('it must fail with no tls'); - } catch (e) { - await clientNoTls.close(); - expect(e.originalError.message).to.include('certificate required'); - } - try { - await clientWithTls.connect(); - await clientEncryptionWithTls.createDataKey('azure', { masterKey }); - expect.fail('it must fail with invalid host'); - } catch (e) { - await clientWithTls.close(); - expect(e.message).to.include('HTTP status=404'); - } - try { - await clientWithTlsExpired.connect(); - await clientEncryptionWithTlsExpired.createDataKey('azure', { masterKey }); - expect.fail('it must fail with expired certificates'); - } catch (e) { - await clientWithTlsExpired.close(); - expect(e.originalError.message).to.include('certificate has expired'); - } - try { - await clientWithInvalidHostname.connect(); - await clientEncryptionWithInvalidHostname.createDataKey('azure', { masterKey }); - expect.fail('it must fail with invalid hostnames'); - } catch (e) { - await clientWithInvalidHostname.close(); - expect(e.originalError.message).to.include('does not match certificate'); - } - }); - }); - - // Case 3. - context('Case 3: GCP', metadata, function () { - const masterKey = { - projectId: 'foo', - location: 'bar', - keyRing: 'baz', - keyName: 'foo' - }; - - it('fails with various invalid tls options', metadata, async function () { - try { - await clientNoTls.connect(); - await clientEncryptionNoTls.createDataKey('gcp', { masterKey }); - expect.fail('it must fail with no tls'); - } catch (e) { - await clientNoTls.close(); - expect(e.originalError.message).to.include('certificate required'); - } - try { - await clientWithTls.connect(); - await clientEncryptionWithTls.createDataKey('gcp', { masterKey }); - expect.fail('it must fail with invalid host'); - } catch (e) { - await clientWithTls.close(); - expect(e.message).to.include('HTTP status=404'); - } - try { - await clientWithTlsExpired.connect(); - await clientEncryptionWithTlsExpired.createDataKey('gcp', { masterKey }); - expect.fail('it must fail with expired certificates'); - } catch (e) { - await clientWithTlsExpired.close(); - expect(e.originalError.message).to.include('certificate has expired'); - } - try { - await clientWithInvalidHostname.connect(); - await clientEncryptionWithInvalidHostname.createDataKey('gcp', { masterKey }); - expect.fail('it must fail with invalid hostnames'); - } catch (e) { - await clientWithInvalidHostname.close(); - expect(e.originalError.message).to.include('does not match certificate'); - } - }); - }); - - // Case 4. - context('Case 4: KMIP', metadata, function () { - it('fails with various invalid tls options', metadata, async function () { - try { - await clientNoTls.connect(); - await clientEncryptionNoTls.createDataKey('kmip'); - expect.fail('it must fail with no tls'); - } catch (e) { - await clientNoTls.close(); - expect(e.originalError.message).to.include('before secure TLS connection'); - } - try { - await clientWithTlsExpired.connect(); - await clientEncryptionWithTlsExpired.createDataKey('kmip'); - expect.fail('it must fail with expired certificates'); - } catch (e) { - await clientWithTlsExpired.close(); - expect(e.originalError.message).to.include('certificate has expired'); - } - try { - await clientWithInvalidHostname.connect(); - await clientEncryptionWithInvalidHostname.createDataKey('kmip'); - expect.fail('it must fail with invalid hostnames'); - } catch (e) { - await clientWithInvalidHostname.close(); - expect(e.originalError.message).to.include('does not match certificate'); - } - }); - }); - }); - describe('Data key and double encryption', function () { // Data key and double encryption // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -336,7 +69,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { beforeEach(function () { const mongodbClientEncryption = this.configuration.mongodbClientEncryption; - // #. Create a MongoClient without encryption enabled (referred to as ``client``). Enable command monitoring to listen for command_started events. + // 1. Create a MongoClient without encryption enabled (referred to as ``client``). Enable command monitoring to listen for command_started events. this.client = this.configuration.newClient({}, { monitorCommands: true }); this.commandStartedEvents = new APMEventCollector(this.client, 'commandStarted', { @@ -361,10 +94,10 @@ describe('Client Side Encryption Prose Tests', metadata, function () { return ( Promise.resolve() .then(() => this.client.connect()) - // #. Using ``client``, drop the collections ``keyvault.datakeys`` and ``db.coll``. + // 2. Using ``client``, drop the collections ``keyvault.datakeys`` and ``db.coll``. .then(() => dropCollection(this.client.db(dataDbName), dataCollName)) .then(() => dropCollection(this.client.db(keyVaultDbName), keyVaultCollName)) - // #. Create the following: + // 3. Create the following: // - A MongoClient configured with auto encryption (referred to as ``client_encrypted``) // - A ``ClientEncryption`` object (referred to as ``client_encryption``) // Configure both objects with ``aws`` and the ``local`` KMS providers as follows: @@ -599,27 +332,517 @@ describe('Client Side Encryption Prose Tests', metadata, function () { }); }); - describe('Custom Endpoint', function () { - // Data keys created with AWS KMS may specify a custom endpoint to contact (instead of the default endpoint derived from the AWS region). + // TODO(NODE-4000): We cannot implement these tests according to spec b/c the tests require a + // connect-less client. So instead we are implementing the tests via APM, + // and confirming that the externalClient is firing off keyVault requests during + // encrypted operations + describe('External Key Vault Test', function () { + const fs = require('fs'); + const path = require('path'); + const { EJSON } = BSON; + function loadExternal(file) { + return EJSON.parse( + fs.readFileSync(path.resolve(__dirname, '../../spec/client-side-encryption/external', file)) + ); + } + + const externalKey = loadExternal('external-key.json'); + const externalSchema = loadExternal('external-schema.json'); beforeEach(function () { - // 1. Create a ``ClientEncryption`` object (referred to as ``client_encryption``) - // Configure with ``aws`` KMS providers as follows: - // .. code:: javascript - // { - // "aws": { } - // } - // Configure with ``keyVaultNamespace`` set to ``keyvault.datakeys``, and a default MongoClient as the ``keyVaultClient``. this.client = this.configuration.newClient(); - const customKmsProviders = getKmsProviders(); - customKmsProviders.azure.identityPlatformEndpoint = 'login.microsoftonline.com:443'; - customKmsProviders.gcp.endpoint = 'oauth2.googleapis.com:443'; + // 1. Create a MongoClient without encryption enabled (referred to as ``client``). + return ( + this.client + .connect() + // 2. Using ``client``, drop the collections ``keyvault.datakeys`` and ``db.coll``. + // Insert the document `external/external-key.json <../external/external-key.json>`_ into ``keyvault.datakeys``. + .then(() => dropCollection(this.client.db(dataDbName), dataCollName)) + .then(() => dropCollection(this.client.db(keyVaultDbName), keyVaultCollName)) + .then(() => { + return this.client + .db(keyVaultDbName) + .collection(keyVaultCollName) + .insertOne(externalKey, { writeConcern: { w: 'majority' } }); + }) + ); + }); - const invalidKmsProviders = getKmsProviders(); - invalidKmsProviders.azure.identityPlatformEndpoint = 'doesnotexist.invalid:443'; - invalidKmsProviders.gcp.endpoint = 'doesnotexist.invalid:443'; - invalidKmsProviders.kmip.endpoint = 'doesnotexist.local:5698'; + afterEach(function () { + if (this.commandStartedEvents) { + this.commandStartedEvents.teardown(); + this.commandStartedEvents = undefined; + } + return Promise.resolve() + .then(() => this.externalClient && this.externalClient.close()) + .then(() => this.clientEncrypted && this.clientEncrypted.close()) + .then(() => this.client && this.client.close()); + }); + + function defineTest(withExternalKeyVault) { + it( + `should work ${withExternalKeyVault ? 'with' : 'without'} external key vault`, + metadata, + function () { + const ClientEncryption = this.configuration.mongodbClientEncryption.ClientEncryption; + return ( + Promise.resolve() + .then(() => { + // If ``withExternalKeyVault == true``, configure both objects with an external key vault client. The external client MUST connect to the same + // MongoDB cluster that is being tested against, except it MUST use the username ``fake-user`` and password ``fake-pwd``. + this.externalClient = this.configuration.newClient( + // this.configuration.url('fake-user', 'fake-pwd'), + // TODO: Do this properly + {}, + { monitorCommands: true } + ); + + this.commandStartedEvents = new APMEventCollector( + this.externalClient, + 'commandStarted', + { + include: ['find'] + } + ); + return this.externalClient.connect(); + }) + // 3. Create the following: + // - A MongoClient configured with auto encryption (referred to as ``client_encrypted``) + // - A ``ClientEncryption`` object (referred to as ``client_encryption``) + // Configure both objects with the ``local`` KMS providers as follows: + // .. code:: javascript + // { "local": { "key": } } + // Configure both objects with ``keyVaultNamespace`` set to ``keyvault.datakeys``. + // Configure ``client_encrypted`` to use the schema `external/external-schema.json <../external/external-schema.json>`_ for ``db.coll`` by setting a schema map like: ``{ "db.coll": }`` + .then(() => { + const options = { + bson: BSON, + keyVaultNamespace, + kmsProviders: getKmsProviders(LOCAL_KEY) + }; + + if (withExternalKeyVault) { + options.keyVaultClient = this.externalClient; + } + + this.clientEncryption = new ClientEncryption( + this.client, + Object.assign({}, options) + ); + this.clientEncrypted = this.configuration.newClient( + {}, + { + autoEncryption: Object.assign({}, options, { + schemaMap: { + 'db.coll': externalSchema + } + }) + } + ); + return this.clientEncrypted.connect(); + }) + .then(() => { + // 4. Use ``client_encrypted`` to insert the document ``{"encrypted": "test"}`` into ``db.coll``. + // If ``withExternalKeyVault == true``, expect an authentication exception to be thrown. Otherwise, expect the insert to succeed. + this.commandStartedEvents.clear(); + return this.clientEncrypted + .db(dataDbName) + .collection(dataCollName) + .insertOne({ encrypted: 'test' }) + .then(() => { + if (withExternalKeyVault) { + expect(this.commandStartedEvents.events).to.containSubset([ + { + commandName: 'find', + databaseName: keyVaultDbName, + command: { find: keyVaultCollName } + } + ]); + } else { + expect(this.commandStartedEvents.events).to.not.containSubset([ + { + commandName: 'find', + databaseName: keyVaultDbName, + command: { find: keyVaultCollName } + } + ]); + } + }); + // TODO: Do this in the spec-compliant way using bad auth credentials + // .then( + // () => { + // if (withExternalKeyVault) { + // throw new Error( + // 'expected insert to fail with authentication error, but it passed' + // ); + // } + // }, + // err => { + // if (!withExternalKeyVault) { + // throw err; + // } + // expect(err).to.be.an.instanceOf(Error); + // } + // ); + }) + .then(() => { + // 5. Use ``client_encryption`` to explicitly encrypt the string ``"test"`` with key ID ``LOCALAAAAAAAAAAAAAAAAA==`` and deterministic algorithm. + // If ``withExternalKeyVault == true``, expect an authentication exception to be thrown. Otherwise, expect the insert to succeed. + this.commandStartedEvents.clear(); + return this.clientEncryption + .encrypt('test', { + keyId: externalKey._id, + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + }) + .then(() => { + if (withExternalKeyVault) { + expect(this.commandStartedEvents.events).to.containSubset([ + { + commandName: 'find', + databaseName: keyVaultDbName, + command: { find: keyVaultCollName } + } + ]); + } else { + expect(this.commandStartedEvents.events).to.not.containSubset([ + { + commandName: 'find', + databaseName: keyVaultDbName, + command: { find: keyVaultCollName } + } + ]); + } + }); + // TODO: Do this in the spec-compliant way using bad auth credentials + // .then( + // () => { + // if (withExternalKeyVault) { + // throw new Error( + // 'expected insert to fail with authentication error, but it passed' + // ); + // } + // }, + // err => { + // if (!withExternalKeyVault) { + // throw err; + // } + // expect(err).to.be.an.instanceOf(Error); + // } + // ); + }) + ); + } + ); + } + // Run the following tests twice, parameterized by a boolean ``withExternalKeyVault``. + defineTest(true); + defineTest(false); + }); + + describe('BSON size limits and batch splitting', function () { + const fs = require('fs'); + const path = require('path'); + const { EJSON } = BSON; + function loadLimits(file) { + return EJSON.parse( + fs.readFileSync(path.resolve(__dirname, '../../spec/client-side-encryption/limits', file)) + ); + } + + const limitsSchema = loadLimits('limits-schema.json'); + const limitsKey = loadLimits('limits-key.json'); + const limitsDoc = loadLimits('limits-doc.json'); + + let hasRunFirstTimeSetup = false; + beforeEach(async function () { + if (hasRunFirstTimeSetup) { + // Even though we have to use a beforeEach here + // We still only want the following code to be run *once* + // before all the tests that follow + return; + } + hasRunFirstTimeSetup = true; + // First, perform the setup. + + // 1. Create a MongoClient without encryption enabled (referred to as ``client``). + this.client = this.configuration.newClient(); + + await this.client + .connect() + // 2. Using ``client``, drop and create the collection ``db.coll`` configured with the included JSON schema `limits/limits-schema.json <../limits/limits-schema.json>`_. + .then(() => dropCollection(this.client.db(dataDbName), dataCollName)) + .then(() => { + return this.client.db(dataDbName).createCollection(dataCollName, { + validator: { $jsonSchema: limitsSchema } + }); + }) + // 3. Using ``client``, drop the collection ``keyvault.datakeys``. Insert the document `limits/limits-key.json <../limits/limits-key.json>`_ + .then(() => dropCollection(this.client.db(keyVaultDbName), keyVaultCollName)) + .then(() => { + return this.client + .db(keyVaultDbName) + .collection(keyVaultCollName) + .insertOne(limitsKey, { writeConcern: { w: 'majority' } }); + }); + }); + + beforeEach(function () { + // 4. Create a MongoClient configured with auto encryption (referred to as ``client_encrypted``) + // Configure with the ``local`` KMS provider as follows: + // .. code:: javascript + // { "local": { "key": } } + // Configure with the ``keyVaultNamespace`` set to ``keyvault.datakeys``. + this.clientEncrypted = this.configuration.newClient( + {}, + { + monitorCommands: true, + autoEncryption: { + keyVaultNamespace, + kmsProviders: getKmsProviders(LOCAL_KEY) + } + } + ); + return this.clientEncrypted.connect().then(() => { + this.encryptedColl = this.clientEncrypted.db(dataDbName).collection(dataCollName); + this.commandStartedEvents = new APMEventCollector(this.clientEncrypted, 'commandStarted', { + include: ['insert'] + }); + }); + }); + + afterEach(function () { + if (this.commandStartedEvents) { + this.commandStartedEvents.teardown(); + this.commandStartedEvents = undefined; + } + if (this.clientEncrypted) { + return this.clientEncrypted.close(); + } + }); + + after(function () { + return this.client && this.client.close(); + }); + + // Using ``client_encrypted`` perform the following operations: + + function repeatedChar(char, length) { + return Array.from({ length }) + .map(() => char) + .join(''); + } + + const testCases = [ + // 1. Insert ``{ "_id": "over_2mib_under_16mib", "unencrypted": }``. + // Expect this to succeed since this is still under the ``maxBsonObjectSize`` limit. + { + description: 'should succeed for over_2mib_under_16mib', + docs: () => [{ _id: 'over_2mib_under_16mib', unencrypted: repeatedChar('a', 2097152) }], + expectedEvents: [{ commandName: 'insert' }] + }, + // 2. Insert the document `limits/limits-doc.json <../limits/limits-doc.json>`_ concatenated with ``{ "_id": "encryption_exceeds_2mib", "unencrypted": < the string "a" repeated (2097152 - 2000) times > }`` + // Note: limits-doc.json is a 1005 byte BSON document that encrypts to a ~10,000 byte document. + // Expect this to succeed since after encryption this still is below the normal maximum BSON document size. + // Note, before auto encryption this document is under the 2 MiB limit. After encryption it exceeds the 2 MiB limit, but does NOT exceed the 16 MiB limit. + { + description: 'should succeed for encryption_exceeds_2mib', + docs: () => [ + Object.assign({}, limitsDoc, { + _id: 'encryption_exceeds_2mib', + unencrypted: repeatedChar('a', 2097152 - 2000) + }) + ], + expectedEvents: [{ commandName: 'insert' }] + }, + // 3. Bulk insert the following: + // - ``{ "_id": "over_2mib_1", "unencrypted": }`` + // - ``{ "_id": "over_2mib_2", "unencrypted": }`` + // Expect the bulk write to succeed and split after first doc (i.e. two inserts occur). This may be verified using `command monitoring `_. + { + description: 'should succeed for bulk over_2mib', + docs: () => [ + { _id: 'over_2mib_1', unencrypted: repeatedChar('a', 2097152) }, + { _id: 'over_2mib_2', unencrypted: repeatedChar('a', 2097152) } + ], + expectedEvents: [{ commandName: 'insert' }, { commandName: 'insert' }] + }, + // 4. Bulk insert the following: + // - The document `limits/limits-doc.json <../limits/limits-doc.json>`_ concatenated with ``{ "_id": "encryption_exceeds_2mib_1", "unencrypted": < the string "a" repeated (2097152 - 2000) times > }`` + // - The document `limits/limits-doc.json <../limits/limits-doc.json>`_ concatenated with ``{ "_id": "encryption_exceeds_2mib_2", "unencrypted": < the string "a" repeated (2097152 - 2000) times > }`` + // Expect the bulk write to succeed and split after first doc (i.e. two inserts occur). This may be verified using `command monitoring `_. + { + description: 'should succeed for bulk encryption_exceeds_2mib', + docs: () => [ + Object.assign({}, limitsDoc, { + _id: 'encryption_exceeds_2mib_1', + unencrypted: repeatedChar('a', 2097152 - 2000) + }), + Object.assign({}, limitsDoc, { + _id: 'encryption_exceeds_2mib_2', + unencrypted: repeatedChar('a', 2097152 - 2000) + }) + ], + expectedEvents: [{ commandName: 'insert' }, { commandName: 'insert' }] + }, + // 5. Insert ``{ "_id": "under_16mib", "unencrypted": ``. + // Expect this to succeed since this is still (just) under the ``maxBsonObjectSize`` limit. + { + description: 'should succeed for under_16mib', + docs: () => [{ _id: 'under_16mib', unencrypted: repeatedChar('a', 16777216 - 2000) }], + expectedEvents: [{ commandName: 'insert' }] + }, + // 6. Insert the document `limits/limits-doc.json <../limits/limits-doc.json>`_ concatenated with ``{ "_id": "encryption_exceeds_16mib", "unencrypted": < the string "a" repeated (16777216 - 2000) times > }`` + // Expect this to fail since encryption results in a document exceeding the ``maxBsonObjectSize`` limit. + { + description: 'should fail for encryption_exceeds_16mib', + docs: () => [ + Object.assign({}, limitsDoc, { + _id: 'encryption_exceeds_16mib', + unencrypted: repeatedChar('a', 16777216 - 2000) + }) + ], + error: true + } + ]; + + testCases.forEach(testCase => { + it(testCase.description, metadata, function () { + return this.encryptedColl.insertMany(testCase.docs()).then( + () => { + if (testCase.error) { + throw new Error('Expected this insert to fail, but it succeeded'); + } + const expectedEvents = Array.from(testCase.expectedEvents); + const actualEvents = pruneEvents(this.commandStartedEvents.events); + + expect(actualEvents) + .to.have.a.lengthOf(expectedEvents.length) + .and.to.containSubset(expectedEvents); + }, + err => { + if (!testCase.error) { + throw err; + } + } + ); + }); + }); + + function pruneEvents(events) { + return events.map(event => { + // We are pruning out the bunch of repeating As, mostly + // b/c an error failure will try to print 2mb of 'a's + // and not have a good time. + event.command = Object.assign({}, event.command); + event.command.documents = event.command.documents.map(doc => { + doc = Object.assign({}, doc); + if (doc.unencrypted) { + doc.unencrypted = "Lots of repeating 'a's"; + } + return doc; + }); + return event; + }); + } + }); + + describe('Views are prohibited', function () { + beforeEach(function () { + // First, perform the setup. + + // 1. Create a MongoClient without encryption enabled (referred to as ``client``). + this.client = this.configuration.newClient(); + + // 2. Using client, drop and create a view named db.view with an empty pipeline. + // E.g. using the command { "create": "view", "viewOn": "coll" }. + return this.client + .connect() + .then(() => dropCollection(this.client.db(dataDbName), dataCollName)) + .then(() => { + return this.client.db(dataDbName).createCollection(dataCollName); + }) + .then(() => { + return this.client + .db(dataDbName) + .createCollection('view', { viewOn: dataCollName, pipeline: [] }) + .then(noop, noop); + }, noop); + }); + + afterEach(function () { + return this.client && this.client.close(); + }); + + beforeEach(function () { + // 3. Create a MongoClient configured with auto encryption (referred to as client_encrypted) + // Configure with the local KMS provider + this.clientEncrypted = this.configuration.newClient( + {}, + { + autoEncryption: { + keyVaultNamespace, + kmsProviders: getKmsProviders(LOCAL_KEY) + } + } + ); + + return this.clientEncrypted.connect(); + }); + + afterEach(function () { + return this.clientEncrypted && this.clientEncrypted.close(); + }); + + // 4. Using client_encrypted, attempt to insert a document into db.view. + // Expect an exception to be thrown containing the message: "cannot auto encrypt a view". + it('should error when inserting into a view with autoEncryption', metadata, function () { + return this.clientEncrypted + .db(dataDbName) + .collection('view') + .insertOne({ a: 1 }) + .then( + () => { + throw new Error('Expected insert to fail, but it succeeded'); + }, + err => { + expect(err) + .to.have.property('message') + .that.matches(/cannot auto encrypt a view/); + } + ); + }); + }); + + describe('Corpus Test', function () { + it('runs in a separate suite', () => { + expect(() => + fs.statSync(path.resolve(__dirname, './client_side_encryption.prose.corpus.test.js')) + ).not.to.throw(); + }); + }); + + describe('Custom Endpoint Test', function () { + // Data keys created with AWS KMS may specify a custom endpoint to contact (instead of the default endpoint derived from the AWS region). + + beforeEach(function () { + // 1. Create a ``ClientEncryption`` object (referred to as ``client_encryption``) + // Configure with ``aws`` KMS providers as follows: + // .. code:: javascript + // { + // "aws": { } + // } + // Configure with ``keyVaultNamespace`` set to ``keyvault.datakeys``, and a default MongoClient as the ``keyVaultClient``. + this.client = this.configuration.newClient(); + + const customKmsProviders = getKmsProviders(); + customKmsProviders.azure.identityPlatformEndpoint = 'login.microsoftonline.com:443'; + customKmsProviders.gcp.endpoint = 'oauth2.googleapis.com:443'; + + const invalidKmsProviders = getKmsProviders(); + invalidKmsProviders.azure.identityPlatformEndpoint = 'doesnotexist.invalid:443'; + invalidKmsProviders.gcp.endpoint = 'doesnotexist.invalid:443'; + invalidKmsProviders.kmip.endpoint = 'doesnotexist.local:5698'; return this.client.connect().then(() => { const mongodbClientEncryption = this.configuration.mongodbClientEncryption; @@ -654,7 +877,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { const testCases = [ { - description: 'no custom endpoint', + description: '1. aws: no custom endpoint', provider: 'aws', masterKey: { region: 'us-east-1', @@ -663,7 +886,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { succeed: true }, { - description: 'custom endpoint', + description: '2. aws: custom endpoint', provider: 'aws', masterKey: { region: 'us-east-1', @@ -673,7 +896,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { succeed: true }, { - description: 'custom endpoint with port', + description: '3. aws: custom endpoint with port', provider: 'aws', masterKey: { region: 'us-east-1', @@ -683,7 +906,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { succeed: true }, { - description: 'custom endpoint with bad url', + description: '4. aws: custom endpoint with bad url', provider: 'aws', masterKey: { region: 'us-east-1', @@ -699,7 +922,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { } }, { - description: 'custom endpoint that does not match region', + description: '5. aws: custom endpoint that does not match region', provider: 'aws', masterKey: { region: 'us-east-1', @@ -716,7 +939,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { } }, { - description: 'custom endpoint with parse error', + description: '6. aws: custom endpoint with parse error', provider: 'aws', masterKey: { region: 'us-east-1', @@ -733,7 +956,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { } }, { - description: 'azure custom endpoint', + description: '7. azure: custom endpoint', provider: 'azure', masterKey: { keyVaultEndpoint: 'key-vault-csfle.vault.azure.net', @@ -743,7 +966,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { checkAgainstInvalid: true }, { - description: 'gcp custom endpoint', + description: '8. gcp: custom endpoint', provider: 'gcp', masterKey: { projectId: 'devprod-drivers', @@ -756,7 +979,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { checkAgainstInvalid: true }, { - description: 'gcp invalid custom endpoint', + description: '9. gcp: invalid custom endpoint', provider: 'gcp', masterKey: { projectId: 'devprod-drivers', @@ -775,7 +998,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { } }, { - description: 'kmip no custom endpoint', + description: '10. kmip: no custom endpoint', provider: 'kmip', masterKey: { keyId: '1' @@ -784,7 +1007,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { checkAgainstInvalid: true }, { - description: 'kmip custom endpoint', + description: '11. kmip: custom endpoint', provider: 'kmip', masterKey: { keyId: '1', @@ -793,7 +1016,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () { succeed: true }, { - description: 'kmip invalid custom endpoint', + description: '12. kmip: invalid custom endpoint', provider: 'kmip', masterKey: { keyId: '1', @@ -811,12 +1034,11 @@ describe('Client Side Encryption Prose Tests', metadata, function () { testCases.forEach(testCase => { it(testCase.description, metadata, function () { - // 2. Call `client_encryption.createDataKey()` with "aws" as the provider and the following masterKey: + // Call `client_encryption.createDataKey()` with as the provider and the following masterKey: // .. code:: javascript // { // ... // } - // Expect this to succeed. Use the returned UUID of the key to explicitly encrypt and decrypt the string "test" to validate it works. const masterKey = testCase.masterKey; const promises = []; @@ -827,523 +1049,392 @@ describe('Client Side Encryption Prose Tests', metadata, function () { throw new Error('Expected test case to fail to create data key, but it succeeded'); } return this.clientEncryption - .encrypt('test', { - keyId, - algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' - }) - .then(encrypted => this.clientEncryption.decrypt(encrypted)) - .then(result => { - expect(result).to.equal('test'); - }); - }, - err => { - if (testCase.succeed) { - throw err; - } - if (!testCase.errorValidator) { - throw new Error('Invalid Error validator'); - } - - testCase.errorValidator(err); - } - ) - ); - - if (testCase.checkAgainstInvalid) { - promises.push( - this.clientEncryptionInvalid.createDataKey(testCase.provider, { masterKey }).then( - () => { - throw new Error('Expected test case to fail to create data key, but it succeeded'); - }, - err => { - expect(err) - .property('message') - .to.match(/KMS request failed/); - } - ) - ); - } - - return Promise.all(promises); - }); - }); - }); - - describe('BSON size limits and batch splitting', function () { - const fs = require('fs'); - const path = require('path'); - const { EJSON } = BSON; - function loadLimits(file) { - return EJSON.parse( - fs.readFileSync(path.resolve(__dirname, '../../spec/client-side-encryption/limits', file)) - ); - } - - const limitsSchema = loadLimits('limits-schema.json'); - const limitsKey = loadLimits('limits-key.json'); - const limitsDoc = loadLimits('limits-doc.json'); - - let hasRunFirstTimeSetup = false; - beforeEach(async function () { - if (hasRunFirstTimeSetup) { - // Even though we have to use a beforeEach here - // We still only want the following code to be run *once* - // before all the tests that follow - return; - } - hasRunFirstTimeSetup = true; - // First, perform the setup. - - // #. Create a MongoClient without encryption enabled (referred to as ``client``). - this.client = this.configuration.newClient(); - - await this.client - .connect() - // #. Using ``client``, drop and create the collection ``db.coll`` configured with the included JSON schema `limits/limits-schema.json <../limits/limits-schema.json>`_. - .then(() => dropCollection(this.client.db(dataDbName), dataCollName)) - .then(() => { - return this.client.db(dataDbName).createCollection(dataCollName, { - validator: { $jsonSchema: limitsSchema } - }); - }) - // #. Using ``client``, drop the collection ``keyvault.datakeys``. Insert the document `limits/limits-key.json <../limits/limits-key.json>`_ - .then(() => dropCollection(this.client.db(keyVaultDbName), keyVaultCollName)) - .then(() => { - return this.client - .db(keyVaultDbName) - .collection(keyVaultCollName) - .insertOne(limitsKey, { writeConcern: { w: 'majority' } }); - }); - }); - - beforeEach(function () { - // #. Create a MongoClient configured with auto encryption (referred to as ``client_encrypted``) - // Configure with the ``local`` KMS provider as follows: - // .. code:: javascript - // { "local": { "key": } } - // Configure with the ``keyVaultNamespace`` set to ``keyvault.datakeys``. - this.clientEncrypted = this.configuration.newClient( - {}, - { - monitorCommands: true, - autoEncryption: { - keyVaultNamespace, - kmsProviders: getKmsProviders(LOCAL_KEY) - } - } - ); - return this.clientEncrypted.connect().then(() => { - this.encryptedColl = this.clientEncrypted.db(dataDbName).collection(dataCollName); - this.commandStartedEvents = new APMEventCollector(this.clientEncrypted, 'commandStarted', { - include: ['insert'] - }); - }); - }); - - afterEach(function () { - if (this.commandStartedEvents) { - this.commandStartedEvents.teardown(); - this.commandStartedEvents = undefined; - } - if (this.clientEncrypted) { - return this.clientEncrypted.close(); - } - }); - - after(function () { - return this.client && this.client.close(); - }); - - // Using ``client_encrypted`` perform the following operations: - - function repeatedChar(char, length) { - return Array.from({ length }) - .map(() => char) - .join(''); - } - - const testCases = [ - // #. Insert ``{ "_id": "over_2mib_under_16mib", "unencrypted": }``. - // Expect this to succeed since this is still under the ``maxBsonObjectSize`` limit. - { - description: 'should succeed for over_2mib_under_16mib', - docs: () => [{ _id: 'over_2mib_under_16mib', unencrypted: repeatedChar('a', 2097152) }], - expectedEvents: [{ commandName: 'insert' }] - }, - // #. Insert the document `limits/limits-doc.json <../limits/limits-doc.json>`_ concatenated with ``{ "_id": "encryption_exceeds_2mib", "unencrypted": < the string "a" repeated (2097152 - 2000) times > }`` - // Note: limits-doc.json is a 1005 byte BSON document that encrypts to a ~10,000 byte document. - // Expect this to succeed since after encryption this still is below the normal maximum BSON document size. - // Note, before auto encryption this document is under the 2 MiB limit. After encryption it exceeds the 2 MiB limit, but does NOT exceed the 16 MiB limit. - { - description: 'should succeed for encryption_exceeds_2mib', - docs: () => [ - Object.assign({}, limitsDoc, { - _id: 'encryption_exceeds_2mib', - unencrypted: repeatedChar('a', 2097152 - 2000) - }) - ], - expectedEvents: [{ commandName: 'insert' }] - }, - // #. Bulk insert the following: - // - ``{ "_id": "over_2mib_1", "unencrypted": }`` - // - ``{ "_id": "over_2mib_2", "unencrypted": }`` - // Expect the bulk write to succeed and split after first doc (i.e. two inserts occur). This may be verified using `command monitoring `_. - { - description: 'should succeed for bulk over_2mib', - docs: () => [ - { _id: 'over_2mib_1', unencrypted: repeatedChar('a', 2097152) }, - { _id: 'over_2mib_2', unencrypted: repeatedChar('a', 2097152) } - ], - expectedEvents: [{ commandName: 'insert' }, { commandName: 'insert' }] - }, - // #. Bulk insert the following: - // - The document `limits/limits-doc.json <../limits/limits-doc.json>`_ concatenated with ``{ "_id": "encryption_exceeds_2mib_1", "unencrypted": < the string "a" repeated (2097152 - 2000) times > }`` - // - The document `limits/limits-doc.json <../limits/limits-doc.json>`_ concatenated with ``{ "_id": "encryption_exceeds_2mib_2", "unencrypted": < the string "a" repeated (2097152 - 2000) times > }`` - // Expect the bulk write to succeed and split after first doc (i.e. two inserts occur). This may be verified using `command monitoring `_. - { - description: 'should succeed for bulk encryption_exceeds_2mib', - docs: () => [ - Object.assign({}, limitsDoc, { - _id: 'encryption_exceeds_2mib_1', - unencrypted: repeatedChar('a', 2097152 - 2000) - }), - Object.assign({}, limitsDoc, { - _id: 'encryption_exceeds_2mib_2', - unencrypted: repeatedChar('a', 2097152 - 2000) - }) - ], - expectedEvents: [{ commandName: 'insert' }, { commandName: 'insert' }] - }, - // #. Insert ``{ "_id": "under_16mib", "unencrypted": ``. - // Expect this to succeed since this is still (just) under the ``maxBsonObjectSize`` limit. - { - description: 'should succeed for under_16mib', - docs: () => [{ _id: 'under_16mib', unencrypted: repeatedChar('a', 16777216 - 2000) }], - expectedEvents: [{ commandName: 'insert' }] - }, - // #. Insert the document `limits/limits-doc.json <../limits/limits-doc.json>`_ concatenated with ``{ "_id": "encryption_exceeds_16mib", "unencrypted": < the string "a" repeated (16777216 - 2000) times > }`` - // Expect this to fail since encryption results in a document exceeding the ``maxBsonObjectSize`` limit. - { - description: 'should fail for encryption_exceeds_16mib', - docs: () => [ - Object.assign({}, limitsDoc, { - _id: 'encryption_exceeds_16mib', - unencrypted: repeatedChar('a', 16777216 - 2000) - }) - ], - error: true - } - ]; - - testCases.forEach(testCase => { - it(testCase.description, metadata, function () { - return this.encryptedColl.insertMany(testCase.docs()).then( - () => { - if (testCase.error) { - throw new Error('Expected this insert to fail, but it succeeded'); - } - const expectedEvents = Array.from(testCase.expectedEvents); - const actualEvents = pruneEvents(this.commandStartedEvents.events); + .encrypt('test', { + keyId, + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' + }) + .then(encrypted => this.clientEncryption.decrypt(encrypted)) + .then(result => { + expect(result).to.equal('test'); + }); + }, + err => { + if (testCase.succeed) { + throw err; + } + if (!testCase.errorValidator) { + throw new Error('Invalid Error validator'); + } - expect(actualEvents) - .to.have.a.lengthOf(expectedEvents.length) - .and.to.containSubset(expectedEvents); - }, - err => { - if (!testCase.error) { - throw err; + testCase.errorValidator(err); } - } + ) ); + + if (testCase.checkAgainstInvalid) { + promises.push( + this.clientEncryptionInvalid.createDataKey(testCase.provider, { masterKey }).then( + () => { + throw new Error('Expected test case to fail to create data key, but it succeeded'); + }, + err => { + expect(err) + .property('message') + .to.match(/KMS request failed/); + } + ) + ); + } + + return Promise.all(promises); }); }); + }); - function pruneEvents(events) { - return events.map(event => { - // We are pruning out the bunch of repeating As, mostly - // b/c an error failure will try to print 2mb of 'a's - // and not have a good time. - event.command = Object.assign({}, event.command); - event.command.documents = event.command.documents.map(doc => { - doc = Object.assign({}, doc); - if (doc.unencrypted) { - doc.unencrypted = "Lots of repeating 'a's"; - } - return doc; - }); - return event; - }); - } + // TODO(NODE-2422): Implement bypass prose tests + describe('Bypass spawning mongocryptd', () => { + it.skip('Via mongocryptdBypassSpawn', () => {}).skipReason = + 'TODO(NODE-2422): Implement "Bypass spawning mongocryptd" tests'; + + it.skip('Via bypassAutoEncryption', () => {}).skipReason = + 'TODO(NODE-2422): Implement "Bypass spawning mongocryptd" tests'; }); - describe('Views are prohibited', function () { - before(function () { - // First, perform the setup. + describe('Deadlock tests', () => { + deadlockTests(metadata); + }); - // #. Create a MongoClient without encryption enabled (referred to as ``client``). - this.client = this.configuration.newClient(); + // TODO(NODE-3151): Implement kms prose tests + describe('KMS TLS Tests', () => { + it.skip('TBD', () => {}).skipReason = 'TODO(NODE-3151): Implement "KMS TLS Tests"'; + }); - return this.client - .connect() - .then(() => dropCollection(this.client.db(dataDbName), dataCollName)) - .then(() => { - return this.client.db(dataDbName).createCollection(dataCollName); - }) - .then(() => { - return this.client - .db(dataDbName) - .createCollection('view', { viewOn: dataCollName, pipeline: [] }) - .then(noop, noop); - }, noop); + /** + * - Create client encryption no tls + * - Create client encryption with tls + * - Create client encryption expired + * - Create client encryption invalid hostname + */ + context('KMS TLS Options Tests', metadata, function () { + let clientNoTls; + let clientWithTls; + let clientWithTlsExpired; + let clientWithInvalidHostname; + let clientEncryptionNoTls; + let clientEncryptionWithTls; + let clientEncryptionWithTlsExpired; + let clientEncryptionWithInvalidHostname; + + beforeEach(async function () { + const tlsCaOptions = { + aws: { + tlsCAFile: process.env.KMIP_TLS_CA_FILE + }, + azure: { + tlsCAFile: process.env.KMIP_TLS_CA_FILE + }, + gcp: { + tlsCAFile: process.env.KMIP_TLS_CA_FILE + }, + kmip: { + tlsCAFile: process.env.KMIP_TLS_CA_FILE + } + }; + const clientNoTlsOptions = { + keyVaultNamespace, + kmsProviders: getKmsProviders(null, null, '127.0.0.1:8002', '127.0.0.1:8002'), + tlsOptions: tlsCaOptions + }; + const clientWithTlsOptions = { + keyVaultNamespace, + kmsProviders: getKmsProviders(null, null, '127.0.0.1:8002', '127.0.0.1:8002'), + tlsOptions: { + aws: { + tlsCAFile: process.env.KMIP_TLS_CA_FILE, + tlsCertificateKeyFile: process.env.KMIP_TLS_CERT_FILE + }, + azure: { + tlsCAFile: process.env.KMIP_TLS_CA_FILE, + tlsCertificateKeyFile: process.env.KMIP_TLS_CERT_FILE + }, + gcp: { + tlsCAFile: process.env.KMIP_TLS_CA_FILE, + tlsCertificateKeyFile: process.env.KMIP_TLS_CERT_FILE + }, + kmip: { + tlsCAFile: process.env.KMIP_TLS_CA_FILE, + tlsCertificateKeyFile: process.env.KMIP_TLS_CERT_FILE + } + } + }; + const clientWithTlsExpiredOptions = { + keyVaultNamespace, + kmsProviders: getKmsProviders(null, '127.0.0.1:8000', '127.0.0.1:8000', '127.0.0.1:8000'), + tlsOptions: tlsCaOptions + }; + const clientWithInvalidHostnameOptions = { + keyVaultNamespace, + kmsProviders: getKmsProviders(null, '127.0.0.1:8001', '127.0.0.1:8001', '127.0.0.1:8001'), + tlsOptions: tlsCaOptions + }; + const mongodbClientEncryption = this.configuration.mongodbClientEncryption; + + switch (this.currentTest.title) { + case 'should fail with no TLS': + clientNoTls = this.configuration.newClient({}, { autoEncryption: clientNoTlsOptions }); + clientEncryptionNoTls = new mongodbClientEncryption.ClientEncryption(clientNoTls, { + ...clientNoTlsOptions, + bson: BSON + }); + await clientNoTls.connect(); + break; + case 'should succeed with valid TLS options': + clientWithTls = this.configuration.newClient( + {}, + { autoEncryption: clientWithTlsOptions } + ); + clientEncryptionWithTls = new mongodbClientEncryption.ClientEncryption(clientWithTls, { + ...clientWithTlsOptions, + bson: BSON + }); + await clientWithTls.connect(); + break; + case 'should fail with an expired certificate': + clientWithTlsExpired = this.configuration.newClient( + {}, + { autoEncryption: clientWithTlsExpiredOptions } + ); + clientEncryptionWithTlsExpired = new mongodbClientEncryption.ClientEncryption( + clientWithTlsExpired, + { ...clientWithTlsExpiredOptions, bson: BSON } + ); + await clientWithTlsExpired.connect(); + break; + case 'should fail with an invalid hostname': + clientWithInvalidHostname = this.configuration.newClient( + {}, + { autoEncryption: clientWithInvalidHostnameOptions } + ); + clientEncryptionWithInvalidHostname = new mongodbClientEncryption.ClientEncryption( + clientWithInvalidHostname, + { ...clientWithInvalidHostnameOptions, bson: BSON } + ); + await clientWithInvalidHostname.connect(); + break; + default: + throw new Error('unexpected test case'); + } }); - after(function () { - return this.client && this.client.close(); + afterEach(async function () { + const allClients = [ + clientNoTls, + clientWithTls, + clientWithTlsExpired, + clientWithInvalidHostname + ]; + for (const client of allClients) { + if (client) { + await client.close(); + } + } }); - beforeEach(function () { - this.clientEncrypted = this.configuration.newClient( - {}, - { - autoEncryption: { - keyVaultNamespace, - kmsProviders: getKmsProviders(LOCAL_KEY) - } + // Case 1. + context('Case 1: AWS', metadata, function () { + const masterKey = { + region: 'us-east-1', + key: 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0', + endpoint: '127.0.0.1:8002' + }; + const masterKeyExpired = { ...masterKey, endpoint: '127.0.0.1:8000' }; + const masterKeyInvalidHostname = { ...masterKey, endpoint: '127.0.0.1:8001' }; + + it('should fail with no TLS', metadata, async function () { + try { + await clientEncryptionNoTls.createDataKey('aws', { masterKey }); + expect.fail('it must fail with no tls'); + } catch (e) { + // Expect an error indicating TLS handshake failed. + expect(e.originalError.message).to.include('certificate required'); } - ); + }); - return this.clientEncrypted.connect(); - }); + it('should succeed with valid TLS options', metadata, async function () { + try { + await clientEncryptionWithTls.createDataKey('aws', { masterKey }); + expect.fail('it must fail to parse response'); + } catch (e) { + // Expect an error from libmongocrypt with a message containing the string: "parse error". + // This implies TLS handshake succeeded. + expect(e.message).to.include('parse error'); + } + }); - afterEach(function () { - return this.clientEncrypted && this.clientEncrypted.close(); + it('should fail with an expired certificate', async function () { + try { + await clientEncryptionWithTlsExpired.createDataKey('aws', { + masterKey: masterKeyExpired + }); + expect.fail('it must fail with invalid certificate'); + } catch (e) { + // Expect an error indicating TLS handshake failed due to an expired certificate. + expect(e.originalError.message).to.include('certificate has expired'); + } + }); + + it('should fail with an invalid hostname', metadata, async function () { + try { + await clientEncryptionWithInvalidHostname.createDataKey('aws', { + masterKey: masterKeyInvalidHostname + }); + expect.fail('it must fail with invalid hostnames'); + } catch (e) { + // Expect an error indicating TLS handshake failed due to an invalid hostname. + expect(e.originalError.message).to.include('does not match certificate'); + } + }); }); - it('should error when inserting into a view with autoEncryption', metadata, function () { - return this.clientEncrypted - .db(dataDbName) - .collection('view') - .insertOne({ a: 1 }) - .then( - () => { - throw new Error('Expected insert to fail, but it succeeded'); - }, - err => { - expect(err) - .to.have.property('message') - .that.matches(/cannot auto encrypt a view/); - } - ); + // Case 2. + context('Case 2: Azure', metadata, function () { + const masterKey = { + keyVaultEndpoint: 'doesnotexist.local', + keyName: 'foo' + }; + + it('should fail with no TLS', metadata, async function () { + try { + await clientEncryptionNoTls.createDataKey('azure', { masterKey }); + expect.fail('it must fail with no tls'); + } catch (e) { + //Expect an error indicating TLS handshake failed. + expect(e.originalError.message).to.include('certificate required'); + } + }); + + it('should succeed with valid TLS options', metadata, async function () { + try { + await clientEncryptionWithTls.createDataKey('azure', { masterKey }); + expect.fail('it must fail with HTTP 404'); + } catch (e) { + // Expect an error from libmongocrypt with a message containing the string: "HTTP status=404". + // This implies TLS handshake succeeded. + expect(e.message).to.include('HTTP status=404'); + } + }); + + it('should fail with an expired certificate', async function () { + try { + await clientEncryptionWithTlsExpired.createDataKey('azure', { masterKey }); + expect.fail('it must fail with expired certificates'); + } catch (e) { + // Expect an error indicating TLS handshake failed due to an expired certificate. + expect(e.originalError.message).to.include('certificate has expired'); + } + }); + + it('should fail with an invalid hostname', metadata, async function () { + try { + await clientEncryptionWithInvalidHostname.createDataKey('azure', { masterKey }); + expect.fail('it must fail with invalid hostnames'); + } catch (e) { + // Expect an error indicating TLS handshake failed due to an invalid hostname. + expect(e.originalError.message).to.include('does not match certificate'); + } + }); }); - }); - // TODO: We cannot implement these tests according to spec b/c the tests require a - // connect-less client. So instead we are implementing the tests via APM, - // and confirming that the externalClient is firing off keyVault requests during - // encrypted operations - describe('External Key Vault', function () { - const fs = require('fs'); - const path = require('path'); - const { EJSON } = BSON; - function loadExternal(file) { - return EJSON.parse( - fs.readFileSync(path.resolve(__dirname, '../../spec/client-side-encryption/external', file)) - ); - } + // Case 3. + context('Case 3: GCP', metadata, function () { + const masterKey = { + projectId: 'foo', + location: 'bar', + keyRing: 'baz', + keyName: 'foo' + }; - const externalKey = loadExternal('external-key.json'); - const externalSchema = loadExternal('external-schema.json'); + it('should fail with no TLS', metadata, async function () { + try { + await clientEncryptionNoTls.createDataKey('gcp', { masterKey }); + expect.fail('it must fail with no tls'); + } catch (e) { + //Expect an error indicating TLS handshake failed. + expect(e.originalError.message).to.include('certificate required'); + } + }); - beforeEach(function () { - this.client = this.configuration.newClient(); + it('should succeed with valid TLS options', metadata, async function () { + try { + await clientEncryptionWithTls.createDataKey('gcp', { masterKey }); + expect.fail('it must fail with HTTP 404'); + } catch (e) { + // Expect an error from libmongocrypt with a message containing the string: "HTTP status=404". + // This implies TLS handshake succeeded. + expect(e.message).to.include('HTTP status=404'); + } + }); - // #. Create a MongoClient without encryption enabled (referred to as ``client``). - return ( - this.client - .connect() - // #. Using ``client``, drop the collections ``keyvault.datakeys`` and ``db.coll``. - // Insert the document `external/external-key.json <../external/external-key.json>`_ into ``keyvault.datakeys``. - .then(() => dropCollection(this.client.db(dataDbName), dataCollName)) - .then(() => dropCollection(this.client.db(keyVaultDbName), keyVaultCollName)) - .then(() => { - return this.client - .db(keyVaultDbName) - .collection(keyVaultCollName) - .insertOne(externalKey, { writeConcern: { w: 'majority' } }); - }) - ); - }); + it('should fail with an expired certificate', async function () { + try { + await clientEncryptionWithTlsExpired.createDataKey('gcp', { masterKey }); + expect.fail('it must fail with expired certificates'); + } catch (e) { + // Expect an error indicating TLS handshake failed due to an expired certificate. + expect(e.originalError.message).to.include('certificate has expired'); + } + }); - afterEach(function () { - if (this.commandStartedEvents) { - this.commandStartedEvents.teardown(); - this.commandStartedEvents = undefined; - } - return Promise.resolve() - .then(() => this.externalClient && this.externalClient.close()) - .then(() => this.clientEncrypted && this.clientEncrypted.close()) - .then(() => this.client && this.client.close()); + it('should fail with an invalid hostname', metadata, async function () { + try { + await clientEncryptionWithInvalidHostname.createDataKey('gcp', { masterKey }); + expect.fail('it must fail with invalid hostnames'); + } catch (e) { + // Expect an error indicating TLS handshake failed due to an invalid hostname. + expect(e.originalError.message).to.include('does not match certificate'); + } + }); }); - function defineTest(withExternalKeyVault) { - it( - `should work ${withExternalKeyVault ? 'with' : 'without'} external key vault`, - metadata, - function () { - const ClientEncryption = this.configuration.mongodbClientEncryption.ClientEncryption; - return ( - Promise.resolve() - .then(() => { - // If ``withExternalKeyVault == true``, configure both objects with an external key vault client. The external client MUST connect to the same - // MongoDB cluster that is being tested against, except it MUST use the username ``fake-user`` and password ``fake-pwd``. - this.externalClient = this.configuration.newClient( - // this.configuration.url('fake-user', 'fake-pwd'), - // TODO: Do this properly - {}, - { monitorCommands: true } - ); + // Case 4. + context('Case 4: KMIP', metadata, function () { + const masterKey = {}; - this.commandStartedEvents = new APMEventCollector( - this.externalClient, - 'commandStarted', - { - include: ['find'] - } - ); - return this.externalClient.connect(); - }) - // #. Create the following: - // - A MongoClient configured with auto encryption (referred to as ``client_encrypted``) - // - A ``ClientEncryption`` object (referred to as ``client_encryption``) - // Configure both objects with the ``local`` KMS providers as follows: - // .. code:: javascript - // { "local": { "key": } } - // Configure both objects with ``keyVaultNamespace`` set to ``keyvault.datakeys``. - // Configure ``client_encrypted`` to use the schema `external/external-schema.json <../external/external-schema.json>`_ for ``db.coll`` by setting a schema map like: ``{ "db.coll": }`` - .then(() => { - const options = { - bson: BSON, - keyVaultNamespace, - kmsProviders: getKmsProviders(LOCAL_KEY) - }; + it('should fail with no TLS', metadata, async function () { + try { + await clientEncryptionNoTls.createDataKey('kmip', { masterKey }); + expect.fail('it must fail with no tls'); + } catch (e) { + //Expect an error indicating TLS handshake failed. + expect(e.originalError.message).to.include('before secure TLS connection'); + } + }); - if (withExternalKeyVault) { - options.keyVaultClient = this.externalClient; - } + it('should succeed with valid TLS options', metadata, async function () { + const keyId = await clientEncryptionWithTls.createDataKey('kmip', { masterKey }); + // expect success + expect(keyId).to.be.an('object'); + }); - this.clientEncryption = new ClientEncryption( - this.client, - Object.assign({}, options) - ); - this.clientEncrypted = this.configuration.newClient( - {}, - { - autoEncryption: Object.assign({}, options, { - schemaMap: { - 'db.coll': externalSchema - } - }) - } - ); - return this.clientEncrypted.connect(); - }) - .then(() => { - // #. Use ``client_encrypted`` to insert the document ``{"encrypted": "test"}`` into ``db.coll``. - // If ``withExternalKeyVault == true``, expect an authentication exception to be thrown. Otherwise, expect the insert to succeed. - this.commandStartedEvents.clear(); - return this.clientEncrypted - .db(dataDbName) - .collection(dataCollName) - .insertOne({ encrypted: 'test' }) - .then(() => { - if (withExternalKeyVault) { - expect(this.commandStartedEvents.events).to.containSubset([ - { - commandName: 'find', - databaseName: keyVaultDbName, - command: { find: keyVaultCollName } - } - ]); - } else { - expect(this.commandStartedEvents.events).to.not.containSubset([ - { - commandName: 'find', - databaseName: keyVaultDbName, - command: { find: keyVaultCollName } - } - ]); - } - }); - // TODO: Do this in the spec-compliant way using bad auth credentials - // .then( - // () => { - // if (withExternalKeyVault) { - // throw new Error( - // 'expected insert to fail with authentication error, but it passed' - // ); - // } - // }, - // err => { - // if (!withExternalKeyVault) { - // throw err; - // } - // expect(err).to.be.an.instanceOf(Error); - // } - // ); - }) - .then(() => { - // #. Use ``client_encryption`` to explicitly encrypt the string ``"test"`` with key ID ``LOCALAAAAAAAAAAAAAAAAA==`` and deterministic algorithm. - // If ``withExternalKeyVault == true``, expect an authentication exception to be thrown. Otherwise, expect the insert to succeed. - this.commandStartedEvents.clear(); - return this.clientEncryption - .encrypt('test', { - keyId: externalKey._id, - algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' - }) - .then(() => { - if (withExternalKeyVault) { - expect(this.commandStartedEvents.events).to.containSubset([ - { - commandName: 'find', - databaseName: keyVaultDbName, - command: { find: keyVaultCollName } - } - ]); - } else { - expect(this.commandStartedEvents.events).to.not.containSubset([ - { - commandName: 'find', - databaseName: keyVaultDbName, - command: { find: keyVaultCollName } - } - ]); - } - }); - // TODO: Do this in the spec-compliant way using bad auth credentials - // .then( - // () => { - // if (withExternalKeyVault) { - // throw new Error( - // 'expected insert to fail with authentication error, but it passed' - // ); - // } - // }, - // err => { - // if (!withExternalKeyVault) { - // throw err; - // } - // expect(err).to.be.an.instanceOf(Error); - // } - // ); - }) - ); + it('should fail with an expired certificate', async function () { + try { + await clientEncryptionWithTlsExpired.createDataKey('kmip', { masterKey }); + expect.fail('it must fail with expired certificates'); + } catch (e) { + // Expect an error indicating TLS handshake failed due to an expired certificate. + expect(e.originalError.message).to.include('certificate has expired'); } - ); - } - // Run the following tests twice, parameterized by a boolean ``withExternalKeyVault``. - defineTest(true); - defineTest(false); - }); + }); - deadlockTests(metadata); + it('should fail with an invalid hostname', metadata, async function () { + try { + await clientEncryptionWithInvalidHostname.createDataKey('kmip', { masterKey }); + expect.fail('it must fail with invalid hostnames'); + } catch (e) { + // Expect an error indicating TLS handshake failed due to an invalid hostname. + expect(e.originalError.message).to.include('does not match certificate'); + } + }); + }); + }); }); diff --git a/test/integration/client-side-encryption/client_side_encryption.spec.test.js b/test/integration/client-side-encryption/client_side_encryption.spec.test.js index fa2c3ff16c..8fc048f599 100644 --- a/test/integration/client-side-encryption/client_side_encryption.spec.test.js +++ b/test/integration/client-side-encryption/client_side_encryption.spec.test.js @@ -40,9 +40,11 @@ const skippedAuthTests = [ 'type=binData', 'type=int', 'type=objectId', + 'type=symbol', 'replaceOne with encryption', 'Insert with encryption on a missing key', 'A local schema should override', + 'Count with deterministic encryption', 'Insert a document with auto encryption using local KMS provider', 'Insert with encryption using key alt name', 'insertMany with encryption', @@ -52,41 +54,25 @@ const skippedAuthTests = [ 'Insert a document with auto encryption using KMIP KMS provider' ]; -const SKIPPED_TESTS = new Set(isAuthEnabled ? skippedAuthTests : []); +// TODO(NODE-4006): Investigate csfle test "operation fails with maxWireVersion < 8" +const skippedMaxWireVersionTest = 'operation fails with maxWireVersion < 8'; -describe('Client Side Encryption', function () { - // TODO: Replace this with using the filter once the filter works on describe blocks - const skipTests = process.env.CSFLE_KMS_PROVIDERS == null; - if (skipTests) { - // console.log('skipping Client Side Encryption Spec tests due to lack of AWS credentials'); - return; - } - - try { - require('mongodb-client-encryption'); - } catch (e) { - console.error( - 'skipping Client Side Encryption Spec tests due to inability to load mongodb-client-encryption' - ); - return; - } +const SKIPPED_TESTS = new Set( + isAuthEnabled ? skippedAuthTests.concat(skippedMaxWireVersionTest) : [skippedMaxWireVersionTest] +); +describe('Client Side Encryption', function () { const testContext = new TestRunnerContext(); + testContext.requiresCSFLE = true; const testSuites = gatherTestSuites( path.join(__dirname, '../../spec/client-side-encryption/tests') ); - after(() => testContext.teardown()); before(function () { return testContext.setup(this.configuration); }); generateTopologyTests(testSuites, testContext, spec => { - return ( - !spec.description.match(/type=symbol/) && - !spec.description.match(/maxWireVersion < 8/) && - !spec.description.match(/Count with deterministic encryption/) && - !SKIPPED_TESTS.has(spec.description) - ); + return !SKIPPED_TESTS.has(spec.description); }); }); diff --git a/test/integration/command-monitoring/command_monitoring.spec.test.js b/test/integration/command-monitoring/command_monitoring.spec.test.js index 7301da92b9..09e0b1b856 100644 --- a/test/integration/command-monitoring/command_monitoring.spec.test.js +++ b/test/integration/command-monitoring/command_monitoring.spec.test.js @@ -262,7 +262,7 @@ describe('Command Monitoring spec tests', function () { test.description === 'A successful find event with a getmore and the server kills the cursor' ) { - this.skipReason = 'TODO(NODE-3308): update spec file'; + this.test.skipReason = 'TODO(NODE-3308): update spec file'; this.skip(); } diff --git a/test/manual/mocharc.json b/test/manual/mocharc.json index 679ed3e227..b129ce1fb9 100644 --- a/test/manual/mocharc.json +++ b/test/manual/mocharc.json @@ -1,5 +1,6 @@ { "require": "ts-node/register", "reporter": "test/tools/reporter/mongodb_reporter.js", + "failZero": true, "color": true } diff --git a/test/tools/runner/filters/client_encryption_filter.js b/test/tools/runner/filters/client_encryption_filter.js index 56f25bfe08..e7ce15da82 100644 --- a/test/tools/runner/filters/client_encryption_filter.js +++ b/test/tools/runner/filters/client_encryption_filter.js @@ -39,7 +39,20 @@ class ClientSideEncryptionFilter { const clientSideEncryption = test.metadata && test.metadata.requires && test.metadata.requires.clientSideEncryption; - return typeof clientSideEncryption !== 'boolean' || clientSideEncryption === this.enabled; + if (clientSideEncryption == null) { + return true; + } + + if (clientSideEncryption !== true) { + throw new Error('ClientSideEncryptionFilter can only be set to true'); + } + + // TODO(NODE-3401): unskip csfle tests on windows + if (process.env.TEST_CSFLE && !this.enabled && process.platform !== 'win32') { + throw new Error('Expected CSFLE to be enabled in the CI'); + } + + return this.enabled; } } diff --git a/test/tools/spec-runner/index.js b/test/tools/spec-runner/index.js index 0cbac9ff69..0ebee503d0 100644 --- a/test/tools/spec-runner/index.js +++ b/test/tools/spec-runner/index.js @@ -10,6 +10,7 @@ const TestRunnerContext = require('./context').TestRunnerContext; const resolveConnectionString = require('./utils').resolveConnectionString; const { LEGACY_HELLO_COMMAND } = require('../../../src/constants'); const { isAnyRequirementSatisfied } = require('../unified-spec-runner/unified-utils'); +const ClientSideEncryptionFilter = require('../runner/filters/client_encryption_filter'); // Promise.try alternative https://stackoverflow.com/questions/60624081/promise-try-without-bluebird/60624164?noredirect=1#comment107255389_60624164 function promiseTry(callback) { @@ -168,11 +169,29 @@ function generateTopologyTests(testSuites, testContext, filter) { shouldRun = false; } - if (typeof filter === 'function' && !filter(spec, this.configuration)) { + if (shouldRun && typeof filter === 'function' && !filter(spec, this.configuration)) { this.currentTest.skipReason = `filtered by custom filter passed to generateTopologyTests`; shouldRun = false; } + + let csfleFilterError = null; + if (shouldRun && testContext.requiresCSFLE) { + const csfleFilter = new ClientSideEncryptionFilter(); + csfleFilter.initializeFilter(null, {}, () => null); + try { + if (!csfleFilter.filter({ metadata: { requires: { clientSideEncryption: true } } })) { + shouldRun = false; + this.currentTest.skipReason = `filtered by ClientSideEncryptionFilter`; + } + } catch (err) { + csfleFilterError = err; + } + } + await utilClient.close(); + if (csfleFilterError) { + throw csfleFilterError; + } if (!shouldRun) this.skip(); };