summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--buildscripts/idl/idl_check_compatibility.py1
-rw-r--r--jstests/core/views/views_all_commands.js1
-rw-r--r--jstests/replsets/db_reads_while_recovering_all_commands.js6
-rw-r--r--jstests/serverless/list_databases_for_all_tenants.js259
-rw-r--r--jstests/sharding/libs/last_lts_mongod_commands.js1
-rw-r--r--jstests/sharding/read_write_concern_defaults_application.js1
-rw-r--r--src/mongo/db/commands/SConscript14
-rw-r--r--src/mongo/db/commands/list_databases.cpp63
-rw-r--r--src/mongo/db/commands/list_databases_common.h134
-rw-r--r--src/mongo/db/commands/list_databases_for_all_tenants.cpp142
-rw-r--r--src/mongo/db/commands/list_databases_for_all_tenants.idl101
-rw-r--r--src/mongo/embedded/mongo_embedded/mongo_embedded_test.cpp1
12 files changed, 671 insertions, 53 deletions
diff --git a/buildscripts/idl/idl_check_compatibility.py b/buildscripts/idl/idl_check_compatibility.py
index 4a421e1353a..75824228167 100644
--- a/buildscripts/idl/idl_check_compatibility.py
+++ b/buildscripts/idl/idl_check_compatibility.py
@@ -161,6 +161,7 @@ ALLOW_ANY_TYPE_LIST: List[str] = [
'find-reply-invalidated',
'getMore-reply-partialResultsReturned',
'getMore-reply-invalidated',
+ 'listDatabasesForAllTenants-reply-tenant',
'create-param-min',
'create-param-max',
]
diff --git a/jstests/core/views/views_all_commands.js b/jstests/core/views/views_all_commands.js
index 0da62002e19..9b8fb5a9972 100644
--- a/jstests/core/views/views_all_commands.js
+++ b/jstests/core/views/views_all_commands.js
@@ -481,6 +481,7 @@ let viewsCommandTests = {
listCollections: {skip: "tested in views/views_creation.js"},
listCommands: {skip: isUnrelated},
listDatabases: {skip: isUnrelated},
+ listDatabasesForAllTenants: {skip: isUnrelated},
listIndexes: {command: {listIndexes: "view"}, expectFailure: true},
listShards: {skip: isUnrelated},
lockInfo: {skip: isUnrelated},
diff --git a/jstests/replsets/db_reads_while_recovering_all_commands.js b/jstests/replsets/db_reads_while_recovering_all_commands.js
index fe4fb73b8dd..4f3d4832433 100644
--- a/jstests/replsets/db_reads_while_recovering_all_commands.js
+++ b/jstests/replsets/db_reads_while_recovering_all_commands.js
@@ -271,6 +271,12 @@ const allCommands = {
expectFailure: true,
expectedErrorCode: ErrorCodes.NotPrimaryOrSecondary
},
+ listDatabasesForAllTenants: {
+ command: {listDatabasesForAllTenants: 1},
+ isAdminCommand: true,
+ expectFailure: true,
+ expectedErrorCode: ErrorCodes.NotPrimaryOrSecondary
+ },
listIndexes: {
command: {listIndexes: collName},
expectFailure: true,
diff --git a/jstests/serverless/list_databases_for_all_tenants.js b/jstests/serverless/list_databases_for_all_tenants.js
new file mode 100644
index 00000000000..98f87d794fe
--- /dev/null
+++ b/jstests/serverless/list_databases_for_all_tenants.js
@@ -0,0 +1,259 @@
+/**
+ * Tests for the listDatabasesForAllTenants command.
+ */
+(function() {
+"use strict";
+
+load('jstests/aggregation/extras/utils.js'); // For arrayEq()
+
+// Given the output from the listDatabasesForAllTenants command, ensures that the total size
+// reported is the sum of the individual db sizes.
+function verifySizeSum(listDatabasesOut) {
+ assert(listDatabasesOut.hasOwnProperty("databases"));
+ const dbList = listDatabasesOut.databases;
+ let sizeSum = 0;
+ for (let i = 0; i < dbList.length; i++) {
+ sizeSum += dbList[i].sizeOnDisk;
+ }
+ assert.eq(sizeSum, listDatabasesOut.totalSize);
+}
+
+// Given the output from the listDatabasesForAllTenants command, ensures that only the names and
+// tenantIds of the databases are listed
+function verifyNameOnly(listDatabasesOut) {
+ // Delete extra meta info only returned by shardsvrs.
+ delete listDatabasesOut.lastCommittedOpTime;
+
+ for (let field in listDatabasesOut) {
+ assert(['totalSize', 'totalSizeMb'].every((f) => f != field), 'unexpected field ' + field);
+ }
+ listDatabasesOut.databases.forEach((database) => {
+ for (let field in database) {
+ assert(['name', 'tenantId'].some((f) => f == field),
+ 'only expected name or tenantId but got: ' + field);
+ }
+ });
+}
+
+// creates 'num' databases on 'conn', each belonging to a different tenant
+function createMultitenantDatabases(conn, tokenConn, num) {
+ let kTenant;
+ let tenantIds = [];
+ let expectedDatabases = [];
+
+ for (let i = 0; i < num; i++) {
+ // Randomly generate a tenantId
+ kTenant = ObjectId();
+ tenantIds.push(kTenant.str);
+
+ // Create a user for kTenant and then set the security token on the connection.
+ assert.commandWorked(conn.getDB('$external').runCommand({
+ createUser: "readWriteUserTenant" + i.toString(),
+ '$tenant': kTenant,
+ roles: [{role: 'readWriteAnyDatabase', db: 'admin'}]
+ }));
+ tokenConn._setSecurityToken(_createSecurityToken(
+ {user: "readWriteUserTenant" + i.toString(), db: '$external', tenant: kTenant}));
+
+ // Create a collection for the tenant and then insert into it.
+ const tokenDB = tokenConn.getDB('auto_gen_db_' + i.toString());
+ assert.commandWorked(tokenDB.createCollection('coll' + i.toString()));
+
+ expectedDatabases.push(
+ {"name": 'auto_gen_db_' + i.toString(), "tenantId": kTenant, "empty": false});
+ }
+ return [tenantIds, expectedDatabases];
+}
+
+// Given the output from the listDatabasesForAllTenants command, ensures that the database entries
+// are correct
+function verifyDatabaseEntries(listDatabasesOut, expectedDatabases) {
+ const fieldsToSkip = ['sizeOnDisk'];
+ assert(
+ arrayEq(expectedDatabases, listDatabasesOut.databases, undefined, undefined, fieldsToSkip),
+ tojson(listDatabasesOut.databases));
+}
+
+// Check that command properly lists all databases created by users authenticated with a security
+// token
+function runTestCheckMultitenantDatabases(mongod, numDBs) {
+ const adminDB = mongod.getDB("admin");
+ const tokenConn = new Mongo(mongod.host);
+
+ // Add a root user that is unauthorized to run the command
+ assert.commandWorked(adminDB.runCommand({createUser: 'admin', pwd: 'pwd', roles: ['root']}));
+
+ // Create numDBs databases, each belonging to a different tenant
+ const [tenantIds, expectedDatabases] = createMultitenantDatabases(mongod, tokenConn, numDBs);
+
+ // Check that all numDB databases were created of the proper size and include the correct
+ // database entries
+ let cmdRes = assert.commandWorked(
+ adminDB.runCommand({listDatabasesForAllTenants: 1, filter: {name: /auto_gen_db_/}}));
+ assert.eq(numDBs, cmdRes.databases.length);
+ verifySizeSum(cmdRes);
+ verifyDatabaseEntries(cmdRes, expectedDatabases);
+
+ return tenantIds;
+}
+
+// Test correctness of filter and nameonly options
+function runTestCheckCmdOptions(mongod, tenantIds) {
+ const adminDB = mongod.getDB("admin");
+
+ // Create 4 databases to verify the correctness of filter and nameOnly
+ assert.commandWorked(mongod.getDB("jstest_list_databases_foo").createCollection("coll0", {
+ '$tenant': ObjectId(tenantIds[0])
+ }));
+ assert.commandWorked(mongod.getDB("jstest_list_databases_bar").createCollection("coll0", {
+ '$tenant': ObjectId(tenantIds[1])
+ }));
+ assert.commandWorked(mongod.getDB("jstest_list_databases_baz").createCollection("coll0", {
+ '$tenant': ObjectId(tenantIds[2])
+ }));
+ assert.commandWorked(mongod.getDB("jstest_list_databases_zap").createCollection("coll0", {
+ '$tenant': ObjectId(tenantIds[3])
+ }));
+
+ // use to verify that the database entries are correct
+ const expectedDatabases2 = [
+ {"name": "jstest_list_databases_foo", "tenantId": ObjectId(tenantIds[0]), "empty": false},
+ {"name": "jstest_list_databases_bar", "tenantId": ObjectId(tenantIds[1]), "empty": false},
+ {"name": "jstest_list_databases_baz", "tenantId": ObjectId(tenantIds[2]), "empty": false},
+ {"name": "jstest_list_databases_zap", "tenantId": ObjectId(tenantIds[3]), "empty": false}
+ ];
+
+ let cmdRes = assert.commandWorked(adminDB.runCommand(
+ {listDatabasesForAllTenants: 1, filter: {name: /jstest_list_databases/}}));
+ assert.eq(4, cmdRes.databases.length);
+ verifySizeSum(cmdRes);
+ verifyDatabaseEntries(cmdRes, expectedDatabases2);
+
+ // Now only list databases starting with a particular prefix.
+ cmdRes = assert.commandWorked(adminDB.runCommand(
+ {listDatabasesForAllTenants: 1, filter: {name: /^jstest_list_databases_ba/}}));
+ assert.eq(2, cmdRes.databases.length);
+ verifySizeSum(cmdRes);
+
+ // Now return only the admin database.
+ cmdRes = assert.commandWorked(
+ adminDB.runCommand({listDatabasesForAllTenants: 1, filter: {name: "admin"}}));
+ assert.eq(1, cmdRes.databases.length);
+ verifySizeSum(cmdRes);
+
+ // Now return only the names.
+ cmdRes = assert.commandWorked(adminDB.runCommand({
+ listDatabasesForAllTenants: 1,
+ filter: {name: /^jstest_list_databases_/},
+ nameOnly: true
+ }));
+ assert.eq(4, cmdRes.databases.length, tojson(cmdRes));
+ verifyNameOnly(cmdRes);
+
+ // Now return only the name of the zap database.
+ cmdRes = assert.commandWorked(
+ adminDB.runCommand({listDatabasesForAllTenants: 1, nameOnly: true, filter: {name: /zap/}}));
+ assert.eq(1, cmdRes.databases.length, tojson(cmdRes));
+ verifyNameOnly(cmdRes);
+
+ // $expr in filter.
+ cmdRes = assert.commandWorked(adminDB.runCommand({
+ listDatabasesForAllTenants: 1,
+ filter: {$expr: {$eq: ["$name", "jstest_list_databases_zap"]}}
+ }));
+ assert.eq(1, cmdRes.databases.length, tojson(cmdRes));
+ assert.eq("jstest_list_databases_zap", cmdRes.databases[0].name, tojson(cmdRes));
+}
+
+// Test that invalid commands fail
+function runTestInvalidCommands(mongod) {
+ const adminDB = mongod.getDB("admin");
+ const tokenConn = new Mongo(mongod.host);
+
+ // $expr with an unbound variable in filter.
+ assert.commandFailed(adminDB.runCommand(
+ {listDatabasesForAllTenants: 1, filter: {$expr: {$eq: ["$name", "$$unbound"]}}}));
+
+ // $expr with a filter that throws at runtime.
+ assert.commandFailed(
+ adminDB.runCommand({listDatabasesForAllTenants: 1, filter: {$expr: {$abs: "$name"}}}));
+
+ // No extensions are allowed in filters.
+ assert.commandFailed(
+ adminDB.runCommand({listDatabasesForAllTenants: 1, filter: {$text: {$search: "str"}}}));
+ assert.commandFailed(adminDB.runCommand({
+ listDatabasesForAllTenants: 1,
+ filter: {
+ $where: function() {
+ return true;
+ }
+ }
+ }));
+ assert.commandFailed(adminDB.runCommand({
+ listDatabasesForAllTenants: 1,
+ filter: {a: {$nearSphere: {$geometry: {type: "Point", coordinates: [0, 0]}}}}
+ }));
+
+ // Remove internal user
+ adminDB.dropUser("internalUsr");
+
+ // Create and authenticate as an admin user with root role
+ assert(adminDB.runCommand({createUser: 'admin', pwd: 'pwd', roles: ['root']}));
+ assert(adminDB.auth("admin", "pwd"));
+
+ // Check that user is not authorized to call the command
+ let cmdRes = assert.commandFailedWithCode(
+ adminDB.runCommand({listDatabasesForAllTenants: 1, filter: {name: /auto_gen_db_/}}),
+ ErrorCodes.Unauthorized);
+
+ // Add user authenticated with security token and check that they cannot run the command
+ const kTenant = ObjectId();
+ assert.commandWorked(mongod.getDB('$external').runCommand({
+ createUser: "unauthorizedUsr",
+ '$tenant': kTenant,
+ roles: [{role: 'readWriteAnyDatabase', db: 'admin'}]
+ }));
+ tokenConn._setSecurityToken(
+ _createSecurityToken({user: "unauthorizedUsr", db: '$external', tenant: kTenant}));
+ const tokenAdminDB = tokenConn.getDB("admin");
+ cmdRes = assert.commandFailedWithCode(
+ tokenAdminDB.runCommand({listDatabasesForAllTenants: 1, filter: {name: /auto_gen_db_/}}),
+ ErrorCodes.Unauthorized);
+}
+
+function runTestsWithMultiTenancySupport() {
+ const mongod = MongoRunner.runMongod(
+ {auth: '', setParameter: {multitenancySupport: true, featureFlagMongoStore: true}});
+ const adminDB = mongod.getDB("admin");
+
+ // Create internal system user that is authorized to run the command
+ assert.commandWorked(
+ adminDB.runCommand({createUser: 'internalUsr', pwd: 'pwd', roles: ['__system']}));
+ assert(adminDB.auth("internalUsr", "pwd"));
+
+ const numDBs = 5;
+ const tenantIds = runTestCheckMultitenantDatabases(mongod, numDBs);
+ runTestCheckCmdOptions(mongod, tenantIds);
+ runTestInvalidCommands(mongod);
+
+ MongoRunner.stopMongod(mongod);
+}
+
+function runTestNoMultiTenancySupport() {
+ const mongod = MongoRunner.runMongod(
+ {auth: '', setParameter: {multitenancySupport: false, featureFlagMongoStore: true}});
+ const adminDB = mongod.getDB("admin");
+
+ assert.commandWorked(
+ adminDB.runCommand({createUser: 'internalUsr', pwd: 'pwd', roles: ['__system']}));
+ assert(adminDB.auth("internalUsr", "pwd"));
+
+ const cmdRes = assert.commandFailedWithCode(adminDB.runCommand({listDatabasesForAllTenants: 1}),
+ ErrorCodes.CommandNotSupported);
+
+ MongoRunner.stopMongod(mongod);
+}
+
+runTestsWithMultiTenancySupport();
+runTestNoMultiTenancySupport();
+}());
diff --git a/jstests/sharding/libs/last_lts_mongod_commands.js b/jstests/sharding/libs/last_lts_mongod_commands.js
index e42fa1f3d6e..88c77f20201 100644
--- a/jstests/sharding/libs/last_lts_mongod_commands.js
+++ b/jstests/sharding/libs/last_lts_mongod_commands.js
@@ -24,6 +24,7 @@ const commandsAddedToMongodSinceLastLTS = [
"clusterUpdate",
"getChangeStreamState",
"getClusterParameter",
+ "listDatabasesForAllTenants",
"rotateCertificates",
"setChangeStreamState",
"setClusterParameter",
diff --git a/jstests/sharding/read_write_concern_defaults_application.js b/jstests/sharding/read_write_concern_defaults_application.js
index 502e80661de..983f97eaacf 100644
--- a/jstests/sharding/read_write_concern_defaults_application.js
+++ b/jstests/sharding/read_write_concern_defaults_application.js
@@ -561,6 +561,7 @@ let testCases = {
listCollections: {skip: "does not accept read or write concern"},
listCommands: {skip: "does not accept read or write concern"},
listDatabases: {skip: "does not accept read or write concern"},
+ listDatabasesForAllTenants: {skip: "does not accept read or write concern"},
listIndexes: {skip: "does not accept read or write concern"},
listShards: {skip: "does not accept read or write concern"},
lockInfo: {skip: "does not accept read or write concern"},
diff --git a/src/mongo/db/commands/SConscript b/src/mongo/db/commands/SConscript
index e9772f9b61f..807f9da817e 100644
--- a/src/mongo/db/commands/SConscript
+++ b/src/mongo/db/commands/SConscript
@@ -278,6 +278,18 @@ env.Library(
)
env.Library(
+ target='list_databases_for_all_tenants_command',
+ source=[
+ 'list_databases_for_all_tenants.idl',
+ ],
+ LIBDEPS_PRIVATE=[
+ '$BUILD_DIR/mongo/base',
+ '$BUILD_DIR/mongo/db/auth/authprivilege',
+ '$BUILD_DIR/mongo/idl/idl_parser',
+ ],
+)
+
+env.Library(
target='create_command',
source=[
'create.idl',
@@ -348,6 +360,7 @@ env.Library(
"lock_info.cpp",
"list_collections.cpp",
"list_databases.cpp",
+ 'list_databases_for_all_tenants.cpp',
"list_indexes.cpp",
"pipeline_command.cpp",
"plan_cache_clear_command.cpp",
@@ -425,6 +438,7 @@ env.Library(
'kill_common',
'list_collections_filter',
'list_databases_command',
+ 'list_databases_for_all_tenants_command',
'rename_collection_idl',
'test_commands_enabled',
'validate_db_metadata_command',
diff --git a/src/mongo/db/commands/list_databases.cpp b/src/mongo/db/commands/list_databases.cpp
index d454817654c..580ab22f094 100644
--- a/src/mongo/db/commands/list_databases.cpp
+++ b/src/mongo/db/commands/list_databases.cpp
@@ -31,14 +31,12 @@
#include "mongo/db/auth/authorization_session.h"
#include "mongo/db/catalog/database_holder.h"
-#include "mongo/db/catalog_raii.h"
#include "mongo/db/client.h"
#include "mongo/db/commands.h"
+#include "mongo/db/commands/list_databases_common.h"
#include "mongo/db/commands/list_databases_gen.h"
-#include "mongo/db/concurrency/exception_util.h"
#include "mongo/db/curop_failpoint_helpers.h"
#include "mongo/db/database_name.h"
-#include "mongo/db/db_raii.h"
#include "mongo/db/matcher/expression.h"
#include "mongo/db/namespace_string.h"
#include "mongo/db/operation_context.h"
@@ -55,7 +53,6 @@ namespace {
// Failpoint which causes to hang "listDatabases" cmd after acquiring global lock in IS mode.
MONGO_FAIL_POINT_DEFINE(hangBeforeListDatabases);
-constexpr auto kName = "name"_sd;
class CmdListDatabases final : public ListDatabasesCmdVersion1Gen<CmdListDatabases> {
public:
AllowedOnSecondary secondaryAllowed(ServiceContext*) const final {
@@ -112,16 +109,7 @@ public:
})(cmd.getAuthorizedDatabases());
// {filter: matchExpression}.
- std::unique_ptr<MatchExpression> filter;
- if (auto filterObj = cmd.getFilter()) {
- // The collator is null because database metadata objects are compared using simple
- // binary comparison.
- auto expCtx = make_intrusive<ExpressionContext>(
- opCtx, std::unique_ptr<CollatorInterface>(nullptr), ns());
- auto matcher = uassertStatusOK(
- MatchExpressionParser::parse(filterObj.value(), std::move(expCtx)));
- filter = std::move(matcher);
- }
+ std::unique_ptr<MatchExpression> filter = list_databases::getFilter(cmd, opCtx, ns());
std::vector<DatabaseName> dbNames;
StorageEngine* storageEngine = getGlobalServiceContext()->getStorageEngine();
@@ -133,45 +121,14 @@ public:
}
std::vector<ListDatabasesReplyItem> items;
-
- const bool filterNameOnly = filter &&
- filter->getCategory() == MatchExpression::MatchCategory::kLeaf &&
- filter->path() == kName;
- long long totalSize = 0;
- for (const auto& dbName : dbNames) {
- if (authorizedDatabases &&
- !as->isAuthorizedForAnyActionOnAnyResourceInDB(dbName.toString())) {
- // We don't have listDatabases on the cluster or find on this database.
- continue;
- }
-
- ListDatabasesReplyItem item(dbName.db());
-
- long long size = 0;
- if (!nameOnly) {
- // Filtering on name only should not require taking locks on filtered-out names.
- if (filterNameOnly && !filter->matchesBSON(item.toBSON())) {
- continue;
- }
-
- AutoGetDbForReadMaybeLockFree lockFreeReadBlock(opCtx, dbName);
- // The database could have been dropped since we called 'listDatabases()' above.
- if (!DatabaseHolder::get(opCtx)->dbExists(opCtx, dbName)) {
- continue;
- }
-
- writeConflictRetry(opCtx, "sizeOnDisk", dbName.toString(), [&] {
- size = storageEngine->sizeOnDiskForDb(opCtx, dbName);
- });
- item.setSizeOnDisk(size);
- item.setEmpty(
- CollectionCatalog::get(opCtx)->getAllCollectionUUIDsFromDb(dbName).empty());
- }
- if (!filter || filter->matchesBSON(item.toBSON())) {
- totalSize += size;
- items.push_back(std::move(item));
- }
- }
+ int64_t totalSize = list_databases::setReplyItems(opCtx,
+ dbNames,
+ items,
+ storageEngine,
+ nameOnly,
+ filter,
+ false /* setTenantId */,
+ authorizedDatabases);
ListDatabasesReply reply(items);
if (!nameOnly) {
diff --git a/src/mongo/db/commands/list_databases_common.h b/src/mongo/db/commands/list_databases_common.h
new file mode 100644
index 00000000000..14dc0884689
--- /dev/null
+++ b/src/mongo/db/commands/list_databases_common.h
@@ -0,0 +1,134 @@
+/**
+ * Copyright (C) 2022-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * 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
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * 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 Server Side 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.
+ */
+#pragma once
+
+#include "mongo/db/auth/authorization_session.h"
+#include "mongo/db/catalog/database_holder.h"
+#include "mongo/db/catalog_raii.h"
+#include "mongo/db/client.h"
+#include "mongo/db/commands/list_databases_for_all_tenants_gen.h"
+#include "mongo/db/commands/list_databases_gen.h"
+#include "mongo/db/concurrency/exception_util.h"
+#include "mongo/db/db_raii.h"
+#include "mongo/db/matcher/expression.h"
+#include "mongo/db/namespace_string.h"
+#include "mongo/db/operation_context.h"
+#include "mongo/db/service_context.h"
+#include "mongo/db/storage/storage_engine.h"
+
+
+namespace mongo {
+
+namespace list_databases {
+
+constexpr auto kName = "name"_sd;
+
+// Initialize ListDatabasesForAllTenantsReplyItem reply item by setting the tenantId
+void inline initializeItemWithTenantId(ListDatabasesForAllTenantsReplyItem& item,
+ const DatabaseName& dbName) {
+ item.setTenantId(dbName.tenantId());
+}
+// Do nothing if provided ListDatabasesReplyItem to satisy the compiler
+void inline initializeItemWithTenantId(ListDatabasesReplyItem& item, const DatabaseName& dbName) {
+ /* No-op */
+}
+
+template <class CommandType>
+std::unique_ptr<MatchExpression> getFilter(CommandType cmd,
+ OperationContext* opCtx,
+ NamespaceString ns) {
+ if (auto filterObj = cmd.getFilter()) {
+ // The collator is null because database metadata objects are compared using simple
+ // binary comparison.
+ auto expCtx = make_intrusive<ExpressionContext>(
+ opCtx, std::unique_ptr<CollatorInterface>(nullptr), ns);
+ auto matcher =
+ uassertStatusOK(MatchExpressionParser::parse(filterObj.get(), std::move(expCtx)));
+ return matcher;
+ }
+ return std::unique_ptr<MatchExpression>{};
+}
+
+template <typename ReplyItemType>
+int64_t setReplyItems(OperationContext* opCtx,
+ const std::vector<DatabaseName>& dbNames,
+ std::vector<ReplyItemType>& items,
+ StorageEngine* storageEngine,
+ bool nameOnly,
+ const std::unique_ptr<MatchExpression>& filter,
+ bool setTenantId,
+ bool authorizedDatabases) {
+ auto* as = AuthorizationSession::get(opCtx->getClient());
+
+ const bool filterNameOnly = filter &&
+ filter->getCategory() == MatchExpression::MatchCategory::kLeaf && filter->path() == kName;
+ int64_t totalSize = 0;
+
+ for (const auto& dbName : dbNames) {
+ if (authorizedDatabases &&
+ !as->isAuthorizedForAnyActionOnAnyResourceInDB(dbName.toString())) {
+ // We don't have listDatabases on the cluster or find on this database.
+ continue;
+ }
+
+ ReplyItemType item(dbName.db());
+ if (setTenantId) {
+ initializeItemWithTenantId(item, dbName);
+ }
+
+ int64_t size = 0;
+ if (!nameOnly) {
+ // Filtering on name only should not require taking locks on filtered-out names.
+ if (filterNameOnly && !filter->matchesBSON(item.toBSON())) {
+ continue;
+ }
+
+ AutoGetDbForReadMaybeLockFree lockFreeReadBlock(opCtx, dbName);
+ // The database could have been dropped since we called 'listDatabases()' originally.
+ if (!DatabaseHolder::get(opCtx)->dbExists(opCtx, dbName)) {
+ continue;
+ }
+
+ writeConflictRetry(opCtx, "sizeOnDisk", dbName.toString(), [&] {
+ size = storageEngine->sizeOnDiskForDb(opCtx, dbName);
+ });
+ item.setSizeOnDisk(size);
+ item.setEmpty(
+ CollectionCatalog::get(opCtx)->getAllCollectionUUIDsFromDb(dbName).empty());
+ }
+ if (!filter || filter->matchesBSON(item.toBSON())) {
+ totalSize += size;
+ items.push_back(std::move(item));
+ }
+ }
+ return totalSize;
+}
+
+} // namespace list_databases
+} // namespace mongo
diff --git a/src/mongo/db/commands/list_databases_for_all_tenants.cpp b/src/mongo/db/commands/list_databases_for_all_tenants.cpp
new file mode 100644
index 00000000000..c7574aef1b4
--- /dev/null
+++ b/src/mongo/db/commands/list_databases_for_all_tenants.cpp
@@ -0,0 +1,142 @@
+/**
+ * Copyright (C) 2022-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * 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
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * 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 Server Side 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.
+ */
+
+#include "mongo/platform/basic.h"
+
+#include "mongo/db/auth/authorization_session.h"
+#include "mongo/db/catalog/database_holder.h"
+#include "mongo/db/client.h"
+#include "mongo/db/commands.h"
+#include "mongo/db/commands/list_databases_common.h"
+#include "mongo/db/commands/list_databases_for_all_tenants_gen.h"
+#include "mongo/db/database_name.h"
+#include "mongo/db/matcher/expression.h"
+#include "mongo/db/multitenancy_gen.h"
+#include "mongo/db/namespace_string.h"
+#include "mongo/db/operation_context.h"
+#include "mongo/db/service_context.h"
+#include "mongo/db/storage/storage_engine.h"
+
+namespace mongo {
+
+namespace {
+
+class CmdListDatabasesForAllTenants final : public TypedCommand<CmdListDatabasesForAllTenants> {
+public:
+ using Request = ListDatabasesForAllTenantsCommand;
+ using Reply = ListDatabasesForAllTenantsReply;
+
+ AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
+ return AllowedOnSecondary::kOptIn;
+ }
+
+ bool adminOnly() const override {
+ return true;
+ }
+
+ bool maintenanceOk() const override {
+ return false;
+ }
+
+ std::string help() const override {
+ return "{ listDatabasesForAllTenants:1, [filter: <filterObject>] [, nameOnly: true ] }\n"
+ "command which lists databases for all tenants on this server";
+ }
+
+ bool skipApiVersionCheck() const override {
+ return true;
+ }
+
+ const AuthorizationContract* getAuthorizationContract() const final {
+ return &Request::kAuthorizationContract;
+ }
+
+ class Invocation final : public InvocationBase {
+ public:
+ using InvocationBase::InvocationBase;
+
+ bool supportsWriteConcern() const override {
+ return false;
+ }
+
+ NamespaceString ns() const override {
+ return NamespaceString(request().getDbName());
+ }
+
+ void doCheckAuthorization(OperationContext* opCtx) const override {
+ AuthorizationSession* authzSession = AuthorizationSession::get(opCtx->getClient());
+ uassert(ErrorCodes::Unauthorized,
+ "Unauthorized",
+ authzSession->isAuthorizedForActionsOnResource(
+ ResourcePattern::forClusterResource(), ActionType::internal));
+ }
+
+ Reply typedRun(OperationContext* opCtx) {
+ CommandHelpers::handleMarkKillOnClientDisconnect(opCtx);
+ uassert(ErrorCodes::CommandNotSupported,
+ "Multitenancy not enabled, command not supported",
+ gMultitenancySupport);
+
+ auto cmd = request();
+
+ // {nameOnly: bool} - default false.
+ const bool nameOnly = cmd.getNameOnly();
+
+ // {filter: matchExpression}.
+ std::unique_ptr<MatchExpression> filter = list_databases::getFilter(cmd, opCtx, ns());
+
+ std::vector<DatabaseName> dbNames;
+ StorageEngine* storageEngine = getGlobalServiceContext()->getStorageEngine();
+ {
+ Lock::GlobalLock lk(opCtx, MODE_IS);
+ dbNames = storageEngine->listDatabases();
+ }
+
+ std::vector<ListDatabasesForAllTenantsReplyItem> items;
+ int64_t totalSize = list_databases::setReplyItems(opCtx,
+ dbNames,
+ items,
+ storageEngine,
+ nameOnly,
+ filter,
+ true /* setTenantId */,
+ false /* authorizedDatabases*/);
+
+ Reply reply(items);
+ if (!nameOnly) {
+ reply.setTotalSize(totalSize);
+ reply.setTotalSizeMb(totalSize / (1024 * 1024));
+ }
+
+ return reply;
+ }
+ };
+} CmdListDatabasesForAllTenants;
+} // namespace
+} // namespace mongo
diff --git a/src/mongo/db/commands/list_databases_for_all_tenants.idl b/src/mongo/db/commands/list_databases_for_all_tenants.idl
new file mode 100644
index 00000000000..a5be6fb29b2
--- /dev/null
+++ b/src/mongo/db/commands/list_databases_for_all_tenants.idl
@@ -0,0 +1,101 @@
+# Copyright (C) 2022-present MongoDB, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the Server Side Public License, version 1,
+# as published by MongoDB, Inc.
+#
+# 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
+# Server Side Public License for more details.
+#
+# You should have received a copy of the Server Side Public License
+# along with this program. If not, see
+# <http://www.mongodb.com/licensing/server-side-public-license>.
+#
+# 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 Server Side 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.
+#
+global:
+ cpp_namespace: "mongo"
+
+
+imports:
+ - "mongo/db/auth/access_checks.idl"
+ - "mongo/db/auth/action_type.idl"
+ - "mongo/idl/basic_types.idl"
+
+structs:
+ ListDatabasesForAllTenantsReplyItem:
+ description: "Individual result"
+ fields:
+ name:
+ type: string
+ unstable: false
+ tenantId:
+ type: tenant_id
+ optional: true
+ unstable: false
+ sizeOnDisk:
+ type: long
+ optional: true
+ unstable: false
+ empty:
+ type: bool
+ optional: true
+ unstable: false
+ shards:
+ type: object_owned
+ optional: true
+ unstable: false
+
+ ListDatabasesForAllTenantsReply:
+ description: "The listDatabasesForAllTenants command's reply."
+ fields:
+ databases:
+ type: array<ListDatabasesForAllTenantsReplyItem>
+ unstable: false
+ totalSize:
+ type: long
+ optional: true
+ unstable: false
+ totalSizeMb:
+ type: long
+ optional: true
+ unstable: false
+
+commands:
+ listDatabasesForAllTenants:
+ description: "listDatabasesForAllTenants Command: lists all databases for all tenants and
+ can only be run if authenticated with internal __system role"
+ command_name: "listDatabasesForAllTenants"
+ cpp_name: ListDatabasesForAllTenantsCommand
+ namespace: ignored
+ api_version: ""
+ access_check:
+ complex:
+ - privilege:
+ resource_pattern: cluster
+ action_type: internal
+ strict: false
+ fields:
+ nameOnly:
+ description: "Return just the database name without metadata"
+ type: safeBool
+ default: false
+ unstable: false
+ filter:
+ description: "Filter description to limit results"
+ type: object
+ optional: true
+ unstable: false
+ reply_type: ListDatabasesForAllTenantsReply
diff --git a/src/mongo/embedded/mongo_embedded/mongo_embedded_test.cpp b/src/mongo/embedded/mongo_embedded/mongo_embedded_test.cpp
index 710e4fbbc2f..c875b0d16b0 100644
--- a/src/mongo/embedded/mongo_embedded/mongo_embedded_test.cpp
+++ b/src/mongo/embedded/mongo_embedded/mongo_embedded_test.cpp
@@ -592,6 +592,7 @@ TEST_F(MongodbCAPITest, RunListCommands) {
"listCollections",
"listCommands",
"listDatabases",
+ "listDatabasesForAllTenants",
"listIndexes",
"lockInfo",
"logMessage",