summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorA. Jesse Jiryu Davis <jesse@mongodb.com>2019-04-19 13:52:12 -0400
committerA. Jesse Jiryu Davis <jesse@mongodb.com>2019-04-24 10:47:53 -0400
commitf202c4c1ba24b9f561e8b11dac5b04fa0eeb4919 (patch)
treeceedc4c78d52590629e81e3aa77bec774dab27a1
parent66fcbb20e58550e652dd95449c696f17ad2f9ce2 (diff)
downloadmongo-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.js51
-rw-r--r--jstests/core/txns/listcollections_autocomplete.js59
-rw-r--r--src/mongo/shell/db.js21
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_.\$]+$/))