/**
* Copyright (C) 2009-2016 MongoDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the GNU Affero General Public License in all respects
* for all of the code used other than as permitted herein. If you modify
* file(s) with this exception, you may extend this exception to your
* version of the file(s), but you are not obligated to do so. If you do not
* wish to do so, delete this exception statement from your version. If you
* delete this exception statement from all source files in the program,
* then also delete it in the license file.
*/
#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kCommand
#include "mongo/platform/basic.h"
#include "mongo/db/commands.h"
#include
#include
#include "mongo/bson/mutable/document.h"
#include "mongo/bson/timestamp.h"
#include "mongo/db/audit.h"
#include "mongo/db/auth/action_set.h"
#include "mongo/db/auth/action_type.h"
#include "mongo/db/auth/authorization_manager.h"
#include "mongo/db/auth/authorization_session.h"
#include "mongo/db/auth/privilege.h"
#include "mongo/db/client.h"
#include "mongo/db/command_generic_argument.h"
#include "mongo/db/commands/test_commands_enabled.h"
#include "mongo/db/curop.h"
#include "mongo/db/jsobj.h"
#include "mongo/db/namespace_string.h"
#include "mongo/db/server_parameters.h"
#include "mongo/rpc/factory.h"
#include "mongo/rpc/op_msg_rpc_impls.h"
#include "mongo/rpc/protocol.h"
#include "mongo/rpc/write_concern_error_detail.h"
#include "mongo/s/stale_exception.h"
#include "mongo/util/invariant.h"
#include "mongo/util/log.h"
#include "mongo/util/mongoutils/str.h"
namespace mongo {
using logger::LogComponent;
namespace {
const char kWriteConcernField[] = "writeConcern";
const WriteConcernOptions kMajorityWriteConcern(
WriteConcernOptions::kMajority,
// Note: Even though we're setting UNSET here, kMajority implies JOURNAL if journaling is
// supported by the mongod.
WriteConcernOptions::SyncMode::UNSET,
Seconds(60));
// Returns true if found to be authorized, false if undecided. Throws if unauthorized.
bool checkAuthorizationImplPreParse(OperationContext* opCtx,
const Command* command,
const OpMsgRequest& request) {
auto client = opCtx->getClient();
if (client->isInDirectClient())
return true;
uassert(ErrorCodes::Unauthorized,
str::stream() << command->getName() << " may only be run against the admin database.",
!command->adminOnly() || request.getDatabase() == NamespaceString::kAdminDb);
auto authzSession = AuthorizationSession::get(client);
if (!authzSession->getAuthorizationManager().isAuthEnabled()) {
// Running without auth, so everything should be allowed except remotely invoked
// commands that have the 'localHostOnlyIfNoAuth' restriction.
uassert(ErrorCodes::Unauthorized,
str::stream() << command->getName()
<< " must run from localhost when running db without auth",
!command->adminOnly() || !command->localHostOnlyIfNoAuth() ||
client->getIsLocalHostConnection());
return true; // Blanket authorization: don't need to check anything else.
}
if (authzSession->isUsingLocalhostBypass())
return false; // Still can't decide on auth because of the localhost bypass.
uassert(ErrorCodes::Unauthorized,
str::stream() << "command " << command->getName() << " requires authentication",
!command->requiresAuth() || authzSession->isAuthenticated());
return false;
}
} // namespace
//////////////////////////////////////////////////////////////
// CommandHelpers
BSONObj CommandHelpers::runCommandDirectly(OperationContext* opCtx, const OpMsgRequest& request) {
auto command = globalCommandRegistry()->findCommand(request.getCommandName());
invariant(command);
rpc::OpMsgReplyBuilder replyBuilder;
std::unique_ptr invocation;
try {
invocation = command->parse(opCtx, request);
invocation->run(opCtx, &replyBuilder);
auto body = replyBuilder.getBodyBuilder();
CommandHelpers::extractOrAppendOk(body);
} catch (const StaleConfigException&) {
// These exceptions are intended to be handled at a higher level.
throw;
} catch (const DBException& ex) {
if (ex.code() == ErrorCodes::Unauthorized) {
CommandHelpers::auditLogAuthEvent(opCtx, invocation.get(), request, ex.code());
}
auto body = replyBuilder.getBodyBuilder();
body.resetToEmpty();
appendCommandStatusNoThrow(body, ex.toStatus());
}
return replyBuilder.releaseBody();
}
void CommandHelpers::auditLogAuthEvent(OperationContext* opCtx,
const CommandInvocation* invocation,
const OpMsgRequest& request,
ErrorCodes::Error err) {
class Hook final : public audit::CommandInterface {
public:
explicit Hook(const CommandInvocation* invocation, const NamespaceString* nss)
: _invocation(invocation), _nss(nss) {}
void redactForLogging(mutablebson::Document* cmdObj) const override {
if (_invocation) {
_invocation->definition()->redactForLogging(cmdObj);
}
}
NamespaceString ns() const override {
return *_nss;
}
bool redactArgs() const override {
return !_invocation;
}
private:
const CommandInvocation* _invocation;
const NamespaceString* _nss;
};
NamespaceString nss = invocation ? invocation->ns() : NamespaceString(request.getDatabase());
audit::logCommandAuthzCheck(opCtx->getClient(), request, Hook(invocation, &nss), err);
}
void CommandHelpers::uassertNoDocumentSequences(StringData commandName,
const OpMsgRequest& request) {
uassert(40472,
str::stream() << "The " << commandName
<< " command does not support document sequences.",
request.sequences.empty());
}
std::string CommandHelpers::parseNsFullyQualified(const BSONObj& cmdObj) {
BSONElement first = cmdObj.firstElement();
uassert(ErrorCodes::BadValue,
str::stream() << "collection name has invalid type " << typeName(first.type()),
first.canonicalType() == canonicalizeBSONType(mongo::String));
const NamespaceString nss(first.valueStringData());
uassert(ErrorCodes::InvalidNamespace,
str::stream() << "Invalid namespace specified '" << nss.ns() << "'",
nss.isValid());
return nss.ns();
}
NamespaceString CommandHelpers::parseNsCollectionRequired(StringData dbname,
const BSONObj& cmdObj) {
// Accepts both BSON String and Symbol for collection name per SERVER-16260
// TODO(kangas) remove Symbol support in MongoDB 3.0 after Ruby driver audit
BSONElement first = cmdObj.firstElement();
uassert(ErrorCodes::InvalidNamespace,
str::stream() << "collection name has invalid type " << typeName(first.type()),
first.canonicalType() == canonicalizeBSONType(mongo::String));
const NamespaceString nss(dbname, first.valueStringData());
uassert(ErrorCodes::InvalidNamespace,
str::stream() << "Invalid namespace specified '" << nss.ns() << "'",
nss.isValid());
return nss;
}
NamespaceStringOrUUID CommandHelpers::parseNsOrUUID(StringData dbname, const BSONObj& cmdObj) {
BSONElement first = cmdObj.firstElement();
if (first.type() == BinData && first.binDataType() == BinDataType::newUUID) {
return {dbname.toString(), uassertStatusOK(UUID::parse(first))};
} else {
// Ensure collection identifier is not a Command
const NamespaceString nss(parseNsCollectionRequired(dbname, cmdObj));
uassert(ErrorCodes::InvalidNamespace,
str::stream() << "Invalid collection name specified '" << nss.ns() << "'",
nss.isNormal());
return nss;
}
}
std::string CommandHelpers::parseNsFromCommand(StringData dbname, const BSONObj& cmdObj) {
BSONElement first = cmdObj.firstElement();
if (first.type() != mongo::String)
return dbname.toString();
return str::stream() << dbname << '.' << cmdObj.firstElement().valueStringData();
}
ResourcePattern CommandHelpers::resourcePatternForNamespace(const std::string& ns) {
if (!NamespaceString::validCollectionComponent(ns)) {
return ResourcePattern::forDatabaseName(ns);
}
return ResourcePattern::forExactNamespace(NamespaceString(ns));
}
Command* CommandHelpers::findCommand(StringData name) {
return globalCommandRegistry()->findCommand(name);
}
bool CommandHelpers::appendCommandStatusNoThrow(BSONObjBuilder& result, const Status& status) {
appendSimpleCommandStatus(result, status.isOK(), status.reason());
BSONObj tmp = result.asTempObj();
if (!status.isOK() && !tmp.hasField("code")) {
result.append("code", status.code());
result.append("codeName", ErrorCodes::errorString(status.code()));
}
if (auto extraInfo = status.extraInfo()) {
extraInfo->serialize(&result);
}
return status.isOK();
}
void CommandHelpers::appendSimpleCommandStatus(BSONObjBuilder& result,
bool ok,
const std::string& errmsg) {
BSONObj tmp = result.asTempObj();
bool have_ok = tmp.hasField("ok");
bool need_errmsg = !ok && !tmp.hasField("errmsg");
if (!have_ok)
result.append("ok", ok ? 1.0 : 0.0);
if (need_errmsg) {
result.append("errmsg", errmsg);
}
}
bool CommandHelpers::extractOrAppendOk(BSONObjBuilder& reply) {
if (auto okField = reply.asTempObj()["ok"]) {
// If ok is present, use its truthiness.
return okField.trueValue();
}
// Missing "ok" field is an implied success.
CommandHelpers::appendSimpleCommandStatus(reply, true);
return true;
}
void CommandHelpers::appendCommandWCStatus(BSONObjBuilder& result,
const Status& awaitReplicationStatus,
const WriteConcernResult& wcResult) {
if (!awaitReplicationStatus.isOK() && !result.hasField("writeConcernError")) {
WriteConcernErrorDetail wcError;
wcError.setStatus(awaitReplicationStatus);
if (wcResult.wTimedOut) {
wcError.setErrInfo(BSON("wtimeout" << true));
}
result.append("writeConcernError", wcError.toBSON());
}
}
BSONObj CommandHelpers::appendPassthroughFields(const BSONObj& cmdObjWithPassthroughFields,
const BSONObj& request) {
BSONObjBuilder b;
b.appendElements(request);
for (const auto& elem : filterCommandRequestForPassthrough(cmdObjWithPassthroughFields)) {
const auto name = elem.fieldNameStringData();
if (isGenericArgument(name) && !request.hasField(name)) {
b.append(elem);
}
}
return b.obj();
}
BSONObj CommandHelpers::appendMajorityWriteConcern(const BSONObj& cmdObj) {
WriteConcernOptions newWC = kMajorityWriteConcern;
if (cmdObj.hasField(kWriteConcernField)) {
auto wc = cmdObj.getField(kWriteConcernField);
// The command has a writeConcern field and it's majority, so we can
// return it as-is.
if (wc["w"].ok() && wc["w"].str() == "majority") {
return cmdObj;
}
if (wc["wtimeout"].ok()) {
// They set a timeout, but aren't using majority WC. We want to use their
// timeout along with majority WC.
newWC = WriteConcernOptions(WriteConcernOptions::kMajority,
WriteConcernOptions::SyncMode::UNSET,
wc["wtimeout"].Number());
}
}
// Append all original fields except the writeConcern field to the new command.
BSONObjBuilder cmdObjWithWriteConcern;
for (const auto& elem : cmdObj) {
const auto name = elem.fieldNameStringData();
if (name != "writeConcern" && !cmdObjWithWriteConcern.hasField(name)) {
cmdObjWithWriteConcern.append(elem);
}
}
// Finally, add the new write concern.
cmdObjWithWriteConcern.append(kWriteConcernField, newWC.toBSON());
return cmdObjWithWriteConcern.obj();
}
BSONObj CommandHelpers::filterCommandRequestForPassthrough(const BSONObj& cmdObj) {
BSONObjIterator cmdIter(cmdObj);
BSONObjBuilder bob;
filterCommandRequestForPassthrough(&cmdIter, &bob);
return bob.obj();
}
void CommandHelpers::filterCommandRequestForPassthrough(BSONObjIterator* cmdIter,
BSONObjBuilder* requestBuilder) {
while (cmdIter->more()) {
auto elem = cmdIter->next();
const auto name = elem.fieldNameStringData();
if (name == "$readPreference") {
BSONObjBuilder(requestBuilder->subobjStart("$queryOptions")).append(elem);
continue;
}
if (isRequestStripArgument(name))
continue;
requestBuilder->append(elem);
}
}
void CommandHelpers::filterCommandReplyForPassthrough(const BSONObj& cmdObj,
BSONObjBuilder* output) {
for (auto elem : cmdObj) {
const auto name = elem.fieldNameStringData();
if (isReplyStripArgument(name))
continue;
output->append(elem);
}
}
BSONObj CommandHelpers::filterCommandReplyForPassthrough(const BSONObj& cmdObj) {
BSONObjBuilder bob;
filterCommandReplyForPassthrough(cmdObj, &bob);
return bob.obj();
}
bool CommandHelpers::isHelpRequest(const BSONElement& helpElem) {
return !helpElem.eoo() && helpElem.trueValue();
}
bool CommandHelpers::uassertShouldAttemptParse(OperationContext* opCtx,
const Command* command,
const OpMsgRequest& request) {
try {
return checkAuthorizationImplPreParse(opCtx, command, request);
} catch (const ExceptionFor& e) {
CommandHelpers::auditLogAuthEvent(opCtx, nullptr, request, e.code());
throw;
}
}
constexpr StringData CommandHelpers::kHelpFieldName;
//////////////////////////////////////////////////////////////
// CommandInvocation
CommandInvocation::~CommandInvocation() = default;
void CommandInvocation::checkAuthorization(OperationContext* opCtx,
const OpMsgRequest& request) const {
// Always send an authorization event to audit log, even if OK.
// Not using a scope guard because auditLogAuthEvent could conceivably throw.
try {
const Command* c = definition();
if (checkAuthorizationImplPreParse(opCtx, c, request)) {
// Blanket authorization: don't need to check anything else.
} else {
try {
doCheckAuthorization(opCtx);
} catch (const ExceptionFor&) {
namespace mmb = mutablebson;
mmb::Document cmdToLog(request.body, mmb::Document::kInPlaceDisabled);
c->redactForLogging(&cmdToLog);
auto dbname = request.getDatabase();
uasserted(ErrorCodes::Unauthorized,
str::stream() << "not authorized on " << dbname << " to execute command "
<< redact(cmdToLog.getObject()));
}
}
} catch (const DBException& e) {
log(LogComponent::kAccessControl) << e.toStatus();
CommandHelpers::auditLogAuthEvent(opCtx, this, request, e.code());
throw;
}
CommandHelpers::auditLogAuthEvent(opCtx, this, request, ErrorCodes::OK);
}
//////////////////////////////////////////////////////////////
// Command
class BasicCommand::Invocation final : public CommandInvocation {
public:
Invocation(OperationContext*, const OpMsgRequest& request, BasicCommand* command)
: CommandInvocation(command),
_command(command),
_request(&request),
_dbName(_request->getDatabase().toString()) {}
private:
void run(OperationContext* opCtx, rpc::ReplyBuilderInterface* result) override {
BSONObjBuilder bob = result->getBodyBuilder();
bool ok = _command->run(opCtx, _dbName, _request->body, bob);
if (!ok)
CommandHelpers::appendSimpleCommandStatus(bob, ok);
}
void explain(OperationContext* opCtx,
ExplainOptions::Verbosity verbosity,
BSONObjBuilder* result) override {
uassertStatusOK(_command->explain(opCtx, *_request, verbosity, result));
}
NamespaceString ns() const override {
return NamespaceString(_command->parseNs(_dbName, cmdObj()));
}
bool supportsWriteConcern() const override {
return _command->supportsWriteConcern(cmdObj());
}
bool supportsReadConcern(repl::ReadConcernLevel level) const override {
return _command->supportsReadConcern(_dbName, cmdObj(), level);
}
bool allowsAfterClusterTime() const override {
return _command->allowsAfterClusterTime(cmdObj());
}
void doCheckAuthorization(OperationContext* opCtx) const override {
uassertStatusOK(_command->checkAuthForOperation(
opCtx, _request->getDatabase().toString(), _request->body));
}
const BSONObj& cmdObj() const {
return _request->body;
}
BasicCommand* const _command;
const OpMsgRequest* const _request;
const std::string _dbName;
};
Command::~Command() = default;
std::unique_ptr BasicCommand::parse(OperationContext* opCtx,
const OpMsgRequest& request) {
CommandHelpers::uassertNoDocumentSequences(getName(), request);
return stdx::make_unique(opCtx, request, this);
}
Command::Command(StringData name, StringData oldName)
: _name(name.toString()),
_commandsExecutedMetric("commands." + _name + ".total", &_commandsExecuted),
_commandsFailedMetric("commands." + _name + ".failed", &_commandsFailed) {
globalCommandRegistry()->registerCommand(this, name, oldName);
}
Status BasicCommand::explain(OperationContext* opCtx,
const OpMsgRequest& request,
ExplainOptions::Verbosity verbosity,
BSONObjBuilder* out) const {
return {ErrorCodes::IllegalOperation, str::stream() << "Cannot explain cmd: " << getName()};
}
Status BasicCommand::checkAuthForOperation(OperationContext* opCtx,
const std::string& dbname,
const BSONObj& cmdObj) const {
return checkAuthForCommand(opCtx->getClient(), dbname, cmdObj);
}
Status BasicCommand::checkAuthForCommand(Client* client,
const std::string& dbname,
const BSONObj& cmdObj) const {
std::vector privileges;
this->addRequiredPrivileges(dbname, cmdObj, &privileges);
if (AuthorizationSession::get(client)->isAuthorizedForPrivileges(privileges))
return Status::OK();
return Status(ErrorCodes::Unauthorized, "unauthorized");
}
void Command::generateHelpResponse(OperationContext* opCtx,
rpc::ReplyBuilderInterface* replyBuilder,
const Command& command) {
BSONObjBuilder helpBuilder;
helpBuilder.append("help",
str::stream() << "help for: " << command.getName() << " " << command.help());
replyBuilder->setCommandReply(helpBuilder.obj());
replyBuilder->setMetadata(rpc::makeEmptyMetadata());
}
bool ErrmsgCommandDeprecated::run(OperationContext* opCtx,
const std::string& db,
const BSONObj& cmdObj,
BSONObjBuilder& result) {
std::string errmsg;
auto ok = errmsgRun(opCtx, db, cmdObj, errmsg, result);
if (!errmsg.empty()) {
CommandHelpers::appendSimpleCommandStatus(result, ok, errmsg);
}
return ok;
}
//////////////////////////////////////////////////////////////
// CommandRegistry
void CommandRegistry::registerCommand(Command* command, StringData name, StringData oldName) {
for (StringData key : {name, oldName}) {
if (key.empty()) {
continue;
}
auto hashedKey = CommandMap::HashedKey(key);
auto iter = _commands.find(hashedKey);
invariant(iter == _commands.end(), str::stream() << "command name collision: " << key);
_commands[hashedKey] = command;
}
}
Command* CommandRegistry::findCommand(StringData name) const {
auto it = _commands.find(name);
if (it == _commands.end())
return nullptr;
return it->second;
}
CommandRegistry* globalCommandRegistry() {
static auto reg = new CommandRegistry();
return reg;
}
} // namespace mongo