/** * 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/catalog/uuid_catalog.h" #include "mongo/db/client.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/write_concern_error_detail.h" #include "mongo/s/stale_exception.h" #include "mongo/util/log.h" namespace mongo { using std::string; using std::stringstream; using logger::LogComponent; Command::CommandMap* Command::_commandsByBestName = nullptr; Command::CommandMap* Command::_commands = nullptr; Counter64 Command::unknownCommands; static ServerStatusMetricField displayUnknownCommands("commands.", &Command::unknownCommands); namespace { ExportedServerParameter testCommandsParameter( ServerParameterSet::getGlobal(), "enableTestCommands", &Command::testCommandsEnabled); } // namespace Command::~Command() = default; BSONObj Command::appendPassthroughFields(const BSONObj& cmdObjWithPassthroughFields, const BSONObj& request) { BSONObjBuilder b; b.appendElements(request); for (const auto& elem : Command::filterCommandRequestForPassthrough(cmdObjWithPassthroughFields)) { const auto name = elem.fieldNameStringData(); if (Command::isGenericArgument(name) && !request.hasField(name)) { b.append(elem); } } return b.obj(); } string Command::parseNsFullyQualified(const string& dbname, 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 Command::parseNsCollectionRequired(const string& 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::BadValue, 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; } NamespaceString Command::parseNsOrUUID(OperationContext* opCtx, const string& dbname, const BSONObj& cmdObj) { BSONElement first = cmdObj.firstElement(); if (first.type() == BinData && first.binDataType() == BinDataType::newUUID) { StatusWith uuidRes = UUID::parse(first); uassertStatusOK(uuidRes); UUIDCatalog& catalog = UUIDCatalog::get(opCtx); return catalog.lookupNSSByUUID(uuidRes.getValue()); } 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; } } string Command::parseNs(const string& dbname, const BSONObj& cmdObj) const { BSONElement first = cmdObj.firstElement(); if (first.type() != mongo::String) return dbname; return str::stream() << dbname << '.' << cmdObj.firstElement().valueStringData(); } ResourcePattern Command::parseResourcePattern(const std::string& dbname, const BSONObj& cmdObj) const { const std::string ns = parseNs(dbname, cmdObj); if (!NamespaceString::validCollectionComponent(ns)) { return ResourcePattern::forDatabaseName(ns); } return ResourcePattern::forExactNamespace(NamespaceString(ns)); } Command::Command(StringData name, StringData oldName) : _name(name.toString()), _commandsExecutedMetric("commands." + _name + ".total", &_commandsExecuted), _commandsFailedMetric("commands." + _name + ".failed", &_commandsFailed) { // register ourself. if (_commands == 0) _commands = new CommandMap(); if (_commandsByBestName == 0) _commandsByBestName = new CommandMap(); Command*& c = (*_commands)[name]; if (c) log() << "warning: 2 commands with name: " << _name; c = this; (*_commandsByBestName)[name] = this; if (!oldName.empty()) (*_commands)[oldName.toString()] = this; } void Command::help(stringstream& help) const { help << "no help defined"; } Status Command::explain(OperationContext* opCtx, const string& dbname, const BSONObj& cmdObj, ExplainOptions::Verbosity verbosity, BSONObjBuilder* out) const { return {ErrorCodes::IllegalOperation, str::stream() << "Cannot explain cmd: " << getName()}; } BSONObj Command::runCommandDirectly(OperationContext* opCtx, const OpMsgRequest& request) { auto command = Command::findCommand(request.getCommandName()); invariant(command); BSONObjBuilder out; try { bool ok = command->publicRun(opCtx, request, out); appendCommandStatus(out, ok); } catch (const StaleConfigException& ex) { // These exceptions are intended to be handled at a higher level and cannot losslessly // round-trip through Status. throw; } catch (const DBException& ex) { out.resetToEmpty(); appendCommandStatus(out, ex.toStatus()); } return out.obj(); } Command* Command::findCommand(StringData name) { CommandMap::const_iterator i = _commands->find(name); if (i == _commands->end()) return 0; return i->second; } bool Command::appendCommandStatus(BSONObjBuilder& result, const Status& status) { appendCommandStatus(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())); } return status.isOK(); } void Command::appendCommandStatus(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); } } void Command::appendCommandWCStatus(BSONObjBuilder& result, const Status& awaitReplicationStatus, const WriteConcernResult& wcResult) { if (!awaitReplicationStatus.isOK() && !result.hasField("writeConcernError")) { WriteConcernErrorDetail wcError; wcError.setErrCode(awaitReplicationStatus.code()); wcError.setErrMessage(awaitReplicationStatus.reason()); if (wcResult.wTimedOut) { wcError.setErrInfo(BSON("wtimeout" << true)); } result.append("writeConcernError", wcError.toBSON()); } } Status BasicCommand::checkAuthForRequest(OperationContext* opCtx, const OpMsgRequest& request) { uassertNoDocumentSequences(request); return checkAuthForOperation(opCtx, request.getDatabase().toString(), request.body); } Status BasicCommand::checkAuthForOperation(OperationContext* opCtx, const std::string& dbname, const BSONObj& cmdObj) { return checkAuthForCommand(opCtx->getClient(), dbname, cmdObj); } Status BasicCommand::checkAuthForCommand(Client* client, const std::string& dbname, const BSONObj& cmdObj) { std::vector privileges; this->addRequiredPrivileges(dbname, cmdObj, &privileges); if (AuthorizationSession::get(client)->isAuthorizedForPrivileges(privileges)) return Status::OK(); return Status(ErrorCodes::Unauthorized, "unauthorized"); } void Command::redactForLogging(mutablebson::Document* cmdObj) {} BSONObj Command::getRedactedCopyForLogging(const BSONObj& cmdObj) { namespace mmb = mutablebson; mmb::Document cmdToLog(cmdObj, mmb::Document::kInPlaceDisabled); redactForLogging(&cmdToLog); BSONObjBuilder bob; cmdToLog.writeTo(&bob); return bob.obj(); } static Status _checkAuthorizationImpl(Command* c, OperationContext* opCtx, const OpMsgRequest& request) { namespace mmb = mutablebson; auto client = opCtx->getClient(); auto dbname = request.getDatabase(); if (c->adminOnly() && dbname != "admin") { return Status(ErrorCodes::Unauthorized, str::stream() << c->getName() << " may only be run against the admin database."); } if (AuthorizationSession::get(client)->getAuthorizationManager().isAuthEnabled()) { Status status = c->checkAuthForRequest(opCtx, request); if (status == ErrorCodes::Unauthorized) { mmb::Document cmdToLog(request.body, mmb::Document::kInPlaceDisabled); c->redactForLogging(&cmdToLog); return Status(ErrorCodes::Unauthorized, str::stream() << "not authorized on " << dbname << " to execute command " << cmdToLog.toString()); } if (!status.isOK()) { return status; } } else if (c->adminOnly() && c->localHostOnlyIfNoAuth() && !client->getIsLocalHostConnection()) { return Status(ErrorCodes::Unauthorized, str::stream() << c->getName() << " must run from localhost when running db without auth"); } return Status::OK(); } Status Command::checkAuthorization(Command* c, OperationContext* opCtx, const OpMsgRequest& request) { Status status = _checkAuthorizationImpl(c, opCtx, request); if (!status.isOK()) { log(LogComponent::kAccessControl) << status; } audit::logCommandAuthzCheck(opCtx->getClient(), request, c, status.code()); return status; } bool Command::publicRun(OperationContext* opCtx, const OpMsgRequest& request, BSONObjBuilder& result) { try { return enhancedRun(opCtx, request, result); } catch (const DBException& e) { if (e.code() == ErrorCodes::Unauthorized) { audit::logCommandAuthzCheck( opCtx->getClient(), request, this, ErrorCodes::Unauthorized); } throw; } } bool Command::isHelpRequest(const BSONElement& helpElem) { return !helpElem.eoo() && helpElem.trueValue(); } const char Command::kHelpFieldName[] = "help"; void Command::generateHelpResponse(OperationContext* opCtx, rpc::ReplyBuilderInterface* replyBuilder, const Command& command) { std::stringstream ss; BSONObjBuilder helpBuilder; ss << "help for: " << command.getName() << " "; command.help(ss); helpBuilder.append("help", ss.str()); replyBuilder->setCommandReply(helpBuilder.obj()); replyBuilder->setMetadata(rpc::makeEmptyMetadata()); } namespace { const stdx::unordered_set userManagementCommands{"createUser", "updateUser", "dropUser", "dropAllUsersFromDatabase", "grantRolesToUser", "revokeRolesFromUser", "createRole", "updateRole", "dropRole", "dropAllRolesFromDatabase", "grantPrivilegesToRole", "revokePrivilegesFromRole", "grantRolesToRole", "revokeRolesFromRole", "_mergeAuthzCollections", "authSchemaUpgrade"}; } // namespace bool Command::isUserManagementCommand(const std::string& name) { return userManagementCommands.count(name); } void BasicCommand::uassertNoDocumentSequences(const OpMsgRequest& request) { uassert(40472, str::stream() << "The " << getName() << " command does not support document sequences.", request.sequences.empty()); } bool BasicCommand::enhancedRun(OperationContext* opCtx, const OpMsgRequest& request, BSONObjBuilder& result) { uassertNoDocumentSequences(request); return run(opCtx, request.getDatabase().toString(), request.body, result); } 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()) { appendCommandStatus(result, ok, errmsg); } return ok; } BSONObj Command::filterCommandRequestForPassthrough(const BSONObj& cmdObj) { BSONObjBuilder bob; for (auto elem : cmdObj) { const auto name = elem.fieldNameStringData(); if (name == "$readPreference") { BSONObjBuilder(bob.subobjStart("$queryOptions")).append(elem); } else if (!Command::isGenericArgument(name) || // name == "$queryOptions" || // name == "maxTimeMS" || // name == "readConcern" || // name == "writeConcern") { // This is the whitelist of generic arguments that commands can be trusted to blindly // forward to the shards. bob.append(elem); } } return bob.obj(); } void Command::filterCommandReplyForPassthrough(const BSONObj& cmdObj, BSONObjBuilder* output) { for (auto elem : cmdObj) { const auto name = elem.fieldNameStringData(); if (name == "$configServerState" || // name == "$gleStats" || // name == "$clusterTime" || // name == "$oplogQueryData" || // name == "$replData" || // name == "operationTime") { continue; } output->append(elem); } } BSONObj Command::filterCommandReplyForPassthrough(const BSONObj& cmdObj) { BSONObjBuilder bob; filterCommandReplyForPassthrough(cmdObj, &bob); return bob.obj(); } } // namespace mongo