diff options
-rw-r--r-- | buildscripts/idl/idl_check_compatibility.py | 1 | ||||
-rw-r--r-- | jstests/core/views/views_all_commands.js | 1 | ||||
-rw-r--r-- | jstests/replsets/db_reads_while_recovering_all_commands.js | 6 | ||||
-rw-r--r-- | jstests/serverless/list_databases_for_all_tenants.js | 259 | ||||
-rw-r--r-- | jstests/sharding/libs/last_lts_mongod_commands.js | 1 | ||||
-rw-r--r-- | jstests/sharding/read_write_concern_defaults_application.js | 1 | ||||
-rw-r--r-- | src/mongo/db/commands/SConscript | 14 | ||||
-rw-r--r-- | src/mongo/db/commands/list_databases.cpp | 63 | ||||
-rw-r--r-- | src/mongo/db/commands/list_databases_common.h | 134 | ||||
-rw-r--r-- | src/mongo/db/commands/list_databases_for_all_tenants.cpp | 142 | ||||
-rw-r--r-- | src/mongo/db/commands/list_databases_for_all_tenants.idl | 101 | ||||
-rw-r--r-- | src/mongo/embedded/mongo_embedded/mongo_embedded_test.cpp | 1 |
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", |