From aa36a9e8ad8c98e828f1b53966672b368d973380 Mon Sep 17 00:00:00 2001 From: Nick Zolnierz Date: Mon, 27 Aug 2018 16:59:22 -0400 Subject: SERVER-36081: Write auth tests for $out and bypassDocumentValidation --- jstests/aggregation/extras/utils.js | 6 +- .../sources/out/bypass_doc_validation.js | 135 +++++++++++++++++++++ jstests/auth/lib/commands_lib.js | 97 ++++++++++++++- 3 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 jstests/aggregation/sources/out/bypass_doc_validation.js diff --git a/jstests/aggregation/extras/utils.js b/jstests/aggregation/extras/utils.js index be246362ac8..bdc644d109e 100644 --- a/jstests/aggregation/extras/utils.js +++ b/jstests/aggregation/extras/utils.js @@ -238,7 +238,7 @@ function orderedArrayEq(al, ar, verbose = false) { /** * Asserts that the given aggregation fails with a specific code. Error message is optional. */ -function assertErrorCode(coll, pipe, code, errmsg) { +function assertErrorCode(coll, pipe, code, errmsg, options = {}) { if (!Array.isArray(pipe)) { pipe = [pipe]; } @@ -246,6 +246,10 @@ function assertErrorCode(coll, pipe, code, errmsg) { let cmd = {pipeline: pipe}; cmd.cursor = {batchSize: 0}; + for (let opt of Object.keys(options)) { + cmd[opt] = options[opt]; + } + let cursorRes = coll.runCommand("aggregate", cmd); if (cursorRes.ok) { let followupBatchSize = 0; // default diff --git a/jstests/aggregation/sources/out/bypass_doc_validation.js b/jstests/aggregation/sources/out/bypass_doc_validation.js new file mode 100644 index 00000000000..3b67647ef14 --- /dev/null +++ b/jstests/aggregation/sources/out/bypass_doc_validation.js @@ -0,0 +1,135 @@ +/** + * Tests for $out with bypassDocumentValidation. + * + * @tags: [assumes_unsharded_collection] + */ +(function() { + "use strict"; + + load("jstests/aggregation/extras/utils.js"); // For assertErrorCode. + + const testDB = db.getSiblingDB("out_bypass_doc_val"); + const sourceColl = testDB.getCollection("source"); + const targetColl = testDB.getCollection("target"); + + targetColl.drop(); + assert.commandWorked(testDB.createCollection(targetColl.getName(), {validator: {a: 2}})); + + sourceColl.drop(); + assert.commandWorked(sourceColl.insert({_id: 0, a: 1})); + + // Test that the bypassDocumentValidation flag is passed through to the writes on the output + // collection. + (function testBypassDocValidationTrue() { + sourceColl.aggregate([{$out: targetColl.getName()}], {bypassDocumentValidation: true}); + assert.eq([{_id: 0, a: 1}], targetColl.find().toArray()); + + sourceColl.aggregate([{$out: {to: targetColl.getName(), mode: "replaceCollection"}}], + {bypassDocumentValidation: true}); + assert.eq([{_id: 0, a: 1}], targetColl.find().toArray()); + + sourceColl.aggregate( + [{$addFields: {a: 3}}, {$out: {to: targetColl.getName(), mode: "replaceDocuments"}}], + {bypassDocumentValidation: true}); + assert.eq([{_id: 0, a: 3}], targetColl.find().toArray()); + + sourceColl.aggregate( + [ + {$replaceRoot: {newRoot: {_id: 1, a: 4}}}, + {$out: {to: targetColl.getName(), mode: "insertDocuments"}} + ], + {bypassDocumentValidation: true}); + assert.eq([{_id: 0, a: 3}, {_id: 1, a: 4}], targetColl.find().sort({_id: 1}).toArray()); + }()); + + // Test that mode "replaceDocuments" passes without the bypassDocumentValidation flag if the + // updated doc is valid. + (function testReplacementStyleUpdateWithoutBypass() { + sourceColl.aggregate( + [{$addFields: {a: 2}}, {$out: {to: targetColl.getName(), mode: "replaceDocuments"}}]); + assert.eq([{_id: 0, a: 2}], targetColl.find({_id: 0}).toArray()); + sourceColl.aggregate( + [{$addFields: {a: 2}}, {$out: {to: targetColl.getName(), mode: "replaceDocuments"}}], + {bypassDocumentValidation: false}); + assert.eq([{_id: 0, a: 2}], targetColl.find({_id: 0}).toArray()); + }()); + + function assertDocValidationFailure(cmdOptions) { + targetColl.remove({}); + assertErrorCode(sourceColl, + [{$out: targetColl.getName()}], + ErrorCodes.DocumentValidationFailure, + "Expected failure without bypass set", + cmdOptions); + + assertErrorCode(sourceColl, + [{$out: {to: targetColl.getName(), mode: "replaceCollection"}}], + ErrorCodes.DocumentValidationFailure, + "Expected failure without bypass set", + cmdOptions); + + assertErrorCode( + sourceColl, + [{$addFields: {a: 3}}, {$out: {to: targetColl.getName(), mode: "replaceDocuments"}}], + ErrorCodes.DocumentValidationFailure, + "Expected failure without bypass set", + cmdOptions); + + assertErrorCode(sourceColl, + [ + {$replaceRoot: {newRoot: {_id: 1, a: 4}}}, + {$out: {to: targetColl.getName(), mode: "insertDocuments"}} + ], + ErrorCodes.DocumentValidationFailure, + "Expected failure without bypass set", + cmdOptions); + assert.eq(0, targetColl.find().itcount()); + } + + // Test that $out fails if the output document is not valid, and the bypassDocumentValidation + // flag is not set. + assertDocValidationFailure({}); + + // Test that $out fails if the output document is not valid, and the bypassDocumentValidation + // flag is explicitly set to false. + assertDocValidationFailure({bypassDocumentValidation: false}); + + // Test that bypassDocumentValidation is *not* needed if the source collection has a + // validator but the output collection does not. + (function testDocValidatorOnSourceCollection() { + targetColl.drop(); + assert.commandWorked(testDB.runCommand({collMod: sourceColl.getName(), validator: {a: 1}})); + + sourceColl.aggregate([{$out: targetColl.getName()}]); + assert.eq([{_id: 0, a: 1}], targetColl.find().toArray()); + + sourceColl.aggregate([{$out: {to: targetColl.getName(), mode: "replaceCollection"}}]); + assert.eq([{_id: 0, a: 1}], targetColl.find().toArray()); + + sourceColl.aggregate( + [{$addFields: {a: 3}}, {$out: {to: targetColl.getName(), mode: "replaceDocuments"}}]); + assert.eq([{_id: 0, a: 3}], targetColl.find().toArray()); + + sourceColl.aggregate([ + {$replaceRoot: {newRoot: {_id: 1, a: 4}}}, + {$out: {to: targetColl.getName(), mode: "insertDocuments"}} + ]); + assert.eq([{_id: 0, a: 3}, {_id: 1, a: 4}], targetColl.find().sort({_id: 1}).toArray()); + }()); + + // Test that the bypassDocumentValidation is casted to true if the value is non-boolean. + (function testNonBooleanBypassDocValidationFlag() { + targetColl.remove({}); + assert.commandWorked(testDB.runCommand({collMod: targetColl.getName(), validator: {a: 1}})); + sourceColl.drop(); + assert.commandWorked(sourceColl.insert({_id: 0, a: 1})); + + sourceColl.aggregate([{$out: targetColl.getName()}], {bypassDocumentValidation: 5}); + assert.eq([{_id: 0, a: 1}], targetColl.find().toArray()); + + sourceColl.aggregate( + [{$addFields: {a: 3}}, {$out: {to: targetColl.getName(), mode: "replaceDocuments"}}], + {bypassDocumentValidation: "false"}); + assert.eq([{_id: 0, a: 3}], targetColl.find().toArray()); + }()); +}()); diff --git a/jstests/auth/lib/commands_lib.js b/jstests/auth/lib/commands_lib.js index cbe3891be69..c50fc1c9d99 100644 --- a/jstests/auth/lib/commands_lib.js +++ b/jstests/auth/lib/commands_lib.js @@ -6,9 +6,8 @@ of all database commands. This file contains an array of test definitions, as well as some shared test logic. -See jstests/auth/commands_builtinRoles.js and -jstests/auth/commands_userDefinedRoles.js for two separate implementations -of the test logic, respectively to test authorization with builtin roles +See jstests/auth/commands_builtin_roles.js and jstests/auth/commands_user_defined_roles.js for two +separate implementations of the test logic, respectively to test authorization with builtin roles and authorization with user-defined roles. Example test definition: @@ -1157,6 +1156,98 @@ var authCommandsLib = { }, ] }, + { + testname: "aggregate_out_legacy_bypass_doc_validation", + command: { + aggregate: "foo", + pipeline: [{$out: "foo_out"}], + cursor: {}, + bypassDocumentValidation: true, + }, + testcases: [ + { + runOnDb: firstDbName, + // Note that the built-in role must have 'bypassDocumentValidation' for this test. + roles: {dbOwner: 1, root: 1, __system: 1}, + privileges: [ + {resource: {db: firstDbName, collection: "foo"}, actions: ["find"]}, + { + resource: {db: firstDbName, collection: "foo_out"}, + actions: ["insert", "remove", "bypassDocumentValidation"] + }, + ] + }, + ] + }, + { + testname: "aggregate_out_replace_collection_bypass_doc_validation", + command: { + aggregate: "foo", + pipeline: [{$out: {to: "foo_out", mode: "replaceCollection"}}], + cursor: {}, + bypassDocumentValidation: true, + }, + testcases: [ + { + runOnDb: firstDbName, + // Note that the built-in role must have 'bypassDocumentValidation' for this test. + roles: {dbOwner: 1, root: 1, __system: 1}, + privileges: [ + {resource: {db: firstDbName, collection: "foo"}, actions: ["find"]}, + { + resource: {db: firstDbName, collection: "foo_out"}, + actions: ["insert", "remove", "bypassDocumentValidation"] + }, + ] + }, + ] + }, + { + testname: "aggregate_out_replace_documents_bypass_doc_validation", + command: { + aggregate: "foo", + pipeline: [{$out: {to: "foo_out", mode: "replaceDocuments"}}], + cursor: {}, + bypassDocumentValidation: true, + }, + testcases: [ + { + runOnDb: firstDbName, + // Note that the built-in role must have 'bypassDocumentValidation' for this test. + roles: {dbOwner: 1, root: 1, __system: 1}, + privileges: [ + {resource: {db: firstDbName, collection: "foo"}, actions: ["find"]}, + { + resource: {db: firstDbName, collection: "foo_out"}, + actions: ["insert", "update", "bypassDocumentValidation"] + }, + ] + }, + ] + }, + { + testname: "aggregate_out_insert_documents_bypass_doc_validation", + command: { + aggregate: "foo", + pipeline: [{$out: {to: "foo_out", mode: "insertDocuments"}}], + cursor: {}, + bypassDocumentValidation: true, + }, + testcases: [ + { + runOnDb: firstDbName, + // Note that the built-in role must have 'bypassDocumentValidation' for this test. + roles: {dbOwner: 1, root: 1, __system: 1}, + privileges: [ + {resource: {db: firstDbName, collection: "foo"}, actions: ["find"]}, + { + resource: {db: firstDbName, collection: "foo_out"}, + actions: ["insert", "bypassDocumentValidation"] + }, + ] + }, + ] + }, { testname: "aggregate_readView_writeCollection", setup: function(db) { -- cgit v1.2.1