|
| 1 | +import * as BSON from 'bson'; |
| 2 | +import { expect } from 'chai'; |
| 3 | +import { readFileSync } from 'fs'; |
| 4 | +import * as path from 'path'; |
| 5 | +import * as util from 'util'; |
| 6 | + |
| 7 | +import { CommandStartedEvent, MongoClient, MongoClientOptions } from '../../mongodb'; |
| 8 | +import { installNodeDNSWorkaroundHooks } from '../../tools/runner/hooks/configuration'; |
| 9 | +import { getEncryptExtraOptions } from '../../tools/utils'; |
| 10 | +import { dropCollection } from '../shared'; |
| 11 | + |
| 12 | +/* REFERENCE: (note commit hash) */ |
| 13 | +/* https://github.com/mongodb/specifications/blob/b3beada 72ae1c992294ae6a8eea572003a274c35/source/client-side-encryption/tests/README.rst#deadlock-tests */ |
| 14 | + |
| 15 | +const LOCAL_KEY = Buffer.from( |
| 16 | + 'Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk', |
| 17 | + 'base64' |
| 18 | +); |
| 19 | + |
| 20 | +const externalKey = BSON.EJSON.parse( |
| 21 | + readFileSync( |
| 22 | + path.resolve(__dirname, '../../spec/client-side-encryption/external/external-key.json'), |
| 23 | + { encoding: 'utf-8' } |
| 24 | + ) |
| 25 | +); |
| 26 | +const $jsonSchema = BSON.EJSON.parse( |
| 27 | + readFileSync( |
| 28 | + path.resolve(__dirname, '../../spec/client-side-encryption/external/external-schema.json'), |
| 29 | + { encoding: 'utf-8' } |
| 30 | + ) |
| 31 | +); |
| 32 | + |
| 33 | +class CapturingMongoClient extends MongoClient { |
| 34 | + commandStartedEvents: Array<CommandStartedEvent> = []; |
| 35 | + clientsCreated = 0; |
| 36 | + constructor(url: string, options: MongoClientOptions = {}) { |
| 37 | + options = { ...options, monitorCommands: true, [Symbol.for('@@mdb.skipPingOnConnect')]: true }; |
| 38 | + if (process.env.MONGODB_API_VERSION) { |
| 39 | + options.serverApi = process.env.MONGODB_API_VERSION as MongoClientOptions['serverApi']; |
| 40 | + } |
| 41 | + |
| 42 | + super(url, options); |
| 43 | + |
| 44 | + this.on('commandStarted', ev => this.commandStartedEvents.push(ev)); |
| 45 | + this.on('topologyOpening', () => this.clientsCreated++); |
| 46 | + } |
| 47 | +} |
| 48 | + |
| 49 | +function deadlockTest( |
| 50 | + { |
| 51 | + maxPoolSize, |
| 52 | + bypassAutoEncryption, |
| 53 | + useKeyVaultClient |
| 54 | + }: { maxPoolSize: number; useKeyVaultClient: boolean; bypassAutoEncryption: boolean }, |
| 55 | + assertions |
| 56 | +) { |
| 57 | + return async function () { |
| 58 | + const url = this.configuration.url(); |
| 59 | + const clientTest = this.clientTest; |
| 60 | + const ciphertext = this.ciphertext; |
| 61 | + |
| 62 | + const clientEncryptedOpts = { |
| 63 | + autoEncryption: { |
| 64 | + keyVaultNamespace: 'keyvault.datakeys', |
| 65 | + kmsProviders: { local: { key: LOCAL_KEY } }, |
| 66 | + bypassAutoEncryption, |
| 67 | + keyVaultClient: useKeyVaultClient ? this.clientKeyVault : undefined, |
| 68 | + extraOptions: getEncryptExtraOptions() |
| 69 | + }, |
| 70 | + maxPoolSize |
| 71 | + }; |
| 72 | + |
| 73 | + const clientEncrypted = new CapturingMongoClient(url, clientEncryptedOpts); |
| 74 | + |
| 75 | + await clientEncrypted.connect(); |
| 76 | + |
| 77 | + try { |
| 78 | + if (bypassAutoEncryption) { |
| 79 | + await clientTest.db('db').collection('coll').insertOne({ _id: 0, encrypted: ciphertext }); |
| 80 | + } else { |
| 81 | + await clientEncrypted |
| 82 | + .db('db') |
| 83 | + .collection('coll') |
| 84 | + .insertOne({ _id: 0, encrypted: 'string0' }); |
| 85 | + } |
| 86 | + |
| 87 | + const res = await clientEncrypted.db('db').collection('coll').findOne({ _id: 0 }); |
| 88 | + |
| 89 | + expect(res).to.have.property('_id', 0); |
| 90 | + expect(res).to.have.property('encrypted', 'string0'); |
| 91 | + assertions(clientEncrypted, this.clientKeyVault); |
| 92 | + } finally { |
| 93 | + await clientEncrypted.close(); |
| 94 | + } |
| 95 | + }; |
| 96 | +} |
| 97 | + |
| 98 | +const metadata = { |
| 99 | + requires: { |
| 100 | + clientSideEncryption: true, |
| 101 | + mongodb: '>=4.2.0', |
| 102 | + topology: '!load-balanced' |
| 103 | + } |
| 104 | +}; |
| 105 | +describe('Connection Pool Deadlock Prevention', function () { |
| 106 | + installNodeDNSWorkaroundHooks(); |
| 107 | + beforeEach(async function () { |
| 108 | + const mongodbClientEncryption = this.configuration.mongodbClientEncryption; |
| 109 | + const url: string = this.configuration.url(); |
| 110 | + |
| 111 | + this.clientTest = new CapturingMongoClient(url); |
| 112 | + this.clientKeyVault = new CapturingMongoClient(url, { |
| 113 | + monitorCommands: true, |
| 114 | + maxPoolSize: 1 |
| 115 | + }); |
| 116 | + |
| 117 | + this.clientEncryption = undefined; |
| 118 | + this.ciphertext = undefined; |
| 119 | + |
| 120 | + await this.clientTest.connect(); |
| 121 | + await this.clientKeyVault.connect(); |
| 122 | + await dropCollection(this.clientTest.db('keyvault'), 'datakeys'); |
| 123 | + await dropCollection(this.clientTest.db('db'), 'coll'); |
| 124 | + |
| 125 | + await this.clientTest |
| 126 | + .db('keyvault') |
| 127 | + .collection('datakeys') |
| 128 | + .insertOne(externalKey, { |
| 129 | + writeConcern: { w: 'majority' } |
| 130 | + }); |
| 131 | + |
| 132 | + await this.clientTest.db('db').createCollection('coll', { validator: { $jsonSchema } }); |
| 133 | + |
| 134 | + this.clientEncryption = new mongodbClientEncryption.ClientEncryption(this.clientTest, { |
| 135 | + kmsProviders: { local: { key: LOCAL_KEY } }, |
| 136 | + keyVaultNamespace: 'keyvault.datakeys', |
| 137 | + keyVaultClient: this.keyVaultClient, |
| 138 | + extraOptions: getEncryptExtraOptions() |
| 139 | + }); |
| 140 | + this.clientEncryption.encryptPromisified = util.promisify( |
| 141 | + this.clientEncryption.encrypt.bind(this.clientEncryption) |
| 142 | + ); |
| 143 | + |
| 144 | + this.ciphertext = await this.clientEncryption.encryptPromisified('string0', { |
| 145 | + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic', |
| 146 | + keyAltName: 'local' |
| 147 | + }); |
| 148 | + }); |
| 149 | + |
| 150 | + afterEach(function () { |
| 151 | + return Promise.all([this.clientKeyVault.close(), this.clientTest.close()]).then(() => { |
| 152 | + this.clientKeyVault = undefined; |
| 153 | + this.clientTest = undefined; |
| 154 | + this.clientEncryption = undefined; |
| 155 | + }); |
| 156 | + }); |
| 157 | + |
| 158 | + const CASE1 = { maxPoolSize: 1, bypassAutoEncryption: false, useKeyVaultClient: false }; |
| 159 | + it( |
| 160 | + 'Case 1', |
| 161 | + metadata, |
| 162 | + deadlockTest(CASE1, clientEncrypted => { |
| 163 | + expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(2); |
| 164 | + |
| 165 | + const events = clientEncrypted.commandStartedEvents; |
| 166 | + expect(events).to.have.lengthOf(4); |
| 167 | + |
| 168 | + expect(events[0].command).to.have.property('listCollections'); |
| 169 | + expect(events[0].command.$db).to.equal('db'); |
| 170 | + |
| 171 | + expect(events[1].command).to.have.property('find'); |
| 172 | + expect(events[1].command.$db).to.equal('keyvault'); |
| 173 | + |
| 174 | + expect(events[2].command).to.have.property('insert'); |
| 175 | + expect(events[2].command.$db).to.equal('db'); |
| 176 | + |
| 177 | + expect(events[3].command).to.have.property('find'); |
| 178 | + expect(events[3].command.$db).to.equal('db'); |
| 179 | + }) |
| 180 | + ); |
| 181 | + |
| 182 | + const CASE2 = { maxPoolSize: 1, bypassAutoEncryption: false, useKeyVaultClient: true }; |
| 183 | + it( |
| 184 | + 'Case 2', |
| 185 | + metadata, |
| 186 | + deadlockTest(CASE2, (clientEncrypted, clientKeyVault) => { |
| 187 | + expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(2); |
| 188 | + |
| 189 | + const events = clientEncrypted.commandStartedEvents; |
| 190 | + expect(events).to.have.lengthOf(3); |
| 191 | + |
| 192 | + expect(events[0].command).to.have.property('listCollections'); |
| 193 | + expect(events[0].command.$db).to.equal('db'); |
| 194 | + |
| 195 | + expect(events[1].command).to.have.property('insert'); |
| 196 | + expect(events[1].command.$db).to.equal('db'); |
| 197 | + |
| 198 | + expect(events[2].command).to.have.property('find'); |
| 199 | + expect(events[2].command.$db).to.equal('db'); |
| 200 | + |
| 201 | + const keyVaultEvents = clientKeyVault.commandStartedEvents; |
| 202 | + expect(keyVaultEvents).to.have.lengthOf(1); |
| 203 | + |
| 204 | + expect(keyVaultEvents[0].command).to.have.property('find'); |
| 205 | + expect(keyVaultEvents[0].command.$db).to.equal('keyvault'); |
| 206 | + }) |
| 207 | + ); |
| 208 | + |
| 209 | + const CASE3 = { maxPoolSize: 1, bypassAutoEncryption: true, useKeyVaultClient: false }; |
| 210 | + it( |
| 211 | + 'Case 3', |
| 212 | + metadata, |
| 213 | + deadlockTest(CASE3, clientEncrypted => { |
| 214 | + expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(2); |
| 215 | + |
| 216 | + const events = clientEncrypted.commandStartedEvents; |
| 217 | + expect(events).to.have.lengthOf(2); |
| 218 | + |
| 219 | + expect(events[0].command).to.have.property('find'); |
| 220 | + expect(events[0].command.$db).to.equal('db'); |
| 221 | + |
| 222 | + expect(events[1].command).to.have.property('find'); |
| 223 | + expect(events[1].command.$db).to.equal('keyvault'); |
| 224 | + }) |
| 225 | + ); |
| 226 | + |
| 227 | + const CASE4 = { maxPoolSize: 1, bypassAutoEncryption: true, useKeyVaultClient: true }; |
| 228 | + it( |
| 229 | + 'Case 4', |
| 230 | + metadata, |
| 231 | + deadlockTest(CASE4, (clientEncrypted, clientKeyVault) => { |
| 232 | + expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(1); |
| 233 | + |
| 234 | + const events = clientEncrypted.commandStartedEvents; |
| 235 | + expect(events).to.have.lengthOf(1); |
| 236 | + |
| 237 | + expect(events[0].command).to.have.property('find'); |
| 238 | + expect(events[0].command.$db).to.equal('db'); |
| 239 | + |
| 240 | + const keyVaultEvents = clientKeyVault.commandStartedEvents; |
| 241 | + expect(keyVaultEvents).to.have.lengthOf(1); |
| 242 | + |
| 243 | + expect(keyVaultEvents[0].command).to.have.property('find'); |
| 244 | + expect(keyVaultEvents[0].command.$db).to.equal('keyvault'); |
| 245 | + }) |
| 246 | + ); |
| 247 | + |
| 248 | + const CASE5 = { maxPoolSize: 0, bypassAutoEncryption: false, useKeyVaultClient: false }; |
| 249 | + it( |
| 250 | + 'Case 5', |
| 251 | + metadata, |
| 252 | + deadlockTest(CASE5, clientEncrypted => { |
| 253 | + expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(1); |
| 254 | + |
| 255 | + const events = clientEncrypted.commandStartedEvents; |
| 256 | + expect(events).to.have.lengthOf(5); |
| 257 | + |
| 258 | + expect(events[0].command).to.have.property('listCollections'); |
| 259 | + expect(events[0].command.$db).to.equal('db'); |
| 260 | + |
| 261 | + expect(events[1].command).to.have.property('listCollections'); |
| 262 | + expect(events[1].command.$db).to.equal('keyvault'); |
| 263 | + |
| 264 | + expect(events[2].command).to.have.property('find'); |
| 265 | + expect(events[2].command.$db).to.equal('keyvault'); |
| 266 | + |
| 267 | + expect(events[3].command).to.have.property('insert'); |
| 268 | + expect(events[3].command.$db).to.equal('db'); |
| 269 | + |
| 270 | + expect(events[4].command).to.have.property('find'); |
| 271 | + expect(events[4].command.$db).to.equal('db'); |
| 272 | + }) |
| 273 | + ); |
| 274 | + |
| 275 | + const CASE6 = { maxPoolSize: 0, bypassAutoEncryption: false, useKeyVaultClient: true }; |
| 276 | + it( |
| 277 | + 'Case 6', |
| 278 | + metadata, |
| 279 | + deadlockTest(CASE6, (clientEncrypted, clientKeyVault) => { |
| 280 | + expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(1); |
| 281 | + |
| 282 | + const events = clientEncrypted.commandStartedEvents; |
| 283 | + expect(events).to.have.lengthOf(3); |
| 284 | + |
| 285 | + expect(events[0].command).to.have.property('listCollections'); |
| 286 | + expect(events[0].command.$db).to.equal('db'); |
| 287 | + |
| 288 | + expect(events[1].command).to.have.property('insert'); |
| 289 | + expect(events[1].command.$db).to.equal('db'); |
| 290 | + |
| 291 | + expect(events[2].command).to.have.property('find'); |
| 292 | + expect(events[2].command.$db).to.equal('db'); |
| 293 | + |
| 294 | + const keyVaultEvents = clientKeyVault.commandStartedEvents; |
| 295 | + expect(keyVaultEvents).to.have.lengthOf(1); |
| 296 | + |
| 297 | + expect(keyVaultEvents[0].command).to.have.property('find'); |
| 298 | + expect(keyVaultEvents[0].command.$db).to.equal('keyvault'); |
| 299 | + }) |
| 300 | + ); |
| 301 | + |
| 302 | + const CASE7 = { maxPoolSize: 0, bypassAutoEncryption: true, useKeyVaultClient: false }; |
| 303 | + it( |
| 304 | + 'Case 7', |
| 305 | + metadata, |
| 306 | + deadlockTest(CASE7, clientEncrypted => { |
| 307 | + expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(1); |
| 308 | + |
| 309 | + const events = clientEncrypted.commandStartedEvents; |
| 310 | + expect(events).to.have.lengthOf(2); |
| 311 | + |
| 312 | + expect(events[0].command).to.have.property('find'); |
| 313 | + expect(events[0].command.$db).to.equal('db'); |
| 314 | + |
| 315 | + expect(events[1].command).to.have.property('find'); |
| 316 | + expect(events[1].command.$db).to.equal('keyvault'); |
| 317 | + }) |
| 318 | + ); |
| 319 | + |
| 320 | + const CASE8 = { maxPoolSize: 0, bypassAutoEncryption: true, useKeyVaultClient: true }; |
| 321 | + it( |
| 322 | + 'Case 8', |
| 323 | + metadata, |
| 324 | + deadlockTest(CASE8, (clientEncrypted, clientKeyVault) => { |
| 325 | + expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(1); |
| 326 | + |
| 327 | + const events = clientEncrypted.commandStartedEvents; |
| 328 | + expect(events).to.have.lengthOf(1); |
| 329 | + |
| 330 | + expect(events[0].command).to.have.property('find'); |
| 331 | + expect(events[0].command.$db).to.equal('db'); |
| 332 | + |
| 333 | + const keyVaultEvents = clientKeyVault.commandStartedEvents; |
| 334 | + expect(keyVaultEvents).to.have.lengthOf(1); |
| 335 | + |
| 336 | + expect(keyVaultEvents[0].command).to.have.property('find'); |
| 337 | + expect(keyVaultEvents[0].command.$db).to.equal('keyvault'); |
| 338 | + }) |
| 339 | + ); |
| 340 | +}); |
0 commit comments