diff options
-rw-r--r-- | jstests/auth/security_token.js | 7 | ||||
-rw-r--r-- | src/mongo/client/dbclient_base.cpp | 7 | ||||
-rw-r--r-- | src/mongo/db/SConscript | 6 | ||||
-rw-r--r-- | src/mongo/db/auth/security_token.cpp | 20 | ||||
-rw-r--r-- | src/mongo/db/commands/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/db/commands/user_management_commands.cpp | 3 | ||||
-rw-r--r-- | src/mongo/db/commands/user_management_commands_common.cpp | 10 | ||||
-rw-r--r-- | src/mongo/db/dollar_tenant_decoration_test.cpp | 151 | ||||
-rw-r--r-- | src/mongo/db/multitenancy.cpp | 45 | ||||
-rw-r--r-- | src/mongo/db/multitenancy.h | 6 | ||||
-rw-r--r-- | src/mongo/db/tenant_namespace_test.cpp | 2 | ||||
-rw-r--r-- | src/mongo/executor/network_interface_tl.cpp | 3 | ||||
-rw-r--r-- | src/mongo/rpc/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/rpc/metadata.cpp | 3 |
14 files changed, 240 insertions, 25 deletions
diff --git a/jstests/auth/security_token.js b/jstests/auth/security_token.js index e0972118af7..4abf08e3cbf 100644 --- a/jstests/auth/security_token.js +++ b/jstests/auth/security_token.js @@ -43,8 +43,11 @@ function makeTokenAndExpect(user, db) { function runTest(conn, enabled, rst = undefined) { const admin = conn.getDB('admin'); const tenantAdmin = conn.getDB(tenantID.str + '_admin'); - assert.commandWorked(admin.runCommand({createUser: 'admin', pwd: 'admin', roles: ['root']})); - assert(admin.auth('admin', 'admin')); + + // Must be authenticated as the internal __system user in order to use $tenant + assert.commandWorked( + admin.runCommand({createUser: 'system', pwd: 'system', roles: ['__system']})); + assert(admin.auth('system', 'system')); // Create a tenant-local user. const createUserCmd = diff --git a/src/mongo/client/dbclient_base.cpp b/src/mongo/client/dbclient_base.cpp index 98c49806f40..70c500fd412 100644 --- a/src/mongo/client/dbclient_base.cpp +++ b/src/mongo/client/dbclient_base.cpp @@ -186,8 +186,11 @@ void appendMetadata(OperationContext* opCtx, } request.body = bob.obj(); - if (auto securityToken = auth::getSecurityToken(opCtx)) { - request.securityToken = securityToken->toBSON(); + + if (opCtx) { + if (auto securityToken = auth::getSecurityToken(opCtx)) { + request.securityToken = securityToken->toBSON(); + } } } } // namespace diff --git a/src/mongo/db/SConscript b/src/mongo/db/SConscript index 2c6e794531f..e126fafcda4 100644 --- a/src/mongo/db/SConscript +++ b/src/mongo/db/SConscript @@ -85,7 +85,9 @@ env.Library( ], LIBDEPS=[ '$BUILD_DIR/mongo/base', + 'auth/auth', 'auth/security_token', + 'multitenancy_params', ], ) @@ -1013,6 +1015,7 @@ env.Library( 'commands/server_status_core', 'initialize_api_parameters', 'introspect', + 'multitenancy', 'not_primary_error_tracker', 'query_exec', 'repl/repl_server_parameters', @@ -2485,6 +2488,7 @@ if wiredtiger: 'dbmessage_test.cpp', 'db_raii_test.cpp', 'db_raii_multi_collection_test.cpp', + 'dollar_tenant_decoration_test.cpp', "explain_test.cpp", 'field_parser_test.cpp', 'field_ref_set_test.cpp', @@ -2537,6 +2541,7 @@ if wiredtiger: '$BUILD_DIR/mongo/bson/util/bson_extract', '$BUILD_DIR/mongo/client/read_preference', '$BUILD_DIR/mongo/db/auth/auth', + '$BUILD_DIR/mongo/db/auth/security_token', '$BUILD_DIR/mongo/db/catalog/catalog_test_fixture', '$BUILD_DIR/mongo/db/catalog/import_collection_oplog_entry', '$BUILD_DIR/mongo/db/catalog/index_build_entry_idl', @@ -2574,6 +2579,7 @@ if wiredtiger: 'logical_session_id_helpers', 'logical_time', 'mirror_maestro', + 'multitenancy', 'multitenancy_params', 'namespace_string', 'op_observer', diff --git a/src/mongo/db/auth/security_token.cpp b/src/mongo/db/auth/security_token.cpp index b38fe1e0b9b..7ab41457a21 100644 --- a/src/mongo/db/auth/security_token.cpp +++ b/src/mongo/db/auth/security_token.cpp @@ -52,13 +52,19 @@ MONGO_INITIALIZER(SecurityTokenOptionValidate)(InitializerContext*) { if (gMultitenancySupport) { logv2::detail::setGetTenantIDCallback([]() -> boost::optional<OID> { auto* client = Client::getCurrent(); - auto* opCtx = client ? client->getOperationContext() : nullptr; - auto token = getSecurityToken(opCtx); - if (token) { - return token->getAuthenticatedUser().getTenant(); - } else { + if (!client) return boost::none; + + if (auto* opCtx = client->getOperationContext()) { + auto token = getSecurityToken(opCtx); + if (token) { + return token->getAuthenticatedUser().getTenant(); + } else { + return boost::none; + } } + + return boost::none; }); } } @@ -114,10 +120,6 @@ void readSecurityTokenMetadata(OperationContext* opCtx, BSONObj securityToken) t } MaybeSecurityToken getSecurityToken(OperationContext* opCtx) { - if (!opCtx) { - return boost::none; - } - return securityTokenDecoration(opCtx); } diff --git a/src/mongo/db/commands/SConscript b/src/mongo/db/commands/SConscript index c8e94e111c9..4735b9f4080 100644 --- a/src/mongo/db/commands/SConscript +++ b/src/mongo/db/commands/SConscript @@ -530,6 +530,7 @@ env.Library( '$BUILD_DIR/mongo/db/exec/sbe_cmd', '$BUILD_DIR/mongo/db/exec/stagedebug_cmd', '$BUILD_DIR/mongo/db/index_builds_coordinator_interface', + '$BUILD_DIR/mongo/db/multitenancy', '$BUILD_DIR/mongo/db/pipeline/pipeline', '$BUILD_DIR/mongo/db/pipeline/process_interface/mongo_process_interface', '$BUILD_DIR/mongo/db/repl/dbcheck', diff --git a/src/mongo/db/commands/user_management_commands.cpp b/src/mongo/db/commands/user_management_commands.cpp index 2a5f37ae267..a9a08d44856 100644 --- a/src/mongo/db/commands/user_management_commands.cpp +++ b/src/mongo/db/commands/user_management_commands.cpp @@ -65,6 +65,7 @@ #include "mongo/db/curop.h" #include "mongo/db/dbdirectclient.h" #include "mongo/db/jsobj.h" +#include "mongo/db/multitenancy.h" #include "mongo/db/operation_context.h" #include "mongo/db/ops/write_ops.h" #include "mongo/db/pipeline/aggregation_request_helper.h" @@ -1020,7 +1021,7 @@ void CmdUMCTyped<CreateUserCommand>::Invocation::typedRun(OperationContext* opCt uassert(ErrorCodes::BadValue, "Username cannot contain NULL characters", cmd.getCommandParameter().find('\0') == std::string::npos); - UserName userName(cmd.getCommandParameter(), dbname, cmd.getTenantOverride()); + UserName userName(cmd.getCommandParameter(), dbname, getActiveTenant(opCtx)); uassert(ErrorCodes::BadValue, "Must provide a 'pwd' field for all user documents, except those" diff --git a/src/mongo/db/commands/user_management_commands_common.cpp b/src/mongo/db/commands/user_management_commands_common.cpp index e6c4539a724..4645c22d506 100644 --- a/src/mongo/db/commands/user_management_commands_common.cpp +++ b/src/mongo/db/commands/user_management_commands_common.cpp @@ -191,16 +191,6 @@ void checkAuthForTypedCommand(OperationContext* opCtx, const CreateUserCommand& as->isAuthorizedForActionsOnResource(ResourcePattern::forDatabaseName(dbname), ActionType::createUser)); - if (request.getTenantOverride() != boost::none) { - const bool isNotTokenAuth = (as->getAuthenticationMode() != - AuthorizationSession::AuthenticationMode::kSecurityToken); - - uassert(ErrorCodes::Unauthorized, - "$tenant parameter to createUser command only accepted in " - "test mode with security tokens enabled but not in use", - getTestCommandsEnabled() && gMultitenancySupport && isNotTokenAuth); - } - auto resolvedRoles = resolveRoleNames(request.getRoles(), dbname); uassertStatusOK(checkAuthorizedToGrantRoles(as, resolvedRoles)); diff --git a/src/mongo/db/dollar_tenant_decoration_test.cpp b/src/mongo/db/dollar_tenant_decoration_test.cpp new file mode 100644 index 00000000000..161164710cc --- /dev/null +++ b/src/mongo/db/dollar_tenant_decoration_test.cpp @@ -0,0 +1,151 @@ +/** + * 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/bson/oid.h" +#include "mongo/db/auth/authorization_manager_impl.h" +#include "mongo/db/auth/authorization_session.h" +#include "mongo/db/auth/authz_manager_external_state_mock.h" +#include "mongo/db/auth/security_token.h" +#include "mongo/db/multitenancy.h" +#include "mongo/db/multitenancy_gen.h" +#include "mongo/db/service_context_test_fixture.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace { + +class DollarTenantDecorationTest : public ScopedGlobalServiceContextForTest, public unittest::Test { +protected: + void setUp() final { + auto authzManagerState = std::make_unique<AuthzManagerExternalStateMock>(); + auto authzManager = std::make_unique<AuthorizationManagerImpl>( + getServiceContext(), std::move(authzManagerState)); + authzManager->setAuthEnabled(true); + AuthorizationManager::set(getServiceContext(), std::move(authzManager)); + + client = getServiceContext()->makeClient("test"); + opCtxPtr = getServiceContext()->makeOperationContext(client.get()); + opCtx = opCtxPtr.get(); + } + + BSONObj makeSecurityToken(const UserName& userName) { + auto authUser = userName.toBSON(true /* serialize token */); + ASSERT_EQ(authUser["tenant"_sd].type(), jstOID); + + BSONObjBuilder token; + token.append("authenticatedUser", authUser); + + auto block = + SHA256Block::computeHash({ConstDataRange(authUser.objdata(), authUser.objsize())}); + token.appendBinData("sig", block.size(), BinDataGeneral, block.data()); + + return token.obj(); + } + + ServiceContext::UniqueClient client; + ServiceContext::UniqueOperationContext opCtxPtr; + OperationContext* opCtx; +}; + +TEST_F(DollarTenantDecorationTest, ParseDollarTenantFromRequestSecurityTokenAlreadySet) { + gMultitenancySupport = true; + + // Ensure the security token is set on the opCtx. + const auto kOid = OID::gen(); + auto token = makeSecurityToken(UserName("user", "admin", kOid)); + auth::readSecurityTokenMetadata(opCtx, token); + ASSERT(getActiveTenant(opCtx)); + ASSERT_EQ(*getActiveTenant(opCtx), kOid); + + // TODO SERVER-62406 use the new ActionType for use with $tenant. + // Grant internal auth so that we're authenticated as the internal __system user. + AuthorizationSession::get(opCtx->getClient())->grantInternalAuthorization(opCtx); + + // The dollarTenantDecoration should not be set because the security token is already set. + const auto kOidParameter = OID::gen(); + auto opMsgRequest = OpMsgRequest::fromDBAndBody("test", BSON("$tenant" << kOidParameter)); + ASSERT_THROWS_CODE( + parseDollarTenantFromRequest(opCtx, opMsgRequest), AssertionException, 6223901); + + // getActiveTenant should still return the tenantId in the security token. + ASSERT(getActiveTenant(opCtx)); + ASSERT_EQ(*getActiveTenant(opCtx), kOid); +} + +TEST_F(DollarTenantDecorationTest, ParseDollarTenantFromRequestNotInternalSecurityUser) { + gMultitenancySupport = true; + const auto kOid = OID::gen(); + + // We are not authenticated as the internal security user. + auto opMsgRequest = OpMsgRequest::fromDBAndBody("test", BSON("$tenant" << kOid)); + ASSERT_THROWS_CODE(parseDollarTenantFromRequest(opCtx, opMsgRequest), + AssertionException, + ErrorCodes::Unauthorized); + ASSERT(!getActiveTenant(opCtx)); +} + +TEST_F(DollarTenantDecorationTest, ParseDollarTenantMultitenancySupportDisabled) { + gMultitenancySupport = false; + const auto kOid = OID::gen(); + + // TODO SERVER-62406 use the new ActionType for use with $tenant. + // Grant internal auth so that we're authenticated as the internal __system user. + AuthorizationSession::get(opCtx->getClient())->grantInternalAuthorization(opCtx); + + // TenantId is passed as the '$tenant' parameter. "multitenancySupport" is disabled, so we + // should throw when attempting to set this tenantId on the opCtx. + auto opMsgRequestParameter = OpMsgRequest::fromDBAndBody("test", BSON("$tenant" << kOid)); + ASSERT_THROWS_CODE(parseDollarTenantFromRequest(opCtx, opMsgRequestParameter), + AssertionException, + ErrorCodes::InvalidOptions); + ASSERT(!getActiveTenant(opCtx)); +} + +TEST_F(DollarTenantDecorationTest, ParseDollarTenantFromRequestSuccess) { + gMultitenancySupport = true; + const auto kOid = OID::gen(); + + // TODO SERVER-62406 use the new ActionType for use with $tenant. + // Grant internal auth so that we're authenticated as the internal __system user. + AuthorizationSession::get(opCtx->getClient())->grantInternalAuthorization(opCtx); + + // The tenantId should be successfully set because "multitenancySupport" is enabled and we're + // authenticated as the internal __system user. + auto opMsgRequest = OpMsgRequest::fromDBAndBody("test", BSON("$tenant" << kOid)); + parseDollarTenantFromRequest(opCtx, opMsgRequest); + + auto tenantId = getActiveTenant(opCtx); + ASSERT(tenantId); + ASSERT_EQ(*tenantId, kOid); +} + +} // namespace +} // namespace mongo diff --git a/src/mongo/db/multitenancy.cpp b/src/mongo/db/multitenancy.cpp index fbe5d6e651a..ddc849ae3e5 100644 --- a/src/mongo/db/multitenancy.cpp +++ b/src/mongo/db/multitenancy.cpp @@ -26,10 +26,15 @@ * exception statement from all source files in the program, then also delete * it in the license file. */ +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kDefault #include "mongo/db/multitenancy.h" +#include "mongo/bson/oid.h" +#include "mongo/db/auth/authorization_session.h" #include "mongo/db/auth/security_token.h" +#include "mongo/db/multitenancy_gen.h" +#include "mongo/logv2/log.h" namespace mongo { @@ -38,12 +43,50 @@ const OID kSystemTenantID( "0102030405" /* process id */ "060708" /* counter */); +// Holds the tenantId for the operation if it was provided in the request on the $tenant field only +// if the tenantId was not also provided in the security token. +const auto dollarTenantDecoration = + OperationContext::declareDecoration<boost::optional<mongo::OID>>(); + +void parseDollarTenantFromRequest(OperationContext* opCtx, const OpMsg& request) { + // The internal security user is allowed to run commands on behalf of a tenant by passing + // the tenantId in the "$tenant" field. + auto tenantElem = request.body["$tenant"]; + if (!tenantElem) + return; + + uassert(ErrorCodes::InvalidOptions, + "Multitenancy not enabled, cannot set $tenant in command body", + gMultitenancySupport); + + // TODO SERVER-62406 check for the new ActionType for use with $tenant. + uassert(ErrorCodes::Unauthorized, + "'$tenant' may only be specified with the internal action type", + AuthorizationSession::get(opCtx->getClient()) + ->isAuthorizedForActionsOnResource(ResourcePattern::forClusterResource(), + ActionType::internal)); + + auto tenantId = tenantElem.OID(); + + uassert(6223901, + str::stream() << "Cannot pass $tenant id if also passing securityToken, securityToken: " + << auth::getSecurityToken(opCtx)->getAuthenticatedUser().getTenant() + << " $tenant: " << tenantId, + !auth::getSecurityToken(opCtx)); + + + dollarTenantDecoration(opCtx) = std::move(tenantId); + LOGV2_DEBUG( + 6223900, 4, "Setting tenantId from $tenant request parameter", "tenantId"_attr = tenantId); +} + boost::optional<OID> getActiveTenant(OperationContext* opCtx) { auto token = auth::getSecurityToken(opCtx); if (!token) { - return boost::none; + return dollarTenantDecoration(opCtx); } + invariant(!dollarTenantDecoration(opCtx)); return token->getAuthenticatedUser().getTenant(); } diff --git a/src/mongo/db/multitenancy.h b/src/mongo/db/multitenancy.h index f107bb6dec0..ba537108d79 100644 --- a/src/mongo/db/multitenancy.h +++ b/src/mongo/db/multitenancy.h @@ -46,6 +46,12 @@ namespace mongo { extern const OID kSystemTenantID; /** + * Parses the tenantId from the '$tenant' field in the request if it exists and + * "multitenancySupport" is enabled. Then, sets the parsed tenantId on the opCtx. + */ +void parseDollarTenantFromRequest(OperationContext* opCtx, const OpMsg& request); + +/** * Extract the active TenantID for this operation. */ boost::optional<OID> getActiveTenant(OperationContext* opCtx); diff --git a/src/mongo/db/tenant_namespace_test.cpp b/src/mongo/db/tenant_namespace_test.cpp index d3f3f418ba5..6b4917422af 100644 --- a/src/mongo/db/tenant_namespace_test.cpp +++ b/src/mongo/db/tenant_namespace_test.cpp @@ -40,6 +40,7 @@ namespace mongo { namespace { TEST(TenantNamespaceTest, TenantNamespaceMultitenancySupportDisabledBasic) { + RAIIServerParameterControllerForTest multitenanyController("multitenancySupport", false); TenantNamespace tenantNs(boost::none, NamespaceString("a.b")); ASSERT(!tenantNs.tenantId()); ASSERT_EQUALS(std::string("a"), tenantNs.db()); @@ -48,6 +49,7 @@ TEST(TenantNamespaceTest, TenantNamespaceMultitenancySupportDisabledBasic) { } TEST(TenantNamespaceTest, TenantNamespaceParseFromDiskMultitenancySupportDisabled) { + RAIIServerParameterControllerForTest multitenanyController("multitenancySupport", false); TenantNamespace tenantNs = TenantNamespace::parseTenantNamespaceFromDisk("a.b"); ASSERT(!tenantNs.tenantId()); ASSERT_EQUALS(std::string("a"), tenantNs.db()); diff --git a/src/mongo/executor/network_interface_tl.cpp b/src/mongo/executor/network_interface_tl.cpp index 8b749126060..5fa9a00e6cd 100644 --- a/src/mongo/executor/network_interface_tl.cpp +++ b/src/mongo/executor/network_interface_tl.cpp @@ -64,6 +64,9 @@ Status appendMetadata(RemoteCommandRequestOnAny* request, request->metadata = bob.obj(); } + if (!request->opCtx) + return Status::OK(); + if (auto securityToken = auth::getSecurityToken(request->opCtx)) { request->securityToken = securityToken->toBSON(); } diff --git a/src/mongo/rpc/SConscript b/src/mongo/rpc/SConscript index e8a35204e4d..1b07ad5c2f3 100644 --- a/src/mongo/rpc/SConscript +++ b/src/mongo/rpc/SConscript @@ -130,6 +130,7 @@ env.Library( 'metadata_impersonated_user', ], LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/db/multitenancy', '$BUILD_DIR/mongo/db/vector_clock', ], ) diff --git a/src/mongo/rpc/metadata.cpp b/src/mongo/rpc/metadata.cpp index fa7911f59b5..ba002cdc748 100644 --- a/src/mongo/rpc/metadata.cpp +++ b/src/mongo/rpc/metadata.cpp @@ -37,6 +37,7 @@ #include "mongo/db/dbmessage.h" #include "mongo/db/jsobj.h" #include "mongo/db/logical_time_validator.h" +#include "mongo/db/multitenancy.h" #include "mongo/db/vector_clock.h" #include "mongo/rpc/metadata/client_metadata.h" #include "mongo/rpc/metadata/impersonated_user_metadata.h" @@ -92,6 +93,8 @@ void readRequestMetadata(OperationContext* opCtx, const OpMsg& opMsg, bool cmdRe readImpersonatedUserMetadata(impersonationElem, opCtx); auth::readSecurityTokenMetadata(opCtx, opMsg.securityToken); + parseDollarTenantFromRequest(opCtx, opMsg); + // We check for "$client" but not "client" here, because currentOp can filter on "client" as // a top-level field. if (clientElem) { |