summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--jstests/auth/drop-user-transaction.js136
-rw-r--r--src/mongo/db/commands/user_management_commands.cpp293
-rw-r--r--src/mongo/db/commands/user_management_commands.idl8
-rw-r--r--src/mongo/db/commands/write_commands/write_commands.cpp2
4 files changed, 358 insertions, 81 deletions
diff --git a/jstests/auth/drop-user-transaction.js b/jstests/auth/drop-user-transaction.js
new file mode 100644
index 00000000000..e0c09251528
--- /dev/null
+++ b/jstests/auth/drop-user-transaction.js
@@ -0,0 +1,136 @@
+// Validate dropUser performed via transaction.
+
+(function() {
+'use strict';
+
+function runTest(conn, testCB) {
+ const admin = conn.getDB('admin');
+ const test = conn.getDB('test');
+ admin.createUser({user: 'admin', pwd: 'pwd', roles: ['__system']});
+ admin.auth('admin', 'pwd');
+
+ // user1 -> role2 -> role1
+ // \___________.^
+ assert.commandWorked(test.runCommand({createRole: 'role1', roles: [], privileges: []}));
+ assert.commandWorked(test.runCommand({createRole: 'role2', roles: ['role1'], privileges: []}));
+ assert.commandWorked(
+ test.runCommand({createUser: 'user1', roles: ['role1', 'role2'], pwd: 'pwd'}));
+
+ const beforeDrop = assert.commandWorked(test.runCommand({usersInfo: 'user1'})).users[0].roles;
+ assert.eq(beforeDrop.length, 2);
+ assert.eq(beforeDrop.map((r) => r.role).sort(), ['role1', 'role2']);
+
+ testCB(test);
+
+ // Callback should end up dropping role1
+ // And we should have no references left to it.
+ const allUsers = assert.commandWorked(test.runCommand({usersInfo: 1})).users;
+ assert.eq(allUsers.length, 1);
+ assert.eq(allUsers[0]._id, 'test.user1');
+ assert.eq(allUsers[0].roles.map((r) => r.role), ['role2']);
+
+ const allRoles = assert.commandWorked(test.runCommand({rolesInfo: 1})).roles;
+ assert.eq(allRoles.length, 1);
+ assert.eq(allRoles[0]._id, 'test.role2');
+ assert.eq(allRoles[0].roles.length, 0);
+
+ admin.logout();
+}
+
+//// Standalone
+// We don't have transactions in standalone mode.
+// Behavior elides transaction machinery, but is still protected by
+// local mutex on the UMC commands.
+// Expect the second command to block.
+{
+ const kFailpointDelay = 10 * 1000;
+ const mongod = MongoRunner.runMongod({auth: null});
+ assert.commandWorked(mongod.getDB('admin').runCommand({
+ configureFailPoint: 'umcTransaction',
+ mode: 'alwaysOn',
+ data: {commitDelayMS: NumberInt(kFailpointDelay)},
+ }));
+
+ runTest(mongod, function(test) {
+ // Pause and cause next op to block.
+ const start = Date.now();
+ const parallelShell = startParallelShell(
+ `
+ db.getSiblingDB('admin').auth('admin', 'pwd');
+ assert.commandWorked(db.getSiblingDB('test').runCommand({dropRole: 'role1'}));
+ `,
+ mongod.port);
+
+ // Other UMCs block.
+ assert.commandWorked(test.runCommand({updateRole: 'role2', privileges: []}));
+ parallelShell();
+ assert.gte(Date.now() - start, kFailpointDelay);
+ });
+
+ MongoRunner.stopMongod(mongod);
+}
+
+//// ReplicaSet
+// Ensure that dropRoles generates a transaction by checking for applyOps.
+{
+ const rst = new ReplSetTest({nodes: 3, keyFile: 'jstests/libs/key1'});
+ rst.startSet();
+ rst.initiate();
+ rst.awaitSecondaryNodes();
+
+ function relevantOp(op) {
+ return ((op.op === 'u') || (op.op === 'd')) &&
+ ((op.ns === 'admin.system.users') || (op.ns === 'admin.system.roles'));
+ }
+
+ function probableTransaction(op) {
+ return (op.op === 'c') && (op.ns === 'admin.$cmd') && (op.o.applyOps !== undefined) &&
+ op.o.applyOps.some(relevantOp);
+ }
+
+ runTest(rst.getPrimary(), function(test) {
+ assert.commandWorked(test.runCommand({dropRole: 'role1'}));
+ const oplog = test.getSiblingDB('local').oplog.rs.find({}).toArray();
+ jsTest.log('Oplog: ' + tojson(oplog));
+
+ // Events were not executed directly on the collections.
+ const updatesAndDrops = oplog.filter(relevantOp);
+ assert.eq(updatesAndDrops.length,
+ 0,
+ 'Found expected actions on priv collections: ' + tojson(updatesAndDrops));
+
+ // They were executed by way of a transaction.
+ const txns = oplog.filter(probableTransaction);
+ assert.eq(
+ txns.length, 1, 'Found unexpected number of probable transactions: ' + tojson(txns));
+
+ const txnOps = txns[0].o.applyOps;
+ assert.eq(
+ txnOps.length, 3, 'Found unexpected number of ops in transaction: ' + tojson(txnOps));
+
+ // Op1: Remove 'role1' from user1
+ const msgUpdateUser = 'First op should be update admin.system.users' + tojson(txnOps);
+ assert.eq(txnOps[0].op, 'u', msgUpdateUser);
+ assert.eq(txnOps[0].ns, 'admin.system.users', msgUpdateUser);
+ assert.eq(txnOps[0].o2._id, 'test.user1', msgUpdateUser);
+ assert.eq(txnOps[0].o.diff.u.roles, [{role: 'role2', db: 'test'}], msgUpdateUser);
+
+ // Op2: Remove 'role1' from role2
+ const msgUpdateRole = 'Second op should be update admin.system.roles' + tojson(txnOps);
+ assert.eq(txnOps[1].op, 'u', msgUpdateRole);
+ assert.eq(txnOps[1].ns, 'admin.system.roles', msgUpdateRole);
+ assert.eq(txnOps[1].o2._id, 'test.role2', msgUpdateRole);
+ assert.eq(txnOps[1].o.diff.u.roles, [], msgUpdateRole);
+
+ // Op3: Remove 'role1' document
+ const msgDropRole = 'Third op should be drop from admin.system.roles' + tojson(txnOps);
+ assert.eq(txnOps[2].op, 'd', msgDropRole);
+ assert.eq(txnOps[2].ns, 'admin.system.roles', msgUpdateRole);
+ assert.eq(txnOps[2].o._id, 'test.role1', msgUpdateRole);
+
+ jsTest.log('Oplog applyOps: ' + tojson(txns));
+ });
+
+ rst.stopSet();
+}
+})();
diff --git a/src/mongo/db/commands/user_management_commands.cpp b/src/mongo/db/commands/user_management_commands.cpp
index 06414694bad..bdc40e745cb 100644
--- a/src/mongo/db/commands/user_management_commands.cpp
+++ b/src/mongo/db/commands/user_management_commands.cpp
@@ -60,17 +60,21 @@
#include "mongo/db/commands/user_management_commands_common.h"
#include "mongo/db/commands/user_management_commands_gen.h"
#include "mongo/db/concurrency/d_concurrency.h"
+#include "mongo/db/curop.h"
#include "mongo/db/dbdirectclient.h"
#include "mongo/db/jsobj.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/ops/write_ops.h"
#include "mongo/db/query/cursor_response.h"
+#include "mongo/db/repl/replication_coordinator.h"
#include "mongo/db/service_context.h"
#include "mongo/logv2/log.h"
#include "mongo/platform/mutex.h"
+#include "mongo/rpc/factory.h"
#include "mongo/rpc/get_status_from_command_result.h"
#include "mongo/s/write_ops/batched_command_response.h"
#include "mongo/stdx/unordered_set.h"
+#include "mongo/transport/service_entry_point.h"
#include "mongo/util/icu.h"
#include "mongo/util/net/ssl_manager.h"
#include "mongo/util/password_digest.h"
@@ -742,6 +746,166 @@ BSONArray vectorToBSON(const std::vector<T>& vec) {
return builder.arr();
}
+MONGO_FAIL_POINT_DEFINE(umcTransaction);
+/**
+ * Handler for performing transaction guarded updates to the auth collections.
+ *
+ * UMCTransaction::commit() must be called after setting up operations,
+ * or the transaction will be aborted on scope exit.
+ */
+class UMCTransaction {
+public:
+ static constexpr StringData kAdminDB = "admin"_sd;
+ static constexpr StringData kCommitTransaction = "commitTransaction"_sd;
+ static constexpr StringData kAbortTransaction = "abortTransaction"_sd;
+
+ UMCTransaction(OperationContext* opCtx, StringData forCommand) {
+ // Don't transactionalize on standalone.
+ _isReplSet = repl::ReplicationCoordinator::get(opCtx)->getReplicationMode() ==
+ repl::ReplicationCoordinator::modeReplSet;
+
+ // Subclient used by transaction operations.
+ _client = opCtx->getServiceContext()->makeClient(forCommand.toString());
+ auto as = AuthorizationSession::get(_client.get());
+ if (as) {
+ as->grantInternalAuthorization(_client.get());
+ }
+
+ AlternativeClientRegion clientRegion(_client);
+ _sessionInfo.setStartTransaction(true);
+ _sessionInfo.setTxnNumber(0);
+ _sessionInfo.setSessionId(LogicalSessionFromClient(UUID::gen()));
+ _sessionInfo.setAutocommit(false);
+ }
+ ~UMCTransaction() {
+ if (_state == TransactionState::kStarted) {
+ abort().ignore();
+ }
+ }
+
+ StatusWith<std::uint32_t> insert(const NamespaceString& nss, const std::vector<BSONObj>& docs) {
+ dassert(nss.db() == kAdminDB);
+ write_ops::Insert op(nss);
+ op.setDocuments(docs);
+ return doCrudOp(op.toBSON({}));
+ }
+
+ StatusWith<std::uint32_t> update(const NamespaceString& nss, BSONObj query, BSONObj update) {
+ dassert(nss.db() == kAdminDB);
+ write_ops::UpdateOpEntry entry;
+ entry.setQ(query);
+ entry.setU(write_ops::UpdateModification::parseFromClassicUpdate(update));
+ entry.setMulti(true);
+ write_ops::Update op(nss);
+ op.setUpdates({entry});
+ return doCrudOp(op.toBSON({}));
+ }
+
+ StatusWith<std::uint32_t> remove(const NamespaceString& nss, BSONObj query) {
+ dassert(nss.db() == kAdminDB);
+ write_ops::DeleteOpEntry entry;
+ entry.setQ(query);
+ entry.setMulti(true);
+ write_ops::Delete op(nss);
+ op.setDeletes({entry});
+ return doCrudOp(op.toBSON({}));
+ }
+
+ Status commit() {
+ auto fp = umcTransaction.scoped();
+ if (fp.isActive()) {
+ IDLParserErrorContext ctx("umcTransaction");
+ auto delay = UMCTransactionFailPoint::parse(ctx, fp.getData()).getCommitDelayMS();
+ LOGV2(4993100,
+ "Sleeping prior to committing UMC transaction",
+ "duration"_attr = Milliseconds(delay));
+ sleepmillis(delay);
+ }
+ return commitOrAbort(kCommitTransaction);
+ }
+
+ Status abort() {
+ return commitOrAbort(kAbortTransaction);
+ }
+
+private:
+ StatusWith<std::uint32_t> doCrudOp(BSONObj op) try {
+ invariant(_state != TransactionState::kDone);
+
+ BSONObjBuilder body(op);
+ auto reply = runCommand(&body);
+ auto status = getStatusFromCommandResult(reply);
+ if (!status.isOK()) {
+ return status;
+ }
+
+ if (_state == TransactionState::kInit) {
+ _state = TransactionState::kStarted;
+ _sessionInfo.setStartTransaction(boost::none);
+ }
+
+ BatchedCommandResponse response;
+ std::string errmsg;
+ if (!response.parseBSON(reply, &errmsg)) {
+ return {ErrorCodes::FailedToParse, errmsg};
+ }
+
+ return response.getN();
+ } catch (const AssertionException& ex) {
+ return ex.toStatus();
+ }
+
+ Status commitOrAbort(StringData cmd) {
+ invariant((cmd == kCommitTransaction) || (cmd == kAbortTransaction));
+ if (_state != TransactionState::kStarted) {
+ return {ErrorCodes::NoSuchTransaction, "UMC Transaction not running"};
+ }
+
+ if (_isReplSet) {
+ BSONObjBuilder cmdBuilder;
+ cmdBuilder.append(cmd, 1);
+ auto status = getStatusFromCommandResult(runCommand(&cmdBuilder));
+ if (!status.isOK()) {
+ return status;
+ }
+ }
+
+ _state = TransactionState::kDone;
+ return Status::OK();
+ }
+
+ BSONObj runCommand(BSONObjBuilder* cmdBuilder) {
+ if (_isReplSet) {
+ // Append logical session (transaction) metadata.
+ _sessionInfo.serialize(cmdBuilder);
+ }
+
+ auto svcCtx = _client->getServiceContext();
+ auto sep = svcCtx->getServiceEntryPoint();
+ auto opMsgRequest = OpMsgRequest::fromDBAndBody(kAdminDB, cmdBuilder->obj());
+ auto requestMessage = rpc::messageFromOpMsgRequest(
+ rpc::supports::kAll, rpc::supports::kOpQueryOnly, opMsgRequest);
+
+ // Switch to our local client and create a short-lived opCtx for this transaction op.
+ AlternativeClientRegion clientRegion(_client);
+ auto subOpCtx = svcCtx->makeOperationContext(Client::getCurrent());
+ auto responseMessage = sep->handleRequest(subOpCtx.get(), requestMessage).get().response;
+ return rpc::makeReply(&responseMessage)->getCommandReply().getOwned();
+ }
+
+private:
+ enum class TransactionState {
+ kInit,
+ kStarted,
+ kDone,
+ };
+
+ bool _isReplSet;
+ ServiceContext::UniqueClient _client;
+ OperationSessionInfoFromClient _sessionInfo;
+ TransactionState _state = TransactionState::kInit;
+};
+
template <typename RequestT, typename ReplyT>
class CmdUMCTyped : public TypedCommand<CmdUMCTyped<RequestT, ReplyT>> {
public:
@@ -1612,70 +1776,42 @@ void CmdUMCTyped<DropRoleCommand, void>::Invocation::typedRun(OperationContext*
}
});
+ UMCTransaction txn(opCtx, DropRoleCommand::kCommandName);
+
// Remove this role from all users
- std::int64_t numMatched;
- auto status = updateAuthzDocuments(
- opCtx,
- AuthorizationManager::usersCollectionNamespace,
- BSON("roles" << BSON("$elemMatch" << BSON(AuthorizationManager::ROLE_NAME_FIELD_NAME
- << roleName.getRole()
- << AuthorizationManager::ROLE_DB_FIELD_NAME
- << roleName.getDB()))),
- BSON("$pull" << BSON("roles" << BSON(AuthorizationManager::ROLE_NAME_FIELD_NAME
- << roleName.getRole()
- << AuthorizationManager::ROLE_DB_FIELD_NAME
- << roleName.getDB()))),
- false,
- true,
- &numMatched);
- if (!status.isOK()) {
- uassertStatusOK(useDefaultCode(status, ErrorCodes::UserModificationFailed)
+ auto swCount = txn.update(AuthorizationManager::usersCollectionNamespace,
+ BSON("roles" << BSON("$elemMatch" << roleName.toBSON())),
+ BSON("$pull" << BSON("roles" << roleName.toBSON())));
+ if (!swCount.isOK()) {
+ uassertStatusOK(useDefaultCode(swCount.getStatus(), ErrorCodes::UserModificationFailed)
.withContext(str::stream()
<< "Failed to remove role " << roleName.getFullName()
<< " from all users"));
}
// Remove this role from all other roles
- status = updateAuthzDocuments(
- opCtx,
- AuthorizationManager::rolesCollectionNamespace,
- BSON("roles" << BSON("$elemMatch" << BSON(AuthorizationManager::ROLE_NAME_FIELD_NAME
- << roleName.getRole()
- << AuthorizationManager::ROLE_DB_FIELD_NAME
- << roleName.getDB()))),
- BSON("$pull" << BSON("roles" << BSON(AuthorizationManager::ROLE_NAME_FIELD_NAME
- << roleName.getRole()
- << AuthorizationManager::ROLE_DB_FIELD_NAME
- << roleName.getDB()))),
- false,
- true,
- &numMatched);
- if (!status.isOK()) {
- uassertStatusOK(useDefaultCode(status, ErrorCodes::RoleModificationFailed)
+ swCount = txn.update(AuthorizationManager::rolesCollectionNamespace,
+ BSON("roles" << BSON("$elemMatch" << roleName.toBSON())),
+ BSON("$pull" << BSON("roles" << roleName.toBSON())));
+ if (!swCount.isOK()) {
+ uassertStatusOK(useDefaultCode(swCount.getStatus(), ErrorCodes::RoleModificationFailed)
.withContext(str::stream()
- << "Removed role " << roleName.getFullName()
- << " from all users but failed to remove from all roles"));
+ << "Failed to remove role " << roleName.getFullName()
+ << " from all users"));
}
- audit::logDropRole(client, roleName);
-
// Finally, remove the actual role document
- status = removeRoleDocuments(
- opCtx,
- BSON(AuthorizationManager::ROLE_NAME_FIELD_NAME
- << roleName.getRole() << AuthorizationManager::ROLE_DB_FIELD_NAME << roleName.getDB()),
- &numMatched);
- if (!status.isOK()) {
- uassertStatusOK(status.withContext(
- str::stream() << "Removed role " << roleName.getFullName()
- << " from all users and roles but failed to actually delete"
- " the role itself"));
+ swCount = txn.remove(AuthorizationManager::rolesCollectionNamespace, roleName.toBSON());
+ if (!swCount.isOK()) {
+ uassertStatusOK(swCount.getStatus().withContext(str::stream() << "Failed to remove role "
+ << roleName.getFullName()));
}
- dassert(numMatched == 0 || numMatched == 1);
- if (numMatched == 0) {
- uasserted(ErrorCodes::RoleNotFound,
- str::stream() << "Role '" << roleName.getFullName() << "' not found");
+ audit::logDropRole(client, roleName);
+
+ auto status = txn.commit();
+ if (!status.isOK()) {
+ uassertStatusOK(status.withContext("Failed applying dropRole transaction"));
}
}
@@ -1702,53 +1838,50 @@ CmdUMCTyped<DropAllRolesFromDatabaseCommand, DropAllRolesFromDatabaseReply>::Inv
}
});
+ UMCTransaction txn(opCtx, DropAllRolesFromDatabaseCommand::kCommandName);
+ auto roleMatch = BSON(AuthorizationManager::ROLE_DB_FIELD_NAME << dbname);
+ auto rolesMatch = BSON("roles" << roleMatch);
+
// Remove these roles from all users
- std::int64_t numMatched;
- auto status = updateAuthzDocuments(
- opCtx,
- AuthorizationManager::usersCollectionNamespace,
- BSON("roles" << BSON(AuthorizationManager::ROLE_DB_FIELD_NAME << dbname)),
- BSON("$pull" << BSON("roles" << BSON(AuthorizationManager::ROLE_DB_FIELD_NAME << dbname))),
- false,
- true,
- &numMatched);
- if (!status.isOK()) {
- uassertStatusOK(useDefaultCode(status, ErrorCodes::UserModificationFailed)
+ auto swCount = txn.update(
+ AuthorizationManager::usersCollectionNamespace, rolesMatch, BSON("$pull" << rolesMatch));
+ if (!swCount.isOK()) {
+ uassertStatusOK(useDefaultCode(swCount.getStatus(), ErrorCodes::UserModificationFailed)
.withContext(str::stream() << "Failed to remove roles from \"" << dbname
<< "\" db from all users"));
}
// Remove these roles from all other roles
- std::string sourceFieldName = str::stream()
- << "roles." << AuthorizationManager::ROLE_DB_FIELD_NAME;
- status = updateAuthzDocuments(
- opCtx,
- AuthorizationManager::rolesCollectionNamespace,
- BSON(sourceFieldName << dbname),
- BSON("$pull" << BSON("roles" << BSON(AuthorizationManager::ROLE_DB_FIELD_NAME << dbname))),
- false,
- true,
- &numMatched);
- if (!status.isOK()) {
- uassertStatusOK(useDefaultCode(status, ErrorCodes::RoleModificationFailed)
+ swCount = txn.update(AuthorizationManager::rolesCollectionNamespace,
+ BSON("roles.db" << dbname),
+ BSON("$pull" << rolesMatch));
+ if (!swCount.isOK()) {
+ uassertStatusOK(useDefaultCode(swCount.getStatus(), ErrorCodes::RoleModificationFailed)
.withContext(str::stream() << "Failed to remove roles from \"" << dbname
<< "\" db from all roles"));
}
- audit::logDropAllRolesFromDatabase(Client::getCurrent(), dbname);
// Finally, remove the actual role documents
- status = removeRoleDocuments(
- opCtx, BSON(AuthorizationManager::ROLE_DB_FIELD_NAME << dbname), &numMatched);
- if (!status.isOK()) {
- uassertStatusOK(status.withContext(
+ swCount = txn.remove(AuthorizationManager::rolesCollectionNamespace, roleMatch);
+ if (!swCount.isOK()) {
+ uassertStatusOK(swCount.getStatus().withContext(
str::stream() << "Removed roles from \"" << dbname
<< "\" db "
" from all users and roles but failed to actually delete"
" those roles themselves"));
}
+ audit::logDropAllRolesFromDatabase(Client::getCurrent(), dbname);
+
+ auto status = txn.commit();
+ if (!status.isOK()) {
+ uassertStatusOK(
+ status.withContext("Failed applying dropAllRolesFromDatabase command transaction"));
+ }
+
+
DropAllRolesFromDatabaseReply reply;
- reply.setCount(numMatched);
+ reply.setCount(swCount.getValue());
return reply;
}
diff --git a/src/mongo/db/commands/user_management_commands.idl b/src/mongo/db/commands/user_management_commands.idl
index ca1c510a872..6fd68d27f65 100644
--- a/src/mongo/db/commands/user_management_commands.idl
+++ b/src/mongo/db/commands/user_management_commands.idl
@@ -60,6 +60,14 @@ structs:
type: int
cpp_name: count
+ UMCTransactionFailPoint:
+ description: Data for umcTransaction failpoint
+ fields:
+ commitDelayMS:
+ type: int
+ default: 0
+ validator: { gte: 0 }
+
commands:
createUser:
description: "Create a user"
diff --git a/src/mongo/db/commands/write_commands/write_commands.cpp b/src/mongo/db/commands/write_commands/write_commands.cpp
index fca0e4f3de4..06ecbf0f175 100644
--- a/src/mongo/db/commands/write_commands/write_commands.cpp
+++ b/src/mongo/db/commands/write_commands/write_commands.cpp
@@ -276,7 +276,7 @@ private:
uassert(50791,
str::stream() << "Cannot write to system collection " << ns().toString()
<< " within a transaction.",
- !ns().isSystem());
+ !ns().isSystem() || ns().isPrivilegeCollection());
auto replCoord = repl::ReplicationCoordinator::get(opCtx);
uassert(50790,
str::stream() << "Cannot write to unreplicated collection " << ns().toString()