diff options
author | A. Jesse Jiryu Davis <jesse@mongodb.com> | 2019-04-19 13:52:12 -0400 |
---|---|---|
committer | A. Jesse Jiryu Davis <jesse@mongodb.com> | 2019-04-24 10:47:53 -0400 |
commit | f202c4c1ba24b9f561e8b11dac5b04fa0eeb4919 (patch) | |
tree | ceedc4c78d52590629e81e3aa77bec774dab27a1 | |
parent | 66fcbb20e58550e652dd95449c696f17ad2f9ce2 (diff) | |
download | mongo-f202c4c1ba24b9f561e8b11dac5b04fa0eeb4919.tar.gz |
SERVER-35638 Short timeout to autocomplete collection names
Also resolves SERVER-40736, test autocompletion of collection names for users
without the listCollections permission.
-rw-r--r-- | jstests/auth/autocomplete_auth.js | 51 | ||||
-rw-r--r-- | jstests/core/txns/listcollections_autocomplete.js | 59 | ||||
-rw-r--r-- | src/mongo/shell/db.js | 21 |
3 files changed, 124 insertions, 7 deletions
diff --git a/jstests/auth/autocomplete_auth.js b/jstests/auth/autocomplete_auth.js new file mode 100644 index 00000000000..5e15cae3718 --- /dev/null +++ b/jstests/auth/autocomplete_auth.js @@ -0,0 +1,51 @@ +/** + * Tests that when a user who lacks the listCollections privilege types 'db.<tab>' in the shell, + * autocompletion shows the collections on which she has permissions. + * + * @tags: [ + * assumes_superuser_permissions, + * assumes_write_concern_unchanged, + * creates_and_authenticates_user, + * requires_auth, + * requires_non_retryable_commands, + * ] + */ + +// Get shell's global scope. +const self = this; + +(function() { + 'use strict'; + + const testName = jsTest.name(); + const conn = MongoRunner.runMongod({auth: ''}); + const admin = conn.getDB('admin'); + admin.createUser({user: 'admin', pwd: 'pass', roles: jsTest.adminUserRoles}); + assert(admin.auth('admin', 'pass')); + + admin.getSiblingDB(testName).createRole({ + role: 'coachTicket', + privileges: [{resource: {db: testName, collection: 'coachClass'}, actions: ['find']}], + roles: [] + }); + + admin.getSiblingDB(testName).createUser( + {user: 'coachPassenger', pwd: 'password', roles: ['coachTicket']}); + + const testDB = conn.getDB(testName); + testDB.coachClass.insertOne({}); + testDB.businessClass.insertOne({}); + + // Must use 'db' to test autocompletion. + self.db = new Mongo(conn.host).getDB(testName); + assert(db.auth('coachPassenger', 'password')); + const authzErrorCode = 13; + assert.commandFailedWithCode(db.runCommand({listCollections: 1}), authzErrorCode); + assert.commandWorked(db.runCommand({find: 'coachClass'})); + assert.commandFailedWithCode(db.runCommand({find: 'businessClass'}), authzErrorCode); + shellAutocomplete('db.'); + assert(__autocomplete__.includes('db.coachClass'), + `Completions should include 'coachClass': ${__autocomplete__}`); + assert(!__autocomplete__.includes('db.businessClass'), + `Completions should NOT include 'businessClass': ${__autocomplete__}`); +})(); diff --git a/jstests/core/txns/listcollections_autocomplete.js b/jstests/core/txns/listcollections_autocomplete.js new file mode 100644 index 00000000000..cf9196a4ff1 --- /dev/null +++ b/jstests/core/txns/listcollections_autocomplete.js @@ -0,0 +1,59 @@ +/** + * Auto complete returns quickly if listCollections is blocked by the transaction lock. + * + * @tags: [uses_transactions] + */ +(function() { + 'use strict'; + + function testAutoComplete() { + // This method updates a global object with an array of strings on success. + assert.soon(() => { + shellAutocomplete("db."); + return true; + }, null, 30 * 1000); + return __autocomplete__; + } + + // Create a collection. + const collName = 'listcollections_autocomplete'; + assert.commandWorked(db[collName].insertOne({}, {writeConcern: {w: 'majority'}})); + + jsTestLog("Start transaction"); + + const session = db.getMongo().startSession(); + const sessionDb = session.getDatabase('test'); + const sessionColl = sessionDb[collName]; + session.startTransaction_forTesting(); + assert.commandWorked(sessionColl.insertOne({})); + + jsTestLog("Start dropDatabase in parallel shell"); + + // Wait for global X lock while blocked behind transaction with global IX lock. + var awaitShell = startParallelShell(function() { + db.getSiblingDB("test2").dropDatabase(); + }); + + jsTestLog("Wait for dropDatabase to appear in currentOp"); + + assert.soon(() => { + return db.currentOp({'command.dropDatabase': 1}).inprog; + }); + + jsTestLog("Test that autocompleting collection names fails quickly"); + + let db_stuff = testAutoComplete(); + assert(!db_stuff.includes(collName), + `Completions should not include "${collName}": ${db_stuff}`); + + // Verify we have some results despite the timeout. + assert.contains('db.adminCommand(', db_stuff); + + jsTestLog("Abort transaction autocomplete collection names"); + + assert.commandWorked(session.abortTransaction_forTesting()); + awaitShell(); + db_stuff = testAutoComplete(); + assert.contains('db.adminCommand(', db_stuff); + assert.contains(`db.${collName}`, db_stuff); +})(); diff --git a/src/mongo/shell/db.js b/src/mongo/shell/db.js index d12e872e417..43123fd9361 100644 --- a/src/mongo/shell/db.js +++ b/src/mongo/shell/db.js @@ -816,14 +816,16 @@ var DB; DB.prototype.getLastErrorCmd = DB.prototype.getLastErrorObj; DB.prototype._getCollectionInfosCommand = function( - filter, nameOnly = false, authorizedCollections = false) { + filter, nameOnly = false, authorizedCollections = false, options = {}) { filter = filter || {}; - var res = this.runCommand({ + const cmd = { listCollections: 1, filter: filter, nameOnly: nameOnly, authorizedCollections: authorizedCollections - }); + }; + + const res = this.runCommand(Object.merge(cmd, options)); if (!res.ok) { throw _getErrorWithCode(res, "listCollections failed: " + tojson(res)); } @@ -898,13 +900,17 @@ var DB; } }; + DB.prototype._getCollectionNamesInternal = function(options) { + return this._getCollectionInfosCommand({}, true, true, options).map(function(infoObj) { + return infoObj.name; + }); + }; + /** * Returns this database's list of collection names in sorted order. */ DB.prototype.getCollectionNames = function() { - return this.getCollectionInfos({}, true, true).map(function(infoObj) { - return infoObj.name; - }); + return this._getCollectionNamesInternal({}); }; DB.prototype.tojson = function() { @@ -1249,7 +1255,8 @@ var DB; }; DB.autocomplete = function(obj) { - var colls = obj.getCollectionNames(); + // Time out if a transaction or other op holds locks we need. Caller suppresses exceptions. + var colls = obj._getCollectionNamesInternal({maxTimeMS: 1000}); var ret = []; for (var i = 0; i < colls.length; i++) { if (colls[i].match(/^[a-zA-Z0-9_.\$]+$/)) |