/** * Test that the coordinateCommitTransaction command can only be run when * authorized to do so. * */ import {ReplSetTest} from "jstests/libs/replsettest.js"; import {ShardingTest} from "jstests/libs/shardingtest.js"; const keyFile = "jstests/libs/key1"; const collName = "mycoll"; const id = UUID("eeeeeeee-eeee-1111-0000-eeeeeeeeeeee"); /** * userConnection represent the connection that a user has to the cluster. * directConnection represents the connection the malicious user has, which * is directly to a shard in a sharded cluster. * Both connections are the same when run on a replica set. */ function testCoordinateCommitTransactionFails(userConnection, directConnection) { jsTestLog("Create a 'good' user who is trying to run a transaction via mongos."); const userTestDB = userConnection.getDB("test"); authutil.asCluster(userConnection, keyFile, () => { userTestDB.getSiblingDB("admin").createUser({user: "root", pwd: "root", roles: ["root"]}); userTestDB.getSiblingDB("admin").createUser({user: "ro", pwd: "ro", roles: ["read"]}); }); assert.eq(1, userTestDB.getSiblingDB("admin").auth("root", "root")); // In the replica set case, since both connections are the same, we've already // created the users. So we don't create the users again. if (userConnection != directConnection) { // The root user is to simulate someone who leaks the LSID. jsTestLog("Create root user and malicious read-only user on the shard."); const testDB = directConnection.getDB("test"); authutil.asCluster(directConnection, keyFile, () => { testDB.getSiblingDB("admin").createUser({user: "ro", pwd: "ro", roles: ["read"]}); testDB.getSiblingDB("admin").createUser({user: "root", pwd: "root", roles: ["root"]}); }); } jsTestLog("A user with read-only permissions can't run coordinateCommitTransaction:"); const connRO = new Mongo(directConnection.host); const testDBRO = connRO.getDB("test"); assert.eq(1, testDBRO.getSiblingDB("admin").auth("ro", "ro")); assert.commandFailedWithCode( testDBRO.adminCommand({ coordinateCommitTransaction: 1, participants: [], }), ErrorCodes.Unauthorized, ); // Insert a document and create the collection because we can't create collections in a // cross-shard transaction. assert.commandWorked(userTestDB[collName].insert({a: -1})); // Next we run one transaction to completion. This is so that config.transactions gets // populated. From the config.transactions entry we will figure out what the uid component of // the lsid is. assert.commandWorked( userTestDB.runCommand({ insert: collName, documents: [{a: 0}], startTransaction: true, lsid: {"id": id}, txnNumber: NumberLong(0), autocommit: false, }), ); assert.commandWorked( userTestDB.adminCommand({ commitTransaction: 1, lsid: {"id": id}, txnNumber: NumberLong(0), autocommit: false, }), ); // Now we start a second transaction, which we will attempt to commit through the malicious user // via the coordinateCommitTransaction command. jsTestLog("Good user starts a second transaction:"); assert.commandWorked( userTestDB.runCommand({ insert: collName, documents: [{a: 1}], startTransaction: true, lsid: {"id": id}, txnNumber: NumberLong(1), autocommit: false, }), ); jsTestLog("Trying to get the uid of user."); const directRootConn = new Mongo(directConnection.host); const directRootTestDB = directRootConn.getDB("test"); assert.eq(1, directRootTestDB.getSiblingDB("admin").auth("root", "root")); let txnEntry = directRootTestDB.getSiblingDB("config").transactions.findOne({"_id.id": id}); jsTestLog("config.transactions entry: " + tojson(txnEntry)); assert(txnEntry._id.uid); jsTestLog("Sending coordinateCommitTransaction when impersonating lsid (id+uid) should fail:"); const readOnlyConn = new Mongo(directConnection.host); const readOnlyTestDB = readOnlyConn.getDB("test"); assert.eq(1, readOnlyTestDB.getSiblingDB("admin").auth("ro", "ro")); const res = assert.commandFailedWithCode( readOnlyTestDB.adminCommand({ coordinateCommitTransaction: 1, lsid: { id: id, uid: txnEntry._id.uid, }, txnNumber: NumberLong(1), autocommit: false, participants: [], }), ErrorCodes.Unauthorized, ); assert(res.errmsg.includes("Unauthorized to set user digest"), res); } jsTestLog("Running test on replica set."); const rs = new ReplSetTest({nodes: 1, setParameter: {logComponentVerbosity: {transaction: 4}}, keyFile: keyFile}); rs.startSet(); rs.initiate(); testCoordinateCommitTransactionFails(rs.getPrimary(), rs.getPrimary()); rs.stopSet(); jsTestLog("Running test on sharded cluster."); const st = new ShardingTest({ shards: 1, rs: {nodes: 1, setParameter: {logComponentVerbosity: {transaction: 4}}}, keyFile: keyFile, }); testCoordinateCommitTransactionFails(st.s, st._connections[0]); st.stop();