// Test setUserWriteBlockMode command. // // @tags: [ // creates_and_authenticates_user, // requires_auth, // requires_fcv_60, // requires_non_retryable_commands, // requires_persistence, // requires_replication, // ] import {UserWriteBlockHelpers} from "jstests/noPassthrough/libs/user_write_blocking.js"; const { WriteBlockState, ShardingFixture, ReplicaFixture, bypassUser, noBypassUser, password, keyfile } = UserWriteBlockHelpers; // For this test to work, we expect the state of the connection to be maintained as: // One db: "testSetUserWriteBlockMode1" // Three collections: // * set_user_write_block_mode_coll1 // * set_user_write_block_mode_coll2 // * set_user_write_block_mode_coll3 // One index: "index" w/ pattern {"b": 1} on db.coll1 // One document: {a: 0, b: 0} on db.coll1 const dbName = "testSetUserWriteBlockMode1"; const coll1Name = jsTestName() + "_coll1"; const coll2Name = jsTestName() + "_coll2"; const coll3Name = jsTestName() + "_coll3"; const indexName = "index"; function setupForTesting(conn) { const db = conn.getDB(dbName); assert.commandWorked(db.createCollection(coll1Name)); assert.commandWorked(db.createCollection(coll2Name)); const coll1 = db[coll1Name]; coll1.insert({a: 0, b: 0}); coll1.createIndex({"b": 1}, {"name": indexName}); } function testCheckedOps(conn, shouldSucceed, expectedFailure) { const transientDbName = "transientDB"; const transientCollNames = ["tc0", "tc1", "tc2"]; const transientIndexName = "transientIndex"; const db = conn.getDB(dbName); const coll1 = db[coll1Name]; const coll2 = db[coll2Name]; // Ensure we successfully maintained state from last run. function assertState() { assert(Array.contains(conn.getDBNames(), db.getName())); assert(!Array.contains(conn.getDBNames(), transientDbName)); assert(Array.contains(db.getCollectionNames(), coll1.getName())); assert(Array.contains(db.getCollectionNames(), coll2.getName())); for (let tName of transientCollNames) { assert(!Array.contains(db.getCollectionNames(), tName)); } const indexes = coll1.getIndexes(); assert.eq(undefined, indexes.find(i => i.name === transientIndexName)); assert.neq(undefined, indexes.find(i => i.name === indexName)); assert.eq(1, coll1.find({a: 0, b: 0}).count()); assert.eq(0, coll1.find({a: 1}).count()); } assertState(); if (shouldSucceed) { // Test CUD assert.commandWorked(coll1.insert({a: 1})); assert.eq(1, coll1.find({a: 1}).count()); assert.commandWorked(coll1.update({a: 1}, {a: 1, b: 2})); assert.eq(1, coll1.find({a: 1, b: 2}).count()); assert.commandWorked(coll1.remove({a: 1})); // Test create index on empty and non-empty colls, collMod, drop index. assert.commandWorked(coll1.createIndex({"a": 1}, {"name": transientIndexName})); assert.commandWorked(db.runCommand( {collMod: coll1Name, "index": {"keyPattern": {"a": 1}, expireAfterSeconds: 200}})); assert.commandWorked(coll1.dropIndex({"a": 1})); assert.commandWorked(coll2.createIndex({"a": 1}, {"name": transientIndexName})); assert.commandWorked(coll2.dropIndex({"a": 1})); // Test create, rename (both to a non-existent and an existing target), drop collection. assert.commandWorked(db.createCollection(transientCollNames[0])); assert.commandWorked(db.createCollection(transientCollNames[1])); assert.commandWorked(db[transientCollNames[0]].renameCollection(transientCollNames[2])); assert.commandWorked( db[transientCollNames[2]].renameCollection(transientCollNames[1], true)); assert(db[transientCollNames[1]].drop()); // Test dropping a (non-empty) database. const transientDb = conn.getDB(transientDbName); assert.commandWorked(transientDb.createCollection("coll")); assert.commandWorked(transientDb.dropDatabase()); } else { // Test CUD assert.commandFailedWithCode(coll1.insert({a: 1}), expectedFailure); assert.commandFailedWithCode(coll1.update({a: 0, b: 0}, {a: 1}), expectedFailure); assert.commandFailedWithCode(coll1.remove({a: 0, b: 0}), expectedFailure); // Test create, collMod, drop index. assert.commandFailedWithCode(coll1.createIndex({"a": 1}, {"name": transientIndexName}), expectedFailure); assert.commandFailedWithCode( db.runCommand( {collMod: coll1Name, "index": {"keyPattern": {"b": 1}, expireAfterSeconds: 200}}), expectedFailure); assert.commandFailedWithCode(coll1.dropIndex({"b": 1}), expectedFailure); assert.commandFailedWithCode(coll2.createIndex({"a": 1}, {"name": transientIndexName}), expectedFailure); // Test create, rename (both to a non-existent and an existing target), drop collection. assert.commandFailedWithCode(db.createCollection(transientCollNames[0]), expectedFailure); assert.commandFailedWithCode(coll2.renameCollection(transientCollNames[1]), expectedFailure); assert.commandFailedWithCode(coll2.renameCollection(coll1Name, true), expectedFailure); assert.commandFailedWithCode(db.runCommand({drop: coll2Name}), expectedFailure); // Test dropping a database. assert.commandFailedWithCode(db.dropDatabase(), expectedFailure); } // Ensure we successfully maintained state on this run. assertState(); } // Checks that an unprivileged user's operations can be logged on the profiling collection. function testProfiling(fixture) { const collName = 'foo'; fixture.asAdmin(({db}) => { assert.commandWorked(db[collName].insert({x: 1})); }); // Enable profiling. const prevProfilingLevel = fixture.setProfilingLevel(2).was; // Perform a find() as an unprivileged user. const comment = UUID(); fixture.asUser(({db}) => { db[collName].find().comment(comment).itcount(); }); // Check that the find() was logged on the profiling collection. fixture.asAdmin(({db}) => { assert.eq(1, db.system.profile.find({'command.comment': comment}).itcount()); }); // Restore the original profiling level. fixture.setProfilingLevel(prevProfilingLevel); } function runTest(fixture) { fixture.asAdmin(({conn}) => setupForTesting(conn)); fixture.assertWriteBlockMode(WriteBlockState.DISABLED); // Ensure that without setUserWriteBlockMode, both users are privileged for CUD ops fixture.asAdmin(({conn}) => testCheckedOps(conn, true)); fixture.asUser(({conn}) => { testCheckedOps(conn, true); // Ensure that the non-privileged user cannot run setUserWriteBlockMode assert.commandFailedWithCode( conn.getDB('admin').runCommand({setUserWriteBlockMode: 1, global: true}), ErrorCodes.Unauthorized); }); fixture.assertWriteBlockMode(WriteBlockState.DISABLED); fixture.enableWriteBlockMode(); fixture.assertWriteBlockMode(WriteBlockState.ENABLED); // Now with setUserWriteBlockMode enabled, ensure that only the bypassUser can CUD fixture.asAdmin(({conn}) => testCheckedOps(conn, true)); fixture.asUser(({conn}) => testCheckedOps(conn, false, ErrorCodes.UserWritesBlocked)); // Ensure that attempting to enabling write blocking again is a no-op under various // circumstances fixture.enableWriteBlockMode(); fixture.assertWriteBlockMode(WriteBlockState.ENABLED); fixture.stepDown(); fixture.enableWriteBlockMode(); fixture.assertWriteBlockMode(WriteBlockState.ENABLED); // Ensure that profiling works while user writes are blocked. testProfiling(fixture); // Restarting the cluster has no impact, as write block state is durable fixture.restart(); fixture.assertWriteBlockMode(WriteBlockState.ENABLED); fixture.asAdmin(({conn}) => { testCheckedOps(conn, true); }); fixture.asUser(({conn}) => { testCheckedOps(conn, false, ErrorCodes.UserWritesBlocked); }); // Now disable userWriteBlockMode and ensure both users can CUD again fixture.disableWriteBlockMode(); fixture.assertWriteBlockMode(WriteBlockState.DISABLED); fixture.asAdmin(({conn}) => testCheckedOps(conn, true)); fixture.asUser(({conn}) => testCheckedOps(conn, true)); // Test that enabling write blocking while there is an active index build on a user collection // (i.e. non-internal) will cause the index build to fail. fixture.asUser(({conn}) => { const db = conn.getDB(jsTestName()); assert.commandWorked(db.createCollection(coll3Name)); assert.commandWorked(db[coll3Name].insert({"a": 2})); }); fixture.asAdmin(({conn}) => { // We use config.system.sessions because it is a collection in an internal DB (config) which // is sharded, meaning index builds will be handled by the shard servers. Indexes on // non-sharded collections in internal DBs are built by the config server, which doesn't // have the UserWriteBlockModeOpObserver installed. const config = conn.getDB('config'); assert.commandWorked(config.system.sessions.insert({"a": 2})); }); const testParallelShellWithFailpoint = makeParallelShell => { const fp = fixture.setFailPoint('hangAfterInitializingIndexBuild'); const shell = makeParallelShell(); fp.wait(); fixture.enableWriteBlockMode(); fp.off(); shell(); fixture.disableWriteBlockMode(); }; const indexName = "testIndex"; // Test that index builds on user collections spawned by both non-privileged and privileged // users will be aborted on enableWriteBlockMode. testParallelShellWithFailpoint(() => fixture.runInParallelShell(false /* asAdmin */, `({conn}) => { assert.commandFailedWithCode( conn.getDB(jsTestName()).${coll3Name}.createIndex({"a": 1}, {"name": "${indexName}"}), ErrorCodes.IndexBuildAborted); }`)); testParallelShellWithFailpoint(() => fixture.runInParallelShell(true /* asAdmin */, `({conn}) => { assert.commandFailedWithCode( conn.getDB(jsTestName()).${coll3Name}.createIndex({"a": 1}, {"name": "${indexName}"}), ErrorCodes.IndexBuildAborted); }`)); // Test that index builds on non-user (internal collections) won't be aborted on // enableWriteBlockMode. testParallelShellWithFailpoint(() => fixture.runInParallelShell(true /* asAdmin */, `({conn}) => { assert.commandWorked( conn.getDB('config').system.sessions.createIndex( {"a": 1}, {"name": "${indexName}"})); }`)); // Ensure index was not successfully created on user db, but was on internal db. fixture.asAdmin(({conn}) => { assert.eq(undefined, conn.getDB(jsTestName()).coll3Name.getIndexes().find(i => i.name === indexName)); assert.neq( undefined, conn.getDB('config').system.sessions.getIndexes().find(i => i.name === indexName)); }); // Test that index builds which hang before commit will block activation of // enableWriteBlockMode. { const fp = fixture.setFailPoint("hangIndexBuildBeforeCommit"); const waitIndexBuild = fixture.runInParallelShell(true /* asAdmin */, `({conn}) => { assert.commandWorked( conn.getDB(jsTestName()).${coll3Name}.createIndex({"a": 1}, {"name": "${indexName}"})); }`); fp.wait(); const waitWriteBlock = fixture.runInParallelShell(true /* asAdmin */, `({conn}) => { assert.commandWorked( conn.getDB("admin").runCommand({setUserWriteBlockMode: 1, global: true})); }`); // Wait, and ensure that the setUserWriteBlockMode has not finished yet (it must wait for // the index build to finish). sleep(3000); fixture.assertWriteBlockMode(UserWriteBlockHelpers.WriteBlockState.DISABLED); fp.off(); waitIndexBuild(); waitWriteBlock(); fixture.assertWriteBlockMode(UserWriteBlockHelpers.WriteBlockState.ENABLED); fixture.disableWriteBlockMode(); } // Validate that temporary collections are allowed to be dropped during step up if user writes // are blocked. { // Create a temporary collection. const collTmpName = "collTmp"; fixture.applyOps( [{op: "c", ns: jsTestName() + ".$cmd", o: {create: collTmpName, temp: true}}]); // Validate that collection exists and it is marked as temporary. fixture.asUser(({conn}) => { const db = conn.getDB(jsTestName()); const collectionInfo = db.getCollectionInfos().find(info => info.name === collTmpName); assert(collectionInfo); assert(collectionInfo.options.temp, 'The collection is not marked as a temporary one: ' + tojson(collectionInfo)); }); // Enable user write block mode and force a stepdown. fixture.enableWriteBlockMode(); fixture.stepDown(); // Validate that temporary collections are dropped during startup recovery. fixture.asUser(({conn}) => { const db = conn.getDB(jsTestName()); const collectionExists = db.getCollectionInfos().some(info => info.name === collTmpName); assert(!collectionExists); }); fixture.disableWriteBlockMode(); } if (fixture.takeGlobalLock) { // Test that serverStatus will produce WriteBlockState.UNKNOWN when the global lock is held. let globalLock = fixture.takeGlobalLock(); try { fixture.assertWriteBlockMode(WriteBlockState.UNKNOWN); } finally { globalLock.unlock(); } } } { // Validate that setting user write blocking fails on standalones const conn = MongoRunner.runMongod({auth: "", bind_ip: "127.0.0.1"}); const admin = conn.getDB("admin"); assert.commandWorked(admin.runCommand( {createUser: "root", pwd: "root", roles: [{role: "__system", db: "admin"}]})); assert(admin.auth("root", "root")); assert.commandFailedWithCode(admin.runCommand({setUserWriteBlockMode: 1, global: true}), ErrorCodes.IllegalOperation); MongoRunner.stopMongod(conn); } // Test on a replset const rst = new ReplicaFixture(); runTest(rst); rst.stop(); // Test on a sharded cluster // By default, our test infrastructure sets the election timeout to a very high value (24 // hours). For this test, we need a shorter election timeout because it relies on nodes // running an election when they do not detect an active primary after restarting nodes. Therefore, // we are setting the electionTimeoutMillis to its default value. const st = new ShardingFixture(true /*initiateWithDefaultElectionTimeout*/); runTest(st); st.stop();