/**
* Copyright (C) 2018 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/bson/mutable/algorithm.h"
#include "mongo/bson/mutable/document.h"
#include "mongo/db/auth/action_set.h"
#include "mongo/db/auth/action_type.h"
#include "mongo/db/auth/authorization_session.h"
#include "mongo/db/auth/privilege.h"
#include "mongo/db/commands.h"
#include "mongo/db/commands/rename_collection.h"
#include "mongo/executor/task_executor_pool.h"
#include "mongo/rpc/get_status_from_command_result.h"
#include "mongo/s/commands/cluster_commands_helpers.h"
#include "mongo/s/commands/cluster_explain.h"
#include "mongo/s/grid.h"
#include "mongo/s/query/store_possible_cursor.h"
#include "mongo/util/log.h"
#include "mongo/util/timer.h"
namespace mongo {
namespace {
bool cursorCommandPassthrough(OperationContext* opCtx,
StringData dbName,
const CachedDatabaseInfo& dbInfo,
const BSONObj& cmdObj,
const NamespaceString& nss,
BSONObjBuilder* out) {
auto response = executeCommandAgainstDatabasePrimary(
opCtx,
dbName,
dbInfo,
CommandHelpers::filterCommandRequestForPassthrough(cmdObj),
ReadPreferenceSetting::get(opCtx),
Shard::RetryPolicy::kIdempotent);
const auto cmdResponse = uassertStatusOK(std::move(response.swResponse));
auto transformedResponse = uassertStatusOK(
storePossibleCursor(opCtx,
dbInfo.primaryId(),
*response.shardHostAndPort,
cmdResponse.data,
nss,
Grid::get(opCtx)->getExecutorPool()->getArbitraryExecutor(),
Grid::get(opCtx)->getCursorManager()));
CommandHelpers::filterCommandReplyForPassthrough(transformedResponse, out);
return true;
}
bool nonShardedCollectionCommandPassthrough(OperationContext* opCtx,
StringData dbName,
const NamespaceString& nss,
const CachedCollectionRoutingInfo& routingInfo,
const BSONObj& cmdObj,
Shard::RetryPolicy retryPolicy,
BSONObjBuilder* out) {
const StringData cmdName(cmdObj.firstElementFieldName());
uassert(ErrorCodes::IllegalOperation,
str::stream() << "Can't do command: " << cmdName << " on a sharded collection",
!routingInfo.cm());
auto responses = scatterGatherVersionedTargetByRoutingTable(opCtx,
dbName,
nss,
routingInfo,
cmdObj,
ReadPreferenceSetting::get(opCtx),
retryPolicy,
{},
{});
invariant(responses.size() == 1);
const auto cmdResponse = uassertStatusOK(std::move(responses.front().swResponse));
const auto status = getStatusFromCommandResult(cmdResponse.data);
uassert(ErrorCodes::IllegalOperation,
str::stream() << "Can't do command: " << cmdName << " on a sharded collection",
!status.isA());
out->appendElementsUnique(CommandHelpers::filterCommandReplyForPassthrough(cmdResponse.data));
return status.isOK();
}
class NotAllowedOnShardedCollectionCmd : public BasicCommand {
protected:
NotAllowedOnShardedCollectionCmd(const char* n) : BasicCommand(n) {}
AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
return AllowedOnSecondary::kAlways;
}
bool run(OperationContext* opCtx,
const std::string& dbName,
const BSONObj& cmdObj,
BSONObjBuilder& result) override {
const NamespaceString nss(parseNs(dbName, cmdObj));
const auto routingInfo =
uassertStatusOK(Grid::get(opCtx)->catalogCache()->getCollectionRoutingInfo(opCtx, nss));
uassert(ErrorCodes::IllegalOperation,
str::stream() << "can't do command: " << getName() << " on sharded collection",
!routingInfo.cm());
const auto primaryShard = routingInfo.db().primary();
// Here, we first filter the command before appending an UNSHARDED shardVersion, because
// "shardVersion" is one of the fields that gets filtered out.
BSONObj filteredCmdObj(CommandHelpers::filterCommandRequestForPassthrough(cmdObj));
BSONObj filteredCmdObjWithVersion(
appendShardVersion(filteredCmdObj, ChunkVersion::UNSHARDED()));
auto commandResponse = uassertStatusOK(primaryShard->runCommandWithFixedRetryAttempts(
opCtx,
ReadPreferenceSetting::get(opCtx),
dbName,
primaryShard->isConfig() ? filteredCmdObj : filteredCmdObjWithVersion,
Shard::RetryPolicy::kIdempotent));
uassert(ErrorCodes::IllegalOperation,
str::stream() << "can't do command: " << getName() << " on a sharded collection",
!ErrorCodes::isStaleShardVersionError(commandResponse.commandStatus.code()));
uassertStatusOK(commandResponse.commandStatus);
if (!commandResponse.writeConcernStatus.isOK()) {
appendWriteConcernErrorToCmdResponse(
primaryShard->getId(), commandResponse.response["writeConcernError"], result);
}
result.appendElementsUnique(
CommandHelpers::filterCommandReplyForPassthrough(std::move(commandResponse.response)));
return true;
}
};
class RenameCollectionCmd : public BasicCommand {
public:
RenameCollectionCmd() : BasicCommand("renameCollection") {}
std::string parseNs(const std::string& dbname, const BSONObj& cmdObj) const override {
return CommandHelpers::parseNsFullyQualified(cmdObj);
}
bool adminOnly() const override {
return true;
}
bool supportsWriteConcern(const BSONObj& cmd) const override {
return true;
}
AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
return AllowedOnSecondary::kNever;
}
Status checkAuthForCommand(Client* client,
const std::string& dbname,
const BSONObj& cmdObj) const override {
return rename_collection::checkAuthForRenameCollectionCommand(client, dbname, cmdObj);
}
bool run(OperationContext* opCtx,
const std::string& dbName,
const BSONObj& cmdObj,
BSONObjBuilder& result) override {
const NamespaceString fromNss(parseNs(dbName, cmdObj));
const NamespaceString toNss([&cmdObj] {
const auto fullnsToElt = cmdObj["to"];
uassert(ErrorCodes::InvalidNamespace,
"'to' must be of type String",
fullnsToElt.type() == BSONType::String);
return fullnsToElt.valueStringData();
}());
uassert(ErrorCodes::InvalidNamespace,
str::stream() << "Invalid target namespace: " << toNss.ns(),
toNss.isValid());
const auto fromRoutingInfo = uassertStatusOK(
Grid::get(opCtx)->catalogCache()->getCollectionRoutingInfo(opCtx, fromNss));
uassert(13138, "You can't rename a sharded collection", !fromRoutingInfo.cm());
const auto toRoutingInfo = uassertStatusOK(
Grid::get(opCtx)->catalogCache()->getCollectionRoutingInfo(opCtx, toNss));
uassert(13139, "You can't rename to a sharded collection", !toRoutingInfo.cm());
uassert(13137,
"Source and destination collections must be on same shard",
fromRoutingInfo.db().primaryId() == toRoutingInfo.db().primaryId());
return nonShardedCollectionCommandPassthrough(
opCtx,
NamespaceString::kAdminDb,
fromNss,
fromRoutingInfo,
appendAllowImplicitCreate(CommandHelpers::filterCommandRequestForPassthrough(cmdObj),
true),
Shard::RetryPolicy::kNoRetry,
&result);
}
} renameCollectionCmd;
class ConvertToCappedCmd : public BasicCommand {
public:
ConvertToCappedCmd() : BasicCommand("convertToCapped") {}
bool supportsWriteConcern(const BSONObj& cmd) const override {
return true;
}
AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
return AllowedOnSecondary::kNever;
}
std::string parseNs(const std::string& dbname, const BSONObj& cmdObj) const override {
return CommandHelpers::parseNsCollectionRequired(dbname, cmdObj).ns();
}
void addRequiredPrivileges(const std::string& dbname,
const BSONObj& cmdObj,
std::vector* out) const override {
ActionSet actions;
actions.addAction(ActionType::convertToCapped);
out->push_back(Privilege(parseResourcePattern(dbname, cmdObj), actions));
}
bool run(OperationContext* opCtx,
const std::string& dbName,
const BSONObj& cmdObj,
BSONObjBuilder& result) override {
const NamespaceString nss(parseNs(dbName, cmdObj));
const auto routingInfo =
uassertStatusOK(Grid::get(opCtx)->catalogCache()->getCollectionRoutingInfo(opCtx, nss));
// convertToCapped creates a temp collection and renames it at the end. It will require
// special handling for create collection.
return nonShardedCollectionCommandPassthrough(
opCtx,
dbName,
nss,
routingInfo,
appendAllowImplicitCreate(CommandHelpers::filterCommandRequestForPassthrough(cmdObj),
true),
Shard::RetryPolicy::kIdempotent,
&result);
}
} convertToCappedCmd;
class SplitVectorCmd : public NotAllowedOnShardedCollectionCmd {
public:
SplitVectorCmd() : NotAllowedOnShardedCollectionCmd("splitVector") {}
std::string parseNs(const std::string& dbname, const BSONObj& cmdObj) const override {
return CommandHelpers::parseNsFullyQualified(cmdObj);
}
bool supportsWriteConcern(const BSONObj& cmd) const override {
return false;
}
Status checkAuthForCommand(Client* client,
const std::string& dbname,
const BSONObj& cmdObj) const override {
if (!AuthorizationSession::get(client)->isAuthorizedForActionsOnResource(
ResourcePattern::forExactNamespace(NamespaceString(parseNs(dbname, cmdObj))),
ActionType::splitVector)) {
return Status(ErrorCodes::Unauthorized, "Unauthorized");
}
return Status::OK();
}
bool run(OperationContext* opCtx,
const std::string& dbName,
const BSONObj& cmdObj,
BSONObjBuilder& result) override {
const NamespaceString nss(parseNs(dbName, cmdObj));
uassert(ErrorCodes::IllegalOperation,
"Performing splitVector across dbs isn't supported via mongos",
nss.db() == dbName);
return NotAllowedOnShardedCollectionCmd::run(opCtx, dbName, cmdObj, result);
}
} splitVectorCmd;
class CmdListCollections : public BasicCommand {
public:
CmdListCollections() : BasicCommand("listCollections") {}
bool supportsWriteConcern(const BSONObj& cmd) const override {
return false;
}
AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
return AllowedOnSecondary::kAlways;
}
bool adminOnly() const override {
return false;
}
Status checkAuthForCommand(Client* client,
const std::string& dbname,
const BSONObj& cmdObj) const final {
AuthorizationSession* authzSession = AuthorizationSession::get(client);
if (authzSession->isAuthorizedToListCollections(dbname, cmdObj)) {
return Status::OK();
}
return Status(ErrorCodes::Unauthorized,
str::stream() << "Not authorized to list collections on db: " << dbname);
}
BSONObj rewriteCommandForListingOwnCollections(OperationContext* opCtx,
const std::string& dbName,
const BSONObj& cmdObj) {
mutablebson::Document rewrittenCmdObj(cmdObj);
mutablebson::Element ownCollections =
mutablebson::findFirstChildNamed(rewrittenCmdObj.root(), "authorizedCollections");
AuthorizationSession* authzSession = AuthorizationSession::get(opCtx->getClient());
// We must strip $ownCollections from the delegated command.
uassertStatusOK(ownCollections.remove());
BSONObj collectionFilter;
// Extract and retain any previous filter
mutablebson::Element oldFilter =
mutablebson::findFirstChildNamed(rewrittenCmdObj.root(), "filter");
// Make a new filter, containing a $and array.
mutablebson::Element newFilter = rewrittenCmdObj.makeElementObject("filter");
mutablebson::Element newFilterAnd = rewrittenCmdObj.makeElementArray("$and");
uassertStatusOK(newFilter.pushBack(newFilterAnd));
// Append a rule to the $and, which rejects system collections.
mutablebson::Element systemCollectionsFilter = rewrittenCmdObj.makeElementObject(
"", BSON("name" << BSON("$regex" << BSONRegEx("^(?!system\\.)"))));
uassertStatusOK(newFilterAnd.pushBack(systemCollectionsFilter));
if (!authzSession->isAuthorizedForAnyActionOnResource(
ResourcePattern::forDatabaseName(dbName))) {
// We passed an auth check which said we might be able to render some collections,
// but it doesn't seem like we should render all of them. We must filter.
// Compute the set of collection names which would be permissible to return.
std::set collectionNames;
for (UserNameIterator nameIter = authzSession->getAuthenticatedUserNames();
nameIter.more();
nameIter.next()) {
User* authUser = authzSession->lookupUser(*nameIter);
const User::ResourcePrivilegeMap& resourcePrivilegeMap = authUser->getPrivileges();
for (const std::pair& resourcePrivilege :
resourcePrivilegeMap) {
const auto& resource = resourcePrivilege.first;
if (resource.isCollectionPattern() || (resource.isExactNamespacePattern() &&
resource.databaseToMatch() == dbName)) {
collectionNames.emplace(resource.collectionToMatch().toString());
}
}
}
// Construct a new filter predicate which returns only collections we were found to
// have privileges for.
BSONObjBuilder predicateBuilder;
BSONObjBuilder nameBuilder(predicateBuilder.subobjStart("name"));
BSONArrayBuilder setBuilder(nameBuilder.subarrayStart("$in"));
// Load the de-duplicated set into a BSON array
for (StringData collectionName : collectionNames) {
setBuilder << collectionName;
}
setBuilder.done();
nameBuilder.done();
collectionFilter = predicateBuilder.obj();
// Filter the results by our collection names.
mutablebson::Element newFilterAndIn =
rewrittenCmdObj.makeElementObject("", collectionFilter);
uassertStatusOK(newFilterAnd.pushBack(newFilterAndIn));
}
// If there was a pre-existing filter, compose it with our new one.
if (oldFilter.ok()) {
uassertStatusOK(oldFilter.remove());
uassertStatusOK(newFilterAnd.pushBack(oldFilter));
}
// Attach our new composite filter back onto the listCollections command object.
uassertStatusOK(rewrittenCmdObj.root().pushBack(newFilter));
return rewrittenCmdObj.getObject();
}
bool run(OperationContext* opCtx,
const std::string& dbName,
const BSONObj& cmdObj,
BSONObjBuilder& result) override {
const auto nss(NamespaceString::makeListCollectionsNSS(dbName));
BSONObj newCmd = cmdObj;
if (newCmd["authorizedCollections"].trueValue()) {
newCmd = rewriteCommandForListingOwnCollections(opCtx, dbName, cmdObj);
}
auto dbInfoStatus = Grid::get(opCtx)->catalogCache()->getDatabase(opCtx, dbName);
if (!dbInfoStatus.isOK()) {
return appendEmptyResultSet(opCtx, result, dbInfoStatus.getStatus(), nss.ns());
}
return cursorCommandPassthrough(
opCtx, dbName, dbInfoStatus.getValue(), newCmd, nss, &result);
}
} cmdListCollections;
class CmdListIndexes : public BasicCommand {
public:
CmdListIndexes() : BasicCommand("listIndexes") {}
std::string parseNs(const std::string& dbname, const BSONObj& cmdObj) const override {
return CommandHelpers::parseNsCollectionRequired(dbname, cmdObj).ns();
}
bool supportsWriteConcern(const BSONObj& cmd) const override {
return false;
}
AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
return AllowedOnSecondary::kAlways;
}
bool adminOnly() const override {
return false;
}
Status checkAuthForCommand(Client* client,
const std::string& dbname,
const BSONObj& cmdObj) const override {
AuthorizationSession* authzSession = AuthorizationSession::get(client);
// Check for the listIndexes ActionType on the database, or find on system.indexes for pre
// 3.0 systems.
const NamespaceString ns(parseNs(dbname, cmdObj));
if (authzSession->isAuthorizedForActionsOnResource(ResourcePattern::forExactNamespace(ns),
ActionType::listIndexes) ||
authzSession->isAuthorizedForActionsOnResource(
ResourcePattern::forExactNamespace(NamespaceString(dbname, "system.indexes")),
ActionType::find)) {
return Status::OK();
}
return Status(ErrorCodes::Unauthorized,
str::stream() << "Not authorized to list indexes on collection: "
<< ns.coll());
}
bool run(OperationContext* opCtx,
const std::string& dbName,
const BSONObj& cmdObj,
BSONObjBuilder& result) override {
const NamespaceString nss(parseNs(dbName, cmdObj));
const auto routingInfo =
uassertStatusOK(Grid::get(opCtx)->catalogCache()->getCollectionRoutingInfo(opCtx, nss));
return cursorCommandPassthrough(opCtx,
nss.db(),
routingInfo.db(),
cmdObj,
NamespaceString::makeListIndexesNSS(nss.db(), nss.coll()),
&result);
}
} cmdListIndexes;
} // namespace
} // namespace mongo