1
2 /**
3 * Copyright (C) 2018-present MongoDB, Inc.
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the Server Side Public License, version 1,
7 * as published by MongoDB, Inc.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * Server Side Public License for more details.
13 *
14 * You should have received a copy of the Server Side Public License
15 * along with this program. If not, see
16 * <http://www.mongodb.com/licensing/server-side-public-license>.
17 *
18 * As a special exception, the copyright holders give permission to link the
19 * code of portions of this program with the OpenSSL library under certain
20 * conditions as described in each individual source file and distribute
21 * linked combinations including the program with the OpenSSL library. You
22 * must comply with the Server Side Public License in all respects for
23 * all of the code used other than as permitted herein. If you modify file(s)
24 * with this exception, you may extend this exception to your version of the
25 * file(s), but you are not obligated to do so. If you do not wish to do so,
26 * delete this exception statement from your version. If you delete this
27 * exception statement from all source files in the program, then also delete
28 * it in the license file.
29 */
30
31 #define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kCommand
32
33 #include "mongo/platform/basic.h"
34
35 #include "mongo/db/commands.h"
36
37 #include <string>
38 #include <vector>
39
40 #include "mongo/bson/mutable/document.h"
41 #include "mongo/bson/timestamp.h"
42 #include "mongo/db/audit.h"
43 #include "mongo/db/auth/action_set.h"
44 #include "mongo/db/auth/action_type.h"
45 #include "mongo/db/auth/authorization_manager.h"
46 #include "mongo/db/auth/authorization_session.h"
47 #include "mongo/db/auth/privilege.h"
48 #include "mongo/db/catalog/uuid_catalog.h"
49 #include "mongo/db/client.h"
50 #include "mongo/db/curop.h"
51 #include "mongo/db/jsobj.h"
52 #include "mongo/db/namespace_string.h"
53 #include "mongo/db/server_parameters.h"
54 #include "mongo/rpc/write_concern_error_detail.h"
55 #include "mongo/s/stale_exception.h"
56 #include "mongo/util/log.h"
57
58 namespace mongo {
59
60 using std::string;
61 using std::stringstream;
62
63 using logger::LogComponent;
64
65 Command::CommandMap* Command::_commandsByBestName = nullptr;
66 Command::CommandMap* Command::_commands = nullptr;
67
68 Counter64 Command::unknownCommands;
69 static ServerStatusMetricField<Counter64> displayUnknownCommands("commands.<UNKNOWN>",
70 &Command::unknownCommands);
71
72 namespace {
73
74 ExportedServerParameter<bool, ServerParameterType::kStartupOnly> testCommandsParameter(
75 ServerParameterSet::getGlobal(), "enableTestCommands", &Command::testCommandsEnabled);
76
77 const char kWriteConcernField[] = "writeConcern";
78 const WriteConcernOptions kMajorityWriteConcern(
79 WriteConcernOptions::kMajority,
80 // Note: Even though we're setting UNSET here, kMajority implies JOURNAL if journaling is
81 // supported by the mongod.
82 WriteConcernOptions::SyncMode::UNSET,
83 WriteConcernOptions::kWriteConcernTimeoutUserCommand);
84
85 } // namespace
86
87 Command::~Command() = default;
88
appendPassthroughFields(const BSONObj & cmdObjWithPassthroughFields,const BSONObj & request)89 BSONObj Command::appendPassthroughFields(const BSONObj& cmdObjWithPassthroughFields,
90 const BSONObj& request) {
91 BSONObjBuilder b;
92 b.appendElements(request);
93 for (const auto& elem :
94 Command::filterCommandRequestForPassthrough(cmdObjWithPassthroughFields)) {
95 const auto name = elem.fieldNameStringData();
96 if (Command::isGenericArgument(name) && !request.hasField(name)) {
97 b.append(elem);
98 }
99 }
100 return b.obj();
101 }
102
appendMajorityWriteConcern(const BSONObj & cmdObj)103 BSONObj Command::appendMajorityWriteConcern(const BSONObj& cmdObj) {
104 if (cmdObj.hasField(kWriteConcernField)) {
105 return cmdObj;
106 }
107 BSONObjBuilder cmdObjWithWriteConcern;
108 cmdObjWithWriteConcern.appendElementsUnique(cmdObj);
109 cmdObjWithWriteConcern.append(kWriteConcernField, kMajorityWriteConcern.toBSON());
110 return cmdObjWithWriteConcern.obj();
111 }
112
parseNsFullyQualified(const string & dbname,const BSONObj & cmdObj)113 string Command::parseNsFullyQualified(const string& dbname, const BSONObj& cmdObj) {
114 BSONElement first = cmdObj.firstElement();
115 uassert(ErrorCodes::BadValue,
116 str::stream() << "collection name has invalid type " << typeName(first.type()),
117 first.canonicalType() == canonicalizeBSONType(mongo::String));
118 const NamespaceString nss(first.valueStringData());
119 uassert(ErrorCodes::InvalidNamespace,
120 str::stream() << "Invalid namespace specified '" << nss.ns() << "'",
121 nss.isValid());
122 return nss.ns();
123 }
124
parseNsCollectionRequired(const string & dbname,const BSONObj & cmdObj)125 NamespaceString Command::parseNsCollectionRequired(const string& dbname, const BSONObj& cmdObj) {
126 // Accepts both BSON String and Symbol for collection name per SERVER-16260
127 // TODO(kangas) remove Symbol support in MongoDB 3.0 after Ruby driver audit
128 BSONElement first = cmdObj.firstElement();
129 const bool isUUID = (first.canonicalType() == canonicalizeBSONType(mongo::BinData) &&
130 first.binDataType() == BinDataType::newUUID);
131 uassert(ErrorCodes::InvalidNamespace,
132 str::stream() << "Collection name must be provided. UUID is not valid in this "
133 << "context",
134 !isUUID);
135 uassert(ErrorCodes::InvalidNamespace,
136 str::stream() << "collection name has invalid type " << typeName(first.type()),
137 first.canonicalType() == canonicalizeBSONType(mongo::String));
138 const NamespaceString nss(dbname, first.valueStringData());
139 uassert(ErrorCodes::InvalidNamespace,
140 str::stream() << "Invalid namespace specified '" << nss.ns() << "'",
141 nss.isValid());
142 return nss;
143 }
144
parseNsOrUUID(OperationContext * opCtx,const string & dbname,const BSONObj & cmdObj)145 NamespaceString Command::parseNsOrUUID(OperationContext* opCtx,
146 const string& dbname,
147 const BSONObj& cmdObj) {
148 BSONElement first = cmdObj.firstElement();
149 if (first.type() == BinData && first.binDataType() == BinDataType::newUUID) {
150 UUIDCatalog& catalog = UUIDCatalog::get(opCtx);
151 UUID uuid = uassertStatusOK(UUID::parse(first));
152 NamespaceString nss = catalog.lookupNSSByUUID(uuid);
153 uassert(ErrorCodes::NamespaceNotFound,
154 str::stream() << "UUID " << uuid << " specified in "
155 << cmdObj.firstElement().fieldNameStringData()
156 << " command not found in "
157 << dbname
158 << " database",
159 nss.isValid() && nss.db() == dbname);
160
161 return nss;
162 } else {
163 // Ensure collection identifier is not a Command
164 const NamespaceString nss(parseNsCollectionRequired(dbname, cmdObj));
165 uassert(ErrorCodes::InvalidNamespace,
166 str::stream() << "Invalid collection name specified '" << nss.ns() << "'",
167 nss.isNormal());
168 return nss;
169 }
170 }
171
parseNs(const string & dbname,const BSONObj & cmdObj) const172 string Command::parseNs(const string& dbname, const BSONObj& cmdObj) const {
173 BSONElement first = cmdObj.firstElement();
174 if (first.type() != mongo::String)
175 return dbname;
176
177 return str::stream() << dbname << '.' << cmdObj.firstElement().valueStringData();
178 }
179
parseResourcePattern(const std::string & dbname,const BSONObj & cmdObj) const180 ResourcePattern Command::parseResourcePattern(const std::string& dbname,
181 const BSONObj& cmdObj) const {
182 const std::string ns = parseNs(dbname, cmdObj);
183 if (!NamespaceString::validCollectionComponent(ns)) {
184 return ResourcePattern::forDatabaseName(ns);
185 }
186 return ResourcePattern::forExactNamespace(NamespaceString(ns));
187 }
188
Command(StringData name,std::vector<StringData> aliases)189 Command::Command(StringData name, std::vector<StringData> aliases)
190 : _name(name.toString()),
191 _commandsExecutedMetric("commands." + _name + ".total", &_commandsExecuted),
192 _commandsFailedMetric("commands." + _name + ".failed", &_commandsFailed) {
193 // register ourself.
194 if (_commands == 0)
195 _commands = new CommandMap();
196 if (_commandsByBestName == 0)
197 _commandsByBestName = new CommandMap();
198 Command*& c = (*_commands)[name];
199 if (c)
200 log() << "warning: 2 commands with name: " << _name;
201 c = this;
202 (*_commandsByBestName)[name] = this;
203
204 for (auto key : aliases) {
205 if (key.empty()) {
206 continue;
207 }
208 (*_commands)[key.toString()] = this;
209 }
210 }
211
help(stringstream & help) const212 void Command::help(stringstream& help) const {
213 help << "no help defined";
214 }
215
explain(OperationContext * opCtx,const string & dbname,const BSONObj & cmdObj,ExplainOptions::Verbosity verbosity,BSONObjBuilder * out) const216 Status Command::explain(OperationContext* opCtx,
217 const string& dbname,
218 const BSONObj& cmdObj,
219 ExplainOptions::Verbosity verbosity,
220 BSONObjBuilder* out) const {
221 return {ErrorCodes::IllegalOperation, str::stream() << "Cannot explain cmd: " << getName()};
222 }
223
runCommandDirectly(OperationContext * opCtx,const OpMsgRequest & request)224 BSONObj Command::runCommandDirectly(OperationContext* opCtx, const OpMsgRequest& request) {
225 auto command = Command::findCommand(request.getCommandName());
226 invariant(command);
227
228 BSONObjBuilder out;
229 try {
230 bool ok = command->publicRun(opCtx, request, out);
231 appendCommandStatus(out, ok);
232 } catch (const StaleConfigException&) {
233 // These exceptions are intended to be handled at a higher level and cannot losslessly
234 // round-trip through Status.
235 throw;
236 } catch (const DBException& ex) {
237 out.resetToEmpty();
238 appendCommandStatus(out, ex.toStatus());
239 }
240 return out.obj();
241 }
242
findCommand(StringData name)243 Command* Command::findCommand(StringData name) {
244 CommandMap::const_iterator i = _commands->find(name);
245 if (i == _commands->end())
246 return 0;
247 return i->second;
248 }
249
appendCommandStatus(BSONObjBuilder & result,const Status & status)250 bool Command::appendCommandStatus(BSONObjBuilder& result, const Status& status) {
251 appendCommandStatus(result, status.isOK(), status.reason());
252 BSONObj tmp = result.asTempObj();
253 if (!status.isOK() && !tmp.hasField("code")) {
254 result.append("code", status.code());
255 result.append("codeName", ErrorCodes::errorString(status.code()));
256 }
257 return status.isOK();
258 }
259
appendCommandStatus(BSONObjBuilder & result,bool ok,const std::string & errmsg)260 void Command::appendCommandStatus(BSONObjBuilder& result, bool ok, const std::string& errmsg) {
261 BSONObj tmp = result.asTempObj();
262 bool have_ok = tmp.hasField("ok");
263 bool need_errmsg = !ok && !tmp.hasField("errmsg");
264
265 if (!have_ok)
266 result.append("ok", ok ? 1.0 : 0.0);
267
268 if (need_errmsg) {
269 result.append("errmsg", errmsg);
270 }
271 }
272
appendCommandWCStatus(BSONObjBuilder & result,const Status & awaitReplicationStatus,const WriteConcernResult & wcResult)273 void Command::appendCommandWCStatus(BSONObjBuilder& result,
274 const Status& awaitReplicationStatus,
275 const WriteConcernResult& wcResult) {
276 if (!awaitReplicationStatus.isOK() && !result.hasField("writeConcernError")) {
277 WriteConcernErrorDetail wcError;
278 wcError.setErrCode(awaitReplicationStatus.code());
279 wcError.setErrMessage(awaitReplicationStatus.reason());
280 if (wcResult.wTimedOut) {
281 wcError.setErrInfo(BSON("wtimeout" << true));
282 }
283 result.append("writeConcernError", wcError.toBSON());
284 }
285 }
286
checkAuthForRequest(OperationContext * opCtx,const OpMsgRequest & request)287 Status BasicCommand::checkAuthForRequest(OperationContext* opCtx, const OpMsgRequest& request) {
288 uassertNoDocumentSequences(request);
289 return checkAuthForOperation(opCtx, request.getDatabase().toString(), request.body);
290 }
291
checkAuthForOperation(OperationContext * opCtx,const std::string & dbname,const BSONObj & cmdObj)292 Status BasicCommand::checkAuthForOperation(OperationContext* opCtx,
293 const std::string& dbname,
294 const BSONObj& cmdObj) {
295 return checkAuthForCommand(opCtx->getClient(), dbname, cmdObj);
296 }
297
checkAuthForCommand(Client * client,const std::string & dbname,const BSONObj & cmdObj)298 Status BasicCommand::checkAuthForCommand(Client* client,
299 const std::string& dbname,
300 const BSONObj& cmdObj) {
301 std::vector<Privilege> privileges;
302 this->addRequiredPrivileges(dbname, cmdObj, &privileges);
303 if (AuthorizationSession::get(client)->isAuthorizedForPrivileges(privileges))
304 return Status::OK();
305 return Status(ErrorCodes::Unauthorized, "unauthorized");
306 }
307
redactForLogging(mutablebson::Document * cmdObj)308 void Command::redactForLogging(mutablebson::Document* cmdObj) {}
309
getRedactedCopyForLogging(const BSONObj & cmdObj)310 BSONObj Command::getRedactedCopyForLogging(const BSONObj& cmdObj) {
311 namespace mmb = mutablebson;
312 mmb::Document cmdToLog(cmdObj, mmb::Document::kInPlaceDisabled);
313 redactForLogging(&cmdToLog);
314 BSONObjBuilder bob;
315 cmdToLog.writeTo(&bob);
316 return bob.obj();
317 }
318
_checkAuthorizationImpl(Command * c,OperationContext * opCtx,const OpMsgRequest & request)319 static Status _checkAuthorizationImpl(Command* c,
320 OperationContext* opCtx,
321 const OpMsgRequest& request) {
322 namespace mmb = mutablebson;
323 auto client = opCtx->getClient();
324 auto dbname = request.getDatabase();
325 if (c->adminOnly() && dbname != "admin") {
326 return Status(ErrorCodes::Unauthorized,
327 str::stream() << c->getName()
328 << " may only be run against the admin database.");
329 }
330 if (AuthorizationSession::get(client)->getAuthorizationManager().isAuthEnabled()) {
331 Status status = c->checkAuthForRequest(opCtx, request);
332 if (status == ErrorCodes::Unauthorized) {
333 mmb::Document cmdToLog(request.body, mmb::Document::kInPlaceDisabled);
334 c->redactForLogging(&cmdToLog);
335 return Status(ErrorCodes::Unauthorized,
336 str::stream() << "not authorized on " << dbname << " to execute command "
337 << redact(cmdToLog.getObject()));
338 }
339 if (!status.isOK()) {
340 return status;
341 }
342 } else if (c->adminOnly() && c->localHostOnlyIfNoAuth() &&
343 !client->getIsLocalHostConnection()) {
344 return Status(ErrorCodes::Unauthorized,
345 str::stream() << c->getName()
346 << " must run from localhost when running db without auth");
347 }
348 return Status::OK();
349 }
350
checkAuthorization(Command * c,OperationContext * opCtx,const OpMsgRequest & request)351 Status Command::checkAuthorization(Command* c,
352 OperationContext* opCtx,
353 const OpMsgRequest& request) {
354 Status status = _checkAuthorizationImpl(c, opCtx, request);
355 if (!status.isOK()) {
356 log(LogComponent::kAccessControl) << status;
357 }
358 audit::logCommandAuthzCheck(opCtx->getClient(), request, c, status.code());
359 return status;
360 }
361
publicRun(OperationContext * opCtx,const OpMsgRequest & request,BSONObjBuilder & result)362 bool Command::publicRun(OperationContext* opCtx,
363 const OpMsgRequest& request,
364 BSONObjBuilder& result) {
365 try {
366 return enhancedRun(opCtx, request, result);
367 } catch (const DBException& e) {
368 if (e.code() == ErrorCodes::Unauthorized) {
369 audit::logCommandAuthzCheck(
370 opCtx->getClient(), request, this, ErrorCodes::Unauthorized);
371 }
372 throw;
373 }
374 }
375
isHelpRequest(const BSONElement & helpElem)376 bool Command::isHelpRequest(const BSONElement& helpElem) {
377 return !helpElem.eoo() && helpElem.trueValue();
378 }
379
380 const char Command::kHelpFieldName[] = "help";
381
generateHelpResponse(OperationContext * opCtx,rpc::ReplyBuilderInterface * replyBuilder,const Command & command)382 void Command::generateHelpResponse(OperationContext* opCtx,
383 rpc::ReplyBuilderInterface* replyBuilder,
384 const Command& command) {
385 std::stringstream ss;
386 BSONObjBuilder helpBuilder;
387 ss << "help for: " << command.getName() << " ";
388 command.help(ss);
389 helpBuilder.append("help", ss.str());
390
391 replyBuilder->setCommandReply(helpBuilder.obj());
392 replyBuilder->setMetadata(rpc::makeEmptyMetadata());
393 }
394
hasAlias(const StringData & alias) const395 bool Command::hasAlias(const StringData& alias) const {
396 return findCommand(alias) == this;
397 }
398
399 namespace {
400 const stdx::unordered_set<std::string> userManagementCommands{"createUser",
401 "updateUser",
402 "dropUser",
403 "dropAllUsersFromDatabase",
404 "grantRolesToUser",
405 "revokeRolesFromUser",
406 "createRole",
407 "updateRole",
408 "dropRole",
409 "dropAllRolesFromDatabase",
410 "grantPrivilegesToRole",
411 "revokePrivilegesFromRole",
412 "grantRolesToRole",
413 "revokeRolesFromRole",
414 "_mergeAuthzCollections",
415 "authSchemaUpgrade"};
416 } // namespace
417
isUserManagementCommand(const std::string & name)418 bool Command::isUserManagementCommand(const std::string& name) {
419 return userManagementCommands.count(name);
420 }
421
uassertNoDocumentSequences(const OpMsgRequest & request)422 void BasicCommand::uassertNoDocumentSequences(const OpMsgRequest& request) {
423 uassert(40472,
424 str::stream() << "The " << getName() << " command does not support document sequences.",
425 request.sequences.empty());
426 }
427
enhancedRun(OperationContext * opCtx,const OpMsgRequest & request,BSONObjBuilder & result)428 bool BasicCommand::enhancedRun(OperationContext* opCtx,
429 const OpMsgRequest& request,
430 BSONObjBuilder& result) {
431 uassertNoDocumentSequences(request);
432 return run(opCtx, request.getDatabase().toString(), request.body, result);
433 }
434
run(OperationContext * opCtx,const std::string & db,const BSONObj & cmdObj,BSONObjBuilder & result)435 bool ErrmsgCommandDeprecated::run(OperationContext* opCtx,
436 const std::string& db,
437 const BSONObj& cmdObj,
438 BSONObjBuilder& result) {
439 std::string errmsg;
440 auto ok = errmsgRun(opCtx, db, cmdObj, errmsg, result);
441 if (!errmsg.empty()) {
442 appendCommandStatus(result, ok, errmsg);
443 }
444 return ok;
445 }
446
filterCommandRequestForPassthrough(const BSONObj & cmdObj)447 BSONObj Command::filterCommandRequestForPassthrough(const BSONObj& cmdObj) {
448 BSONObjBuilder bob;
449 for (auto elem : cmdObj) {
450 const auto name = elem.fieldNameStringData();
451 if (name == "$readPreference") {
452 BSONObjBuilder(bob.subobjStart("$queryOptions")).append(elem);
453 } else if (!Command::isGenericArgument(name) || //
454 name == "$queryOptions" || //
455 name == "maxTimeMS" || //
456 name == "readConcern" || //
457 name == "writeConcern" || //
458 name == "lsid" || //
459 name == "txnNumber") {
460 // This is the whitelist of generic arguments that commands can be trusted to blindly
461 // forward to the shards.
462 bob.append(elem);
463 }
464 }
465 return bob.obj();
466 }
467
filterCommandReplyForPassthrough(const BSONObj & cmdObj,BSONObjBuilder * output)468 void Command::filterCommandReplyForPassthrough(const BSONObj& cmdObj, BSONObjBuilder* output) {
469 for (auto elem : cmdObj) {
470 const auto name = elem.fieldNameStringData();
471 if (name == "$configServerState" || //
472 name == "$gleStats" || //
473 name == "$clusterTime" || //
474 name == "$oplogQueryData" || //
475 name == "$replData" || //
476 name == "operationTime") {
477 continue;
478 }
479 output->append(elem);
480 }
481 }
482
filterCommandReplyForPassthrough(const BSONObj & cmdObj)483 BSONObj Command::filterCommandReplyForPassthrough(const BSONObj& cmdObj) {
484 BSONObjBuilder bob;
485 filterCommandReplyForPassthrough(cmdObj, &bob);
486 return bob.obj();
487 }
488
489 } // namespace mongo
490