diff options
author | Sara Golemon <sara.golemon@mongodb.com> | 2022-06-16 14:15:24 -0500 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2022-07-07 16:06:08 +0000 |
commit | 51e8972dca292dbc6f80d13252b8555c8ce633c9 (patch) | |
tree | 9c48e9a142ba48faf17dc0fd9e8ddd8e444a0b69 | |
parent | a7ce4c0c9b3abf5bc27675a4b5edde401371a2fd (diff) | |
download | mongo-51e8972dca292dbc6f80d13252b8555c8ce633c9.tar.gz |
SERVER-66586 Add multitenancy support to UMC commands
-rw-r--r-- | jstests/auth/multitenancy_test_authzn.js | 194 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_manager.h | 12 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_manager_impl.cpp | 15 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_manager_impl.h | 5 | ||||
-rw-r--r-- | src/mongo/db/auth/authz_manager_external_state_local.cpp | 18 | ||||
-rw-r--r-- | src/mongo/db/auth/role_name_or_string.cpp | 5 | ||||
-rw-r--r-- | src/mongo/db/auth/role_name_or_string.h | 3 | ||||
-rw-r--r-- | src/mongo/db/auth/umc_info_command_arg.h | 5 | ||||
-rw-r--r-- | src/mongo/db/commands/user_management_commands.cpp | 521 | ||||
-rw-r--r-- | src/mongo/db/commands/user_management_commands_common.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/commands/user_management_commands_common.h | 3 | ||||
-rw-r--r-- | src/mongo/db/namespace_string.cpp | 22 | ||||
-rw-r--r-- | src/mongo/embedded/embedded_auth_manager.cpp | 4 |
13 files changed, 562 insertions, 247 deletions
diff --git a/jstests/auth/multitenancy_test_authzn.js b/jstests/auth/multitenancy_test_authzn.js new file mode 100644 index 00000000000..496e4af4c12 --- /dev/null +++ b/jstests/auth/multitenancy_test_authzn.js @@ -0,0 +1,194 @@ +// Test creation of local users and roles for multitenancy. + +(function() { +'use strict'; + +if (!TestData.setParameters.featureFlagMongoStore) { + assert.throws(() => MongoRunner.runMongod({ + setParameter: "multitenancySupport=true", + })); + return; +} + +function setup(conn) { + const admin = conn.getDB('admin'); + assert.commandWorked( + admin.runCommand({createUser: 'admin', pwd: 'admin', roles: ['__system']})); + assert(admin.auth('admin', 'admin')); +} + +function runTests(conn, tenant, multitenancySupport) { + const expectSuccess = (tenant === null) || (multitenancySupport && TestData.enableTestCommands); + jsTest.log("Runing test: " + tojson({tenant: tenant, multi: multitenancySupport})); + + function checkSuccess(result) { + if (expectSuccess) { + return assert.commandWorked(result); + } else { + assert.commandFailedWithCode(result, ErrorCodes.InvalidOptions); + return false; + } + } + + // TODO (SERVER-67657) Use $tenant with {find:...} operation + const tenantAdmin = conn.getDB((tenant ? tenant.str + '_' : '') + 'admin'); + const test = conn.getDB('test'); + const cmdSuffix = (tenant === null) ? {} : {"$tenant": tenant}; + function runCmd(cmd) { + const cmdToRun = Object.assign({}, cmd, cmdSuffix); + return test.runCommand(cmdToRun); + } + + function validateCounts(expectUsers, expectRoles) { + const filter = {db: 'test'}; + const admin = conn.getDB('admin'); + + if (!expectSuccess) { + expectUsers = expectRoles = 0; + } + + // usersInfo/rolesInfo commands return expected data. + const usersInfo = assert.commandWorked(runCmd({usersInfo: 1})).users; + const rolesInfo = assert.commandWorked(runCmd({rolesInfo: 1, showPrivileges: true})).roles; + assert.eq(usersInfo.length, expectUsers, tojson(usersInfo)); + assert.eq(rolesInfo.length, expectRoles, tojson(rolesInfo)); + + if (tenant) { + // Look for users/roles in tenant specific collections directly. + const tenantUsers = tenantAdmin.system.users.find(filter).toArray(); + const tenantRoles = tenantAdmin.system.roles.find(filter).toArray(); + + assert.eq(tenantUsers.length, expectUsers, tojson(tenantUsers)); + assert.eq(tenantRoles.length, expectRoles, tojson(tenantRoles)); + + // Found users/roles in tenant, don't look for them in base collections. + expectUsers = expectRoles = 0; + } + + // Check base system collections, generally should be empty, unless we're in no-tenant mode. + const systemUsers = admin.system.users.find(filter).toArray(); + const systemRoles = admin.system.roles.find(filter).toArray(); + assert.eq(systemUsers.length, expectUsers, tojson(systemUsers)); + assert.eq(systemRoles.length, expectRoles, tojson(systemRoles)); + } + + // createUser/createRole + checkSuccess(runCmd({createUser: 'user1', 'pwd': 'pwd', roles: []})); + checkSuccess(runCmd({createRole: 'role1', roles: [], privileges: []})); + checkSuccess(runCmd({createRole: 'role2', roles: ['role1'], privileges: []})); + checkSuccess( + runCmd({createRole: 'role3', roles: [{db: 'test', role: 'role1'}], privileges: []})); + checkSuccess(runCmd({createUser: 'user2', 'pwd': 'pwd', roles: ['role2', 'role3']})); + + const rwMyColl_privs = [{ + resource: {db: 'test', collection: 'myColl'}, + actions: ['find', 'insert', 'remove', 'update'] + }]; + const myCollUser_roles = [{role: 'rwMyColl', db: 'test'}]; + checkSuccess(runCmd({createRole: 'rwMyColl', roles: [], privileges: rwMyColl_privs})); + checkSuccess(runCmd({createUser: 'myCollUser', pwd: 'pwd', roles: myCollUser_roles})); + validateCounts(3, 4); + + if (tenant && expectSuccess) { + const myCollUser = tenantAdmin.system.users.find({_id: 'test.myCollUser'}).toArray()[0]; + assert.eq(tojson(myCollUser.roles), tojson(myCollUser_roles), tojson(myCollUser)); + const rwMyColl = tenantAdmin.system.roles.find({_id: 'test.rwMyColl'}).toArray()[0]; + assert.eq(tojson(rwMyColl.privileges), tojson(rwMyColl_privs), tojson(rwMyColl)); + const role2 = tenantAdmin.system.roles.find({_id: 'test.role2'}).toArray()[0]; + assert.eq(tojson(role2.roles), tojson([{role: 'role1', db: 'test'}]), tojson(role2)); + const role3 = tenantAdmin.system.roles.find({_id: 'test.role3'}).toArray()[0]; + assert.eq(tojson(role3.roles), tojson([{role: 'role1', db: 'test'}]), tojson(role3)); + } + + // grant/revoke privileges + const rwMyColl_addPrivs = + [{resource: {db: 'test', collection: 'otherColl'}, actions: ['find']}]; + checkSuccess(runCmd({grantPrivilegesToRole: 'rwMyColl', privileges: rwMyColl_addPrivs})); + checkSuccess(runCmd({ + revokePrivilegesFromRole: 'rwMyColl', + privileges: [{resource: {db: 'test', collection: 'myColl'}, actions: ['find']}] + })); + validateCounts(3, 4); + + if (tenant && expectSuccess) { + const rwMyColl_expectPrivs = [ + {resource: {db: 'test', collection: 'myColl'}, actions: ['insert', 'remove', 'update']}, + {resource: {db: 'test', collection: 'otherColl'}, actions: ['find']} + ]; + const rwMyColl = tenantAdmin.system.roles.find({_id: 'test.rwMyColl'}).toArray()[0]; + assert.eq(tojson(rwMyColl.privileges), tojson(rwMyColl_expectPrivs), tojson(rwMyColl)); + } + + // Grant/Revoke Roles to/fromfrom User/Role + checkSuccess(runCmd({grantRolesToUser: 'user1', roles: ['role1']})); + checkSuccess(runCmd({revokeRolesFromUser: 'user2', roles: ['role2']})); + checkSuccess(runCmd({grantRolesToRole: 'role1', roles: ['rwMyColl']})); + checkSuccess(runCmd({revokeRolesFromRole: 'role3', roles: ['role1']})); + validateCounts(3, 4); + + if (tenant && expectSuccess) { + const user1 = tenantAdmin.system.users.find({_id: 'test.user1'}).toArray()[0]; + assert.eq(tojson(user1.roles), tojson([{role: 'role1', db: 'test'}]), tojson(user1)); + const user2 = tenantAdmin.system.users.find({_id: 'test.user2'}).toArray()[0]; + assert.eq(tojson(user2.roles), tojson([{role: 'role3', db: 'test'}]), tojson(user2)); + + const role1 = tenantAdmin.system.roles.find({_id: 'test.role1'}).toArray()[0]; + assert.eq(tojson(role1.roles), tojson([{role: 'rwMyColl', db: 'test'}]), tojson(role1)); + const role3 = tenantAdmin.system.roles.find({_id: 'test.role3'}).toArray()[0]; + assert.eq(tojson(role3.roles), tojson([]), tojson(role3)); + } + + // updateUser/updateRole + checkSuccess(runCmd({updateUser: 'user1', roles: ['role2']})); + checkSuccess(runCmd({updateRole: 'role2', roles: ['rwMyColl']})); + validateCounts(3, 4); + + if (tenant && expectSuccess) { + const user1 = tenantAdmin.system.users.find({_id: 'test.user1'}).toArray()[0]; + assert.eq(tojson(user1.roles), tojson([{role: 'role2', db: 'test'}]), tojson(user1)); + const role2 = tenantAdmin.system.roles.find({_id: 'test.role2'}).toArray()[0]; + assert.eq(tojson(role2.roles), tojson([{role: 'rwMyColl', db: 'test'}]), tojson(role2)); + } + + // dropUser/dropRole + checkSuccess(runCmd({dropRole: 'role2'})); + checkSuccess(runCmd({dropUser: 'myCollUser'})); + validateCounts(2, 3); + + if (tenant && expectSuccess) { + // role2 should have been revoked from user1 during drop,. + const user1 = tenantAdmin.system.users.find({_id: 'test.user1'}).toArray()[0]; + assert.eq(tojson(user1.roles), tojson([]), tojson(user1)); + assert.eq(0, tenantAdmin.system.users.find({_id: 'test.myCollUser'}).toArray().length); + assert.eq(0, tenantAdmin.system.roles.find({_id: 'test.role2'}).toArray().length); + } + + // Cleanup + checkSuccess(runCmd({dropAllUsersFromDatabase: 1})); + checkSuccess(runCmd({dropAllRolesFromDatabase: 1})); + validateCounts(0, 0); +} + +// This isn't relevant to this test, but requires enableTestCommands, which we want to frob. +TestData.roleGraphInvalidationIsFatal = false; + +function spanOptions(cb) { + [true].forEach(function(enableTestCommands) { + TestData.enableTestCommands = enableTestCommands; + [true].forEach(function(multitenancySupport) { + jsTest.log( + {enableTestCommands: enableTestCommands, multitenancySupport: multitenancySupport}); + cb({multitenancySupport: multitenancySupport}); + }); + }); +} + +spanOptions(function(setParams) { + const standalone = MongoRunner.runMongod({auth: "", setParameter: setParams}); + jsTest.log('Standalone started'); + setup(standalone); + runTests(standalone, null, setParams.multitenancySupport); + runTests(standalone, ObjectId(), setParams.multitenancySupport); + MongoRunner.stopMongod(standalone); +}); +})(); diff --git a/src/mongo/db/auth/authorization_manager.h b/src/mongo/db/auth/authorization_manager.h index fca637f77d3..69077aebe6a 100644 --- a/src/mongo/db/auth/authorization_manager.h +++ b/src/mongo/db/auth/authorization_manager.h @@ -39,7 +39,7 @@ #include "mongo/db/auth/privilege_format.h" #include "mongo/db/auth/resource_pattern.h" #include "mongo/db/auth/user.h" -#include "mongo/db/jsobj.h" +#include "mongo/db/database_name.h" #include "mongo/db/namespace_string.h" #include "mongo/db/tenant_id.h" @@ -326,14 +326,16 @@ public: virtual void invalidateUserByName(OperationContext* opCtx, const UserName& user) = 0; /** - * Invalidates all users who's source is "dbname" and removes them from the user cache. + * Invalidates all users whose source is "dbname" and removes them from the user cache. */ - virtual void invalidateUsersFromDB(OperationContext* opCtx, StringData dbname) = 0; + virtual void invalidateUsersFromDB(OperationContext* opCtx, const DatabaseName& dbname) = 0; /** - * Invalidate all users associated with a given tenant. + * Invalidate all users associated with a given tenant, + * or entire cache if tenant == boost::none. */ - virtual void invalidateUsersByTenant(OperationContext* opCtx, const TenantId& tenant) = 0; + virtual void invalidateUsersByTenant(OperationContext* opCtx, + const boost::optional<TenantId>& tenant) = 0; /** * Retrieves all users whose source is "$external" and checks if the corresponding user in the diff --git a/src/mongo/db/auth/authorization_manager_impl.cpp b/src/mongo/db/auth/authorization_manager_impl.cpp index 4ecfb5af2df..2d1d6117736 100644 --- a/src/mongo/db/auth/authorization_manager_impl.cpp +++ b/src/mongo/db/auth/authorization_manager_impl.cpp @@ -684,16 +684,23 @@ void AuthorizationManagerImpl::invalidateUserByName(OperationContext* opCtx, _userCache.invalidateKey(UserRequest(userName, boost::none)); } -void AuthorizationManagerImpl::invalidateUsersFromDB(OperationContext* opCtx, StringData dbname) { +void AuthorizationManagerImpl::invalidateUsersFromDB(OperationContext* opCtx, + const DatabaseName& dbname) { LOGV2_DEBUG(20236, 2, "Invalidating all users from database", "database"_attr = dbname); _updateCacheGeneration(); _authSchemaVersionCache.invalidateAll(); - _userCache.invalidateKeyIf( - [&](const UserRequest& userRequest) { return userRequest.name.getDB() == dbname; }); + _userCache.invalidateKeyIf([&](const UserRequest& userRequest) { + return userRequest.name.getDatabaseName() == dbname; + }); } void AuthorizationManagerImpl::invalidateUsersByTenant(OperationContext* opCtx, - const TenantId& tenant) { + const boost::optional<TenantId>& tenant) { + if (!tenant) { + invalidateUserCache(opCtx); + return; + } + LOGV2_DEBUG(6323600, 2, "Invalidating tenant users", "tenant"_attr = tenant); _updateCacheGeneration(); _authSchemaVersionCache.invalidateAll(); diff --git a/src/mongo/db/auth/authorization_manager_impl.h b/src/mongo/db/auth/authorization_manager_impl.h index 81f247b2b1d..fcec7b97e32 100644 --- a/src/mongo/db/auth/authorization_manager_impl.h +++ b/src/mongo/db/auth/authorization_manager_impl.h @@ -105,9 +105,10 @@ public: */ void invalidateUserByName(OperationContext* opCtx, const UserName& user) override; - void invalidateUsersFromDB(OperationContext* opCtx, StringData dbname) override; + void invalidateUsersFromDB(OperationContext* opCtx, const DatabaseName& dbname) override; - void invalidateUsersByTenant(OperationContext* opCtx, const TenantId& tenant) override; + void invalidateUsersByTenant(OperationContext* opCtx, + const boost::optional<TenantId>& tenant) override; /** * Verify role information for users in the $external database and insert updated information diff --git a/src/mongo/db/auth/authz_manager_external_state_local.cpp b/src/mongo/db/auth/authz_manager_external_state_local.cpp index 7020dfd5ef6..717361a9b83 100644 --- a/src/mongo/db/auth/authz_manager_external_state_local.cpp +++ b/src/mongo/db/auth/authz_manager_external_state_local.cpp @@ -164,7 +164,8 @@ constexpr auto kAuthenticationRestrictionFieldName = "authenticationRestrictions std::vector<RoleName> filterAndMapRole(BSONObjBuilder* builder, BSONObj role, ResolveRoleOption option, - bool liftAuthenticationRestrictions) { + bool liftAuthenticationRestrictions, + const boost::optional<TenantId>& tenant) { std::vector<RoleName> subRoles; bool sawRestrictions = false; @@ -173,7 +174,7 @@ std::vector<RoleName> filterAndMapRole(BSONObjBuilder* builder, uassert( ErrorCodes::BadValue, "Invalid roles field, expected array", elem.type() == Array); for (const auto& roleName : elem.Obj()) { - subRoles.push_back(RoleName::parseFromBSON(roleName)); + subRoles.push_back(RoleName::parseFromBSON(roleName, tenant)); } if ((option & ResolveRoleOption::kRoles) == 0) { continue; @@ -379,7 +380,8 @@ Status AuthzManagerExternalStateLocal::getUserDescription(OperationContext* opCt return status; } - directRoles = filterAndMapRole(&resultBuilder, userDoc, ResolveRoleOption::kAll, false); + directRoles = filterAndMapRole( + &resultBuilder, userDoc, ResolveRoleOption::kAll, false, userName.getTenant()); } else { uassert(ErrorCodes::BadValue, "Illegal combination of pre-defined roles with tenant identifier", @@ -439,7 +441,6 @@ StatusWith<ResolvedRoleData> AuthzManagerExternalStateLocal::resolveRoles( const bool processRests = option & ResolveRoleOption::kRestrictions; const bool walkIndirect = (option & ResolveRoleOption::kDirectOnly) == 0; - auto optTenant = getActiveTenant(opCtx); RoleNameSet inheritedRoles; PrivilegeVector inheritedPrivileges; RestrictionDocuments::sequence_type inheritedRestrictions; @@ -478,7 +479,7 @@ StatusWith<ResolvedRoleData> AuthzManagerExternalStateLocal::resolveRoles( << "', expected an array but found " << typeName(elem.type())}; } for (const auto& subroleElem : elem.Obj()) { - auto subrole = RoleName::parseFromBSON(subroleElem, optTenant); + auto subrole = RoleName::parseFromBSON(subroleElem, role.getTenant()); if (visited.count(subrole) || nextFrontier.count(subrole)) { continue; } @@ -613,7 +614,7 @@ Status AuthzManagerExternalStateLocal::getRolesDescription( } BSONObjBuilder roleBuilder; - auto subRoles = filterAndMapRole(&roleBuilder, roleDoc, option, true); + auto subRoles = filterAndMapRole(&roleBuilder, roleDoc, option, true, role.getTenant()); auto data = uassertStatusOK(resolveRoles(opCtx, subRoles, option)); data.roles->insert(subRoles.cbegin(), subRoles.cend()); serializeResolvedRoles(&roleBuilder, data, roleDoc); @@ -681,14 +682,15 @@ Status AuthzManagerExternalStateLocal::getRoleDescriptionsForDB( } return query(opCtx, - getRolesCollection(getActiveTenant(opCtx)), + getRolesCollection(dbname.tenantId()), BSON(AuthorizationManager::ROLE_DB_FIELD_NAME << dbname.db()), BSONObj(), [&](const BSONObj& roleDoc) { try { BSONObjBuilder roleBuilder; - auto subRoles = filterAndMapRole(&roleBuilder, roleDoc, option, true); + auto subRoles = filterAndMapRole( + &roleBuilder, roleDoc, option, true, dbname.tenantId()); roleBuilder.append("isBuiltin", false); auto data = uassertStatusOK(resolveRoles(opCtx, subRoles, option)); data.roles->insert(subRoles.cbegin(), subRoles.cend()); diff --git a/src/mongo/db/auth/role_name_or_string.cpp b/src/mongo/db/auth/role_name_or_string.cpp index f9091dfbaae..72d7b4773ca 100644 --- a/src/mongo/db/auth/role_name_or_string.cpp +++ b/src/mongo/db/auth/role_name_or_string.cpp @@ -45,9 +45,10 @@ const T& variant_get(const U* variant) { return *value; } -RoleName RoleNameOrString::getRoleName(StringData dbname) const { +RoleName RoleNameOrString::getRoleName(const DatabaseName& dbname) const { if (std::holds_alternative<RoleName>(_roleName)) { - return variant_get<RoleName>(&_roleName); + auto role = variant_get<RoleName>(&_roleName); + return RoleName(role.getName(), role.getDB(), dbname.tenantId()); } else { dassert(std::holds_alternative<std::string>(_roleName)); return RoleName(variant_get<std::string>(&_roleName), dbname); diff --git a/src/mongo/db/auth/role_name_or_string.h b/src/mongo/db/auth/role_name_or_string.h index d75e1f332a9..ccc933cce0c 100644 --- a/src/mongo/db/auth/role_name_or_string.h +++ b/src/mongo/db/auth/role_name_or_string.h @@ -37,6 +37,7 @@ #include "mongo/base/string_data.h" #include "mongo/bson/bsonelement.h" #include "mongo/bson/bsonobjbuilder.h" +#include "mongo/db/database_name.h" namespace mongo { @@ -66,7 +67,7 @@ public: * Returns the fully qualified RoleName if present, * or constructs a RoleName using the parsed role and provided dbname. */ - RoleName getRoleName(StringData dbname) const; + RoleName getRoleName(const DatabaseName& dbname) const; private: std::variant<RoleName, std::string> _roleName; diff --git a/src/mongo/db/auth/umc_info_command_arg.h b/src/mongo/db/auth/umc_info_command_arg.h index dbb49fe8d0a..13a7ef586f2 100644 --- a/src/mongo/db/auth/umc_info_command_arg.h +++ b/src/mongo/db/auth/umc_info_command_arg.h @@ -36,6 +36,7 @@ #include "mongo/bson/bsonobjbuilder.h" #include "mongo/db/auth/role_name.h" #include "mongo/db/auth/user_name.h" +#include "mongo/db/database_name.h" #include "mongo/stdx/variant.h" namespace mongo { @@ -132,7 +133,7 @@ public: /** * For isExact() commands, returns a set of T with unspecified DB names resolved with $dbname. */ - std::vector<T> getElements(StringData dbname) const { + std::vector<T> getElements(const DatabaseName& dbname) const { if (!isExact()) { dassert(false); uasserted(ErrorCodes::InternalError, "Unable to get exact match for wildcard query"); @@ -190,7 +191,7 @@ private: } } - static T getElement(Single elem, StringData dbname) { + static T getElement(Single elem, const DatabaseName& dbname) { if (stdx::holds_alternative<T>(elem)) { return stdx::get<T>(elem); } else { diff --git a/src/mongo/db/commands/user_management_commands.cpp b/src/mongo/db/commands/user_management_commands.cpp index 63bfeb73a03..70b868a5cda 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/dbdirectclient.h" #include "mongo/db/jsobj.h" #include "mongo/db/multitenancy.h" +#include "mongo/db/multitenancy_gen.h" #include "mongo/db/operation_context.h" #include "mongo/db/ops/write_ops.h" #include "mongo/db/pipeline/aggregation_request_helper.h" @@ -96,9 +97,20 @@ namespace { constexpr auto kOne = "1"_sd; Status useDefaultCode(const Status& status, ErrorCodes::Error defaultCode) { - if (status.code() != ErrorCodes::UnknownError) - return status; - return Status(defaultCode, status.reason()); + if (status.code() == ErrorCodes::UnknownError) { + return Status(defaultCode, status.reason()); + } + + return status; +} + +template <typename T> +StatusWith<T> useDefaultCode(StatusWith<T>&& status, ErrorCodes::Error defaultCode) { + if (!status.isOK()) { + return useDefaultCode(status.getStatus(), defaultCode); + } + + return std::move(status); } template <typename Container> @@ -210,36 +222,37 @@ Status checkOkayToGrantPrivilegesToRole(const RoleName& role, const PrivilegeVec return Status::OK(); } -// Temporary placeholder pending availability of NamespaceWithTenant. -NamespaceString getNamespaceWithTenant(const NamespaceString& nss, - const boost::optional<TenantId>& tenant) { - if (tenant) { - return NamespaceString(str::stream() << tenant.get() << '_' << nss.db(), nss.coll()); +// TODO (SERVER-67423) Convert DBClient to accept DatabaseName type. +// Currently tenant is lost on the way from DBDirectClient to runCommand. +// For now, just mangle the NamespaceName into (`tenant_db`, `coll`) format. +NamespaceString patchTenantNSS(const NamespaceString& nss) { + if (auto tenant = nss.tenantId()) { + return NamespaceString( + boost::none, str::stream() << *tenant << '_' << nss.dbName().db(), nss.coll()); } else { return nss; } } -/** - * Finds all documents matching "query" in "collectionName". For each document returned, - * calls the function resultProcessor on it. - * Should only be called on collections with authorization documents in them - * (ie admin.system.users and admin.system.roles). - */ -Status queryAuthzDocument(OperationContext* opCtx, - const NamespaceString& collectionName, - const BSONObj& query, - const BSONObj& projection, - const std::function<void(const BSONObj&)>& resultProcessor) { - try { - DBDirectClient client(opCtx); - FindCommandRequest findRequest{collectionName}; - findRequest.setFilter(query); - findRequest.setProjection(projection); - client.find(std::move(findRequest), resultProcessor); - return Status::OK(); - } catch (const DBException& e) { - return e.toStatus(); +// TODO (SERVER-67423) Convert DBClient to accept DatabaseName type. +NamespaceString usersNSS(const boost::optional<TenantId>& tenant) { + if (tenant) { + return NamespaceString(boost::none, + str::stream() << *tenant << '_' << NamespaceString::kAdminDb, + NamespaceString::kSystemUsers); + } else { + return AuthorizationManager::usersCollectionNamespace; + } +} + +// TODO (SERVER-67423) Convert DBClient to accept DatabaseName type. +NamespaceString rolesNSS(const boost::optional<TenantId>& tenant) { + if (tenant) { + return NamespaceString(boost::none, + str::stream() << *tenant << '_' << NamespaceString::kAdminDb, + NamespaceString::kSystemRoles); + } else { + return AuthorizationManager::rolesCollectionNamespace; } } @@ -251,16 +264,14 @@ Status queryAuthzDocument(OperationContext* opCtx, * (ie admin.system.users and admin.system.roles). */ Status insertAuthzDocument(OperationContext* opCtx, - const NamespaceString& collectionName, - const BSONObj& document) { - try { - DBDirectClient client(opCtx); - write_ops::checkWriteErrors( - client.insert(write_ops::InsertCommandRequest(collectionName, {document}))); - return Status::OK(); - } catch (const DBException& e) { - return e.toStatus(); - } + const NamespaceString& nss, + const BSONObj& document) try { + DBDirectClient client(opCtx); + write_ops::checkWriteErrors( + client.insert(write_ops::InsertCommandRequest(patchTenantNSS(nss), {document}))); + return Status::OK(); +} catch (const DBException& e) { + return e.toStatus(); } /** @@ -269,34 +280,30 @@ Status insertAuthzDocument(OperationContext* opCtx, * Should only be called on collections with authorization documents in them * (ie admin.system.users and admin.system.roles). */ -Status updateAuthzDocuments(OperationContext* opCtx, - const NamespaceString& collectionName, - const BSONObj& query, - const BSONObj& updatePattern, - bool upsert, - bool multi, - std::int64_t* numMatched) { - try { - DBDirectClient client(opCtx); - auto result = client.update([&] { - write_ops::UpdateCommandRequest updateOp(collectionName); - updateOp.setUpdates({[&] { - write_ops::UpdateOpEntry entry; - entry.setQ(query); - entry.setU(write_ops::UpdateModification::parseFromClassicUpdate(updatePattern)); - entry.setMulti(multi); - entry.setUpsert(upsert); - return entry; - }()}); - return updateOp; - }()); - - *numMatched = result.getN(); - write_ops::checkWriteErrors(result); - return Status::OK(); - } catch (const DBException& e) { - return e.toStatus(); - } +StatusWith<std::int64_t> updateAuthzDocuments(OperationContext* opCtx, + const NamespaceString& nss, + const BSONObj& query, + const BSONObj& updatePattern, + bool upsert, + bool multi) try { + DBDirectClient client(opCtx); + auto result = client.update([&] { + write_ops::UpdateCommandRequest updateOp(patchTenantNSS(nss)); + updateOp.setUpdates({[&] { + write_ops::UpdateOpEntry entry; + entry.setQ(query); + entry.setU(write_ops::UpdateModification::parseFromClassicUpdate(updatePattern)); + entry.setMulti(multi); + entry.setUpsert(upsert); + return entry; + }()}); + return updateOp; + }()); + + write_ops::checkWriteErrors(result); + return result.getN(); +} catch (const DBException& e) { + return e.toStatus(); } /** @@ -316,16 +323,18 @@ Status updateOneAuthzDocument(OperationContext* opCtx, const BSONObj& query, const BSONObj& updatePattern, bool upsert) { - std::int64_t numMatched; - Status status = updateAuthzDocuments( - opCtx, collectionName, query, updatePattern, upsert, false, &numMatched); - if (!status.isOK()) { - return status; + auto swNumMatched = + updateAuthzDocuments(opCtx, collectionName, query, updatePattern, upsert, false); + if (!swNumMatched.isOK()) { + return swNumMatched.getStatus(); } + + auto numMatched = swNumMatched.getValue(); dassert(numMatched == 1 || numMatched == 0); if (numMatched == 0) { - return Status(ErrorCodes::NoMatchingDocument, "No document found"); + return {ErrorCodes::NoMatchingDocument, "No document found"}; } + return Status::OK(); } @@ -335,50 +344,46 @@ Status updateOneAuthzDocument(OperationContext* opCtx, * Should only be called on collections with authorization documents in them * (ie admin.system.users and admin.system.roles). */ -Status removeAuthzDocuments(OperationContext* opCtx, - const NamespaceString& collectionName, - const BSONObj& query, - std::int64_t* numRemoved) { - try { - DBDirectClient client(opCtx); - auto result = client.remove([&] { - write_ops::DeleteCommandRequest deleteOp(collectionName); - deleteOp.setDeletes({[&] { - write_ops::DeleteOpEntry entry; - entry.setQ(query); - entry.setMulti(true); - return entry; - }()}); - return deleteOp; - }()); - - *numRemoved = result.getN(); - write_ops::checkWriteErrors(result); - return Status::OK(); - } catch (const DBException& e) { - return e.toStatus(); - } +StatusWith<std::int64_t> removeAuthzDocuments(OperationContext* opCtx, + const NamespaceString& nss, + const BSONObj& query) try { + DBDirectClient client(opCtx); + auto result = client.remove([&] { + write_ops::DeleteCommandRequest deleteOp(patchTenantNSS(nss)); + deleteOp.setDeletes({[&] { + write_ops::DeleteOpEntry entry; + entry.setQ(query); + entry.setMulti(true); + return entry; + }()}); + return deleteOp; + }()); + + write_ops::checkWriteErrors(result); + return result.getN(); +} catch (const DBException& e) { + return e.toStatus(); } /** * Creates the given role object in the given database. */ -Status insertRoleDocument(OperationContext* opCtx, const BSONObj& roleObj) { - Status status = - insertAuthzDocument(opCtx, AuthorizationManager::rolesCollectionNamespace, roleObj); +Status insertRoleDocument(OperationContext* opCtx, + const BSONObj& roleObj, + const boost::optional<TenantId>& tenant) { + auto status = insertAuthzDocument(opCtx, rolesNSS(tenant), roleObj); if (status.isOK()) { return status; } + if (status.code() == ErrorCodes::DuplicateKey) { std::string name = roleObj[AuthorizationManager::ROLE_NAME_FIELD_NAME].String(); std::string source = roleObj[AuthorizationManager::ROLE_DB_FIELD_NAME].String(); return Status(ErrorCodes::Error(51002), str::stream() << "Role \"" << name << "@" << source << "\" already exists"); } - if (status.code() == ErrorCodes::UnknownError) { - return Status(ErrorCodes::RoleModificationFailed, status.reason()); - } - return status; + + return useDefaultCode(status, ErrorCodes::RoleModificationFailed); } /** @@ -387,7 +392,7 @@ Status insertRoleDocument(OperationContext* opCtx, const BSONObj& roleObj) { Status updateRoleDocument(OperationContext* opCtx, const RoleName& role, const BSONObj& updateObj) { Status status = updateOneAuthzDocument( opCtx, - AuthorizationManager::rolesCollectionNamespace, + rolesNSS(role.getTenant()), BSON(AuthorizationManager::ROLE_NAME_FIELD_NAME << role.getRole() << AuthorizationManager::ROLE_DB_FIELD_NAME << role.getDB()), updateObj, @@ -395,28 +400,23 @@ Status updateRoleDocument(OperationContext* opCtx, const RoleName& role, const B if (status.isOK()) { return status; } + if (status.code() == ErrorCodes::NoMatchingDocument) { return Status(ErrorCodes::RoleNotFound, str::stream() << "Role " << role << " not found"); } - if (status.code() == ErrorCodes::UnknownError) { - return Status(ErrorCodes::RoleModificationFailed, status.reason()); - } - return status; + + return useDefaultCode(status, ErrorCodes::RoleModificationFailed); } /** * Removes roles matching the given query. * Writes into *numRemoved the number of role documents that were modified. */ -Status removeRoleDocuments(OperationContext* opCtx, - const BSONObj& query, - std::int64_t* numRemoved) { - Status status = removeAuthzDocuments( - opCtx, AuthorizationManager::rolesCollectionNamespace, query, numRemoved); - if (status.code() == ErrorCodes::UnknownError) { - return Status(ErrorCodes::RoleModificationFailed, status.reason()); - } - return status; +StatusWith<std::int64_t> removeRoleDocuments(OperationContext* opCtx, + const BSONObj& query, + const boost::optional<TenantId>& tenant) { + return useDefaultCode(removeAuthzDocuments(opCtx, rolesNSS(tenant), query), + ErrorCodes::RoleModificationFailed); } /** @@ -425,7 +425,7 @@ Status removeRoleDocuments(OperationContext* opCtx, Status insertPrivilegeDocument(OperationContext* opCtx, const BSONObj& userObj, const boost::optional<TenantId>& tenant = boost::none) { - auto nss = getNamespaceWithTenant(AuthorizationManager::usersCollectionNamespace, tenant); + auto nss = usersNSS(tenant); Status status = insertAuthzDocument(opCtx, nss, userObj); if (status.isOK()) { return status; @@ -453,15 +453,14 @@ Status updatePrivilegeDocument(OperationContext* opCtx, dassert(queryObj.hasField(AuthorizationManager::USER_NAME_FIELD_NAME)); dassert(queryObj.hasField(AuthorizationManager::USER_DB_FIELD_NAME)); - const auto status = updateOneAuthzDocument( - opCtx, AuthorizationManager::usersCollectionNamespace, queryObj, updateObj, false); - if (status.code() == ErrorCodes::UnknownError) { - return {ErrorCodes::UserModificationFailed, status.reason()}; - } + const auto status = + updateOneAuthzDocument(opCtx, usersNSS(user.getTenant()), queryObj, updateObj, false); + if (status.code() == ErrorCodes::NoMatchingDocument) { return {ErrorCodes::UserNotFound, str::stream() << "User " << user << " not found"}; } - return status; + + return useDefaultCode(status, ErrorCodes::UserModificationFailed); } /** @@ -471,28 +470,23 @@ Status updatePrivilegeDocument(OperationContext* opCtx, Status updatePrivilegeDocument(OperationContext* opCtx, const UserName& user, const BSONObj& updateObj) { - const auto status = updatePrivilegeDocument( + return updatePrivilegeDocument( opCtx, user, BSON(AuthorizationManager::USER_NAME_FIELD_NAME << user.getUser() << AuthorizationManager::USER_DB_FIELD_NAME << user.getDB()), updateObj); - return status; } /** * Removes users for the given database matching the given query. * Writes into *numRemoved the number of user documents that were modified. */ -Status removePrivilegeDocuments(OperationContext* opCtx, - const BSONObj& query, - std::int64_t* numRemoved) { - Status status = removeAuthzDocuments( - opCtx, AuthorizationManager::usersCollectionNamespace, query, numRemoved); - if (status.code() == ErrorCodes::UnknownError) { - return Status(ErrorCodes::UserModificationFailed, status.reason()); - } - return status; +StatusWith<std::int64_t> removePrivilegeDocuments(OperationContext* opCtx, + const BSONObj& query, + const boost::optional<TenantId>& tenant) { + return useDefaultCode(removeAuthzDocuments(opCtx, usersNSS(tenant), query), + ErrorCodes::UserModificationFailed); } /** @@ -746,7 +740,9 @@ public: static constexpr StringData kCommitTransaction = "commitTransaction"_sd; static constexpr StringData kAbortTransaction = "abortTransaction"_sd; - UMCTransaction(OperationContext* opCtx, StringData forCommand) { + UMCTransaction(OperationContext* opCtx, + StringData forCommand, + const boost::optional<TenantId>& tenant) { // Don't transactionalize on standalone. _isReplSet = repl::ReplicationCoordinator::get(opCtx)->getReplicationMode() == repl::ReplicationCoordinator::modeReplSet; @@ -758,6 +754,12 @@ public: as->grantInternalAuthorization(_client.get()); } + if (tenant) { + _dbName = str::stream() << *tenant << '_' << kAdminDB; + } else { + _dbName = kAdminDB.toString(); + } + AlternativeClientRegion clientRegion(_client); _sessionInfo.setStartTransaction(true); _sessionInfo.setTxnNumber(0); @@ -771,14 +773,14 @@ public: } StatusWith<std::uint32_t> insert(const NamespaceString& nss, const std::vector<BSONObj>& docs) { - dassert(nss.db() == kAdminDB); + dassert(validNamespace(nss)); write_ops::InsertCommandRequest op(nss); op.setDocuments(docs); return doCrudOp(op.toBSON({})); } StatusWith<std::uint32_t> update(const NamespaceString& nss, BSONObj query, BSONObj update) { - dassert(nss.db() == kAdminDB); + dassert(validNamespace(nss)); write_ops::UpdateOpEntry entry; entry.setQ(query); entry.setU(write_ops::UpdateModification::parseFromClassicUpdate(update)); @@ -789,7 +791,7 @@ public: } StatusWith<std::uint32_t> remove(const NamespaceString& nss, BSONObj query) { - dassert(nss.db() == kAdminDB); + dassert(validNamespace(nss)); write_ops::DeleteOpEntry entry; entry.setQ(query); entry.setMulti(true); @@ -816,6 +818,23 @@ public: } private: + static bool validNamespace(const NamespaceString& nss) { + if (nss.dbName().db() == kAdminDB) { + return true; + } + if (gMultitenancySupport && !nss.tenantId()) { + // TODO (SERVER-67423) Convert DBClient to accept DatabaseName type. + try { + auto parsed = + NamespaceString::parseFromStringExpectTenantIdInMultitenancyMode(nss.ns()); + return parsed.dbName().db() == kAdminDB; + } catch (const DBException&) { + } + } + + return false; + } + StatusWith<std::uint32_t> doCrudOp(BSONObj op) try { invariant(_state != TransactionState::kDone); @@ -872,7 +891,7 @@ private: auto svcCtx = _client->getServiceContext(); auto sep = svcCtx->getServiceEntryPoint(); - auto opMsgRequest = OpMsgRequest::fromDBAndBody(kAdminDB, cmdBuilder->obj()); + auto opMsgRequest = OpMsgRequest::fromDBAndBody(_dbName, cmdBuilder->obj()); auto requestMessage = opMsgRequest.serialize(); // Switch to our local client and create a short-lived opCtx for this transaction op. @@ -891,16 +910,24 @@ private: bool _isReplSet; ServiceContext::UniqueClient _client; + std::string _dbName; OperationSessionInfoFromClient _sessionInfo; TransactionState _state = TransactionState::kInit; }; +enum class SupportTenantOption { + kNever, + kTestOnly, + kAlways, +}; + // Used by most UMC commands. struct UMCStdParams { static constexpr bool adminOnly = false; static constexpr bool supportsWriteConcern = true; static constexpr auto allowedOnSecondary = BasicCommand::AllowedOnSecondary::kNever; static constexpr bool skipApiVersionCheck = false; + static constexpr auto supportTenant = SupportTenantOption::kTestOnly; }; // Used by {usersInfo:...} and {rolesInfo:...} @@ -909,6 +936,7 @@ struct UMCInfoParams { static constexpr bool supportsWriteConcern = false; static constexpr auto allowedOnSecondary = BasicCommand::AllowedOnSecondary::kOptIn; static constexpr bool skipApiVersionCheck = false; + static constexpr auto supportTenant = SupportTenantOption::kAlways; }; // Used by {invalidateUserCache:...} @@ -917,6 +945,7 @@ struct UMCInvalidateUserCacheParams { static constexpr bool supportsWriteConcern = false; static constexpr auto allowedOnSecondary = BasicCommand::AllowedOnSecondary::kAlways; static constexpr bool skipApiVersionCheck = false; + static constexpr auto supportTenant = SupportTenantOption::kAlways; }; // Used by {_getUserCacheGeneration:...} @@ -925,6 +954,7 @@ struct UMCGetUserCacheGenParams { static constexpr bool supportsWriteConcern = false; static constexpr auto allowedOnSecondary = BasicCommand::AllowedOnSecondary::kAlways; static constexpr bool skipApiVersionCheck = true; + static constexpr auto supportTenant = SupportTenantOption::kNever; }; template <typename T> @@ -976,6 +1006,20 @@ public: typename TC::AllowedOnSecondary secondaryAllowed(ServiceContext*) const final { return Params::allowedOnSecondary; } + + bool allowedWithSecurityToken() const final { + switch (Params::supportTenant) { + case SupportTenantOption::kAlways: + return true; + case SupportTenantOption::kTestOnly: + return getTestCommandsEnabled(); + case SupportTenantOption::kNever: + return false; + } + + MONGO_UNREACHABLE; + return false; + } }; @@ -991,24 +1035,28 @@ public: template <> void CmdUMCTyped<CreateUserCommand>::Invocation::typedRun(OperationContext* opCtx) { const auto& cmd = request(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); // Validate input - uassert(ErrorCodes::BadValue, "Cannot create users in the local database", dbname != "local"); + uassert(ErrorCodes::BadValue, + "Cannot create users in the local database", + dbname.db() != NamespaceString::kLocalDb); uassert(ErrorCodes::BadValue, "Username cannot contain NULL characters", cmd.getCommandParameter().find('\0') == std::string::npos); - UserName userName(cmd.getCommandParameter(), dbname, getActiveTenant(opCtx)); + UserName userName(cmd.getCommandParameter(), dbname); + const bool isExternal = dbname.db() == NamespaceString::kExternalDb; uassert(ErrorCodes::BadValue, "Must provide a 'pwd' field for all user documents, except those" " with '$external' as the user's source db", - (cmd.getPwd() != boost::none) || (dbname == "$external")); + (cmd.getPwd() != boost::none) || isExternal); uassert(ErrorCodes::BadValue, "Cannot set the password for users defined on the '$external' database", - (cmd.getPwd() == boost::none) || (dbname != "$external")); + (cmd.getPwd() == boost::none) || !isExternal); uassert(ErrorCodes::BadValue, "mechanisms field must not be empty", @@ -1017,8 +1065,7 @@ void CmdUMCTyped<CreateUserCommand>::Invocation::typedRun(OperationContext* opCt #ifdef MONGO_CONFIG_SSL auto configuration = opCtx->getClient()->session()->getSSLConfiguration(); - if ((dbname == "$external") && configuration && - configuration->isClusterMember(userName.getUser())) { + if (isExternal && configuration && configuration->isClusterMember(userName.getUser())) { if (gEnforceUserClusterSeparation) { uasserted(ErrorCodes::BadValue, "Cannot create an x.509 user with a subjectname that would be " @@ -1099,7 +1146,8 @@ public: template <> void CmdUMCTyped<UpdateUserCommand>::Invocation::typedRun(OperationContext* opCtx) { const auto& cmd = request(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); UserName userName(cmd.getCommandParameter(), dbname); uassert(ErrorCodes::BadValue, @@ -1197,7 +1245,8 @@ CmdUMCTyped<DropUserCommand> cmdDropUser; template <> void CmdUMCTyped<DropUserCommand>::Invocation::typedRun(OperationContext* opCtx) { const auto& cmd = request(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); UserName userName(cmd.getCommandParameter(), dbname); auto* serviceContext = opCtx->getClient()->getServiceContext(); @@ -1206,16 +1255,15 @@ void CmdUMCTyped<DropUserCommand>::Invocation::typedRun(OperationContext* opCtx) audit::logDropUser(Client::getCurrent(), userName); - std::int64_t numMatched; - auto status = removePrivilegeDocuments( + auto swNumMatched = removePrivilegeDocuments( opCtx, BSON(AuthorizationManager::USER_NAME_FIELD_NAME << userName.getUser() << AuthorizationManager::USER_DB_FIELD_NAME << userName.getDB()), - &numMatched); + userName.getTenant()); // Must invalidate even on bad status - what if the write succeeded but the GLE failed? authzManager->invalidateUserByName(opCtx, userName); - uassertStatusOK(status); + auto numMatched = uassertStatusOK(swNumMatched); uassert(ErrorCodes::UserNotFound, str::stream() << "User '" << userName << "' not found", @@ -1227,25 +1275,24 @@ template <> DropAllUsersFromDatabaseReply CmdUMCTyped<DropAllUsersFromDatabaseCommand>::Invocation::typedRun( OperationContext* opCtx) { const auto& cmd = request(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); auto* client = opCtx->getClient(); auto* serviceContext = client->getServiceContext(); auto* authzManager = AuthorizationManager::get(serviceContext); auto lk = uassertStatusOK(requireWritableAuthSchema28SCRAM(opCtx, authzManager)); - audit::logDropAllUsersFromDatabase(client, dbname); + audit::logDropAllUsersFromDatabase(client, dbname.db()); - std::int64_t numRemoved; - auto status = removePrivilegeDocuments( - opCtx, BSON(AuthorizationManager::USER_DB_FIELD_NAME << dbname), &numRemoved); + auto swNumRemoved = removePrivilegeDocuments( + opCtx, BSON(AuthorizationManager::USER_DB_FIELD_NAME << dbname.db()), dbname.tenantId()); // Must invalidate even on bad status - what if the write succeeded but the GLE failed? authzManager->invalidateUsersFromDB(opCtx, dbname); - uassertStatusOK(status); DropAllUsersFromDatabaseReply reply; - reply.setCount(numRemoved); + reply.setCount(uassertStatusOK(swNumRemoved)); return reply; } @@ -1253,7 +1300,8 @@ CmdUMCTyped<GrantRolesToUserCommand> cmdGrantRolesToUser; template <> void CmdUMCTyped<GrantRolesToUserCommand>::Invocation::typedRun(OperationContext* opCtx) { const auto& cmd = request(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); UserName userName(cmd.getCommandParameter(), dbname); uassert(ErrorCodes::BadValue, @@ -1288,7 +1336,8 @@ CmdUMCTyped<RevokeRolesFromUserCommand> cmdRevokeRolesFromUser; template <> void CmdUMCTyped<RevokeRolesFromUserCommand>::Invocation::typedRun(OperationContext* opCtx) { const auto& cmd = request(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); UserName userName(cmd.getCommandParameter(), dbname); uassert(ErrorCodes::BadValue, @@ -1325,7 +1374,8 @@ UsersInfoReply CmdUMCTyped<UsersInfoCommand, UMCInfoParams>::Invocation::typedRu OperationContext* opCtx) { const auto& cmd = request(); const auto& arg = cmd.getCommandParameter(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); auto* authzManager = AuthorizationManager::get(opCtx->getServiceContext()); auto lk = uassertStatusOK(requireReadableAuthSchema26Upgrade(opCtx, authzManager)); @@ -1404,7 +1454,7 @@ UsersInfoReply CmdUMCTyped<UsersInfoCommand, UMCInfoParams>::Invocation::typedRu // Leave the pipeline unconstrained, we want to return every user. } else if (arg.isAllOnCurrentDB()) { pipeline.push_back( - BSON("$match" << BSON(AuthorizationManager::USER_DB_FIELD_NAME << dbname))); + BSON("$match" << BSON(AuthorizationManager::USER_DB_FIELD_NAME << dbname.db()))); } else { invariant(arg.isExact()); BSONArrayBuilder usersMatchArray; @@ -1448,11 +1498,10 @@ UsersInfoReply CmdUMCTyped<UsersInfoCommand, UMCInfoParams>::Invocation::typedRu DBDirectClient client(opCtx); rpc::OpMsgReplyBuilder replyBuilder; - AggregateCommandRequest aggRequest(AuthorizationManager::usersCollectionNamespace, - std::move(pipeline)); + AggregateCommandRequest aggRequest(usersNSS(dbname.tenantId()), std::move(pipeline)); // Impose no cursor privilege requirements, as cursor is drained internally uassertStatusOK(runAggregate(opCtx, - AuthorizationManager::usersCollectionNamespace, + usersNSS(dbname.tenantId()), aggRequest, aggregation_request_helper::serializeToCommandObj(aggRequest), PrivilegeVector(), @@ -1481,24 +1530,27 @@ CmdUMCTyped<CreateRoleCommand> cmdCreateRole; template <> void CmdUMCTyped<CreateRoleCommand>::Invocation::typedRun(OperationContext* opCtx) { const auto& cmd = request(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); uassert( ErrorCodes::BadValue, "Role name must be non-empty", !cmd.getCommandParameter().empty()); RoleName roleName(cmd.getCommandParameter(), dbname); - uassert(ErrorCodes::BadValue, "Cannot create roles in the local database", dbname != "local"); + uassert(ErrorCodes::BadValue, + "Cannot create roles in the local database", + dbname.db() != NamespaceString::kLocalDb); uassert(ErrorCodes::BadValue, "Cannot create roles in the $external database", - dbname != "$external"); + dbname.db() != NamespaceString::kExternalDb); uassert(ErrorCodes::BadValue, "Cannot create roles with the same name as a built-in role", !auth::isBuiltinRole(roleName)); BSONObjBuilder roleObjBuilder; - roleObjBuilder.append("_id", str::stream() << roleName.getDB() << "." << roleName.getRole()); + roleObjBuilder.append("_id", roleName.getUnambiguousName()); roleObjBuilder.append(AuthorizationManager::ROLE_NAME_FIELD_NAME, roleName.getRole()); roleObjBuilder.append(AuthorizationManager::ROLE_DB_FIELD_NAME, roleName.getDB()); @@ -1527,14 +1579,15 @@ void CmdUMCTyped<CreateRoleCommand>::Invocation::typedRun(OperationContext* opCt audit::logCreateRole( client, roleName, resolvedRoleNames, cmd.getPrivileges(), bsonAuthRestrictions); - uassertStatusOK(insertRoleDocument(opCtx, roleObjBuilder.done())); + uassertStatusOK(insertRoleDocument(opCtx, roleObjBuilder.done(), roleName.getTenant())); } CmdUMCTyped<UpdateRoleCommand> cmdUpdateRole; template <> void CmdUMCTyped<UpdateRoleCommand>::Invocation::typedRun(OperationContext* opCtx) { const auto& cmd = request(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); RoleName roleName(cmd.getCommandParameter(), dbname); const bool hasRoles = cmd.getRoles() != boost::none; @@ -1601,7 +1654,7 @@ void CmdUMCTyped<UpdateRoleCommand>::Invocation::typedRun(OperationContext* opCt auto status = updateRoleDocument(opCtx, roleName, updateDocumentBuilder.obj()); // Must invalidate even on bad status - what if the write succeeded but the GLE failed? - authzManager->invalidateUserCache(opCtx); + authzManager->invalidateUsersByTenant(opCtx, dbname.tenantId()); uassertStatusOK(status); } @@ -1609,7 +1662,8 @@ CmdUMCTyped<GrantPrivilegesToRoleCommand> cmdGrantPrivilegesToRole; template <> void CmdUMCTyped<GrantPrivilegesToRoleCommand>::Invocation::typedRun(OperationContext* opCtx) { const auto& cmd = request(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); RoleName roleName(cmd.getCommandParameter(), dbname); uassert(ErrorCodes::BadValue, @@ -1650,7 +1704,7 @@ void CmdUMCTyped<GrantPrivilegesToRoleCommand>::Invocation::typedRun(OperationCo auto status = updateRoleDocument(opCtx, roleName, updateBSONBuilder.done()); // Must invalidate even on bad status - what if the write succeeded but the GLE failed? - authzManager->invalidateUserCache(opCtx); + authzManager->invalidateUsersByTenant(opCtx, dbname.tenantId()); uassertStatusOK(status); } @@ -1658,7 +1712,8 @@ CmdUMCTyped<RevokePrivilegesFromRoleCommand> cmdRevokePrivilegesFromRole; template <> void CmdUMCTyped<RevokePrivilegesFromRoleCommand>::Invocation::typedRun(OperationContext* opCtx) { const auto& cmd = request(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); RoleName roleName(cmd.getCommandParameter(), dbname); uassert(ErrorCodes::BadValue, @@ -1704,7 +1759,7 @@ void CmdUMCTyped<RevokePrivilegesFromRoleCommand>::Invocation::typedRun(Operatio auto status = updateRoleDocument(opCtx, roleName, updateBSONBuilder.done()); // Must invalidate even on bad status - what if the write succeeded but the GLE failed? - authzManager->invalidateUserCache(opCtx); + authzManager->invalidateUsersByTenant(opCtx, dbname.tenantId()); uassertStatusOK(status); } @@ -1712,7 +1767,8 @@ CmdUMCTyped<GrantRolesToRoleCommand> cmdGrantRolesToRole; template <> void CmdUMCTyped<GrantRolesToRoleCommand>::Invocation::typedRun(OperationContext* opCtx) { const auto& cmd = request(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); RoleName roleName(cmd.getCommandParameter(), dbname); uassert(ErrorCodes::BadValue, @@ -1744,7 +1800,7 @@ void CmdUMCTyped<GrantRolesToRoleCommand>::Invocation::typedRun(OperationContext auto status = updateRoleDocument( opCtx, roleName, BSON("$set" << BSON("roles" << containerToBSONArray(directRoles)))); // Must invalidate even on bad status - what if the write succeeded but the GLE failed? - authzManager->invalidateUserCache(opCtx); + authzManager->invalidateUsersByTenant(opCtx, dbname.tenantId()); uassertStatusOK(status); } @@ -1752,7 +1808,8 @@ CmdUMCTyped<RevokeRolesFromRoleCommand> cmdRevokeRolesFromRole; template <> void CmdUMCTyped<RevokeRolesFromRoleCommand>::Invocation::typedRun(OperationContext* opCtx) { const auto& cmd = request(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); RoleName roleName(cmd.getCommandParameter(), dbname); uassert(ErrorCodes::BadValue, @@ -1783,7 +1840,7 @@ void CmdUMCTyped<RevokeRolesFromRoleCommand>::Invocation::typedRun(OperationCont auto status = updateRoleDocument( opCtx, roleName, BSON("$set" << BSON("roles" << containerToBSONArray(roles)))); // Must invalidate even on bad status - what if the write succeeded but the GLE failed? - authzManager->invalidateUserCache(opCtx); + authzManager->invalidateUsersByTenant(opCtx, dbname.tenantId()); uassertStatusOK(status); } @@ -1800,6 +1857,7 @@ bool shouldRetryTransaction(const Status& status) { } Status retryTransactionOps(OperationContext* opCtx, + const boost::optional<TenantId>& tenant, StringData forCommand, TxnOpsCallback ops, TxnAuditCallback audit) { @@ -1822,7 +1880,7 @@ Status retryTransactionOps(OperationContext* opCtx, "reason"_attr = status); } - UMCTransaction txn(opCtx, forCommand); + UMCTransaction txn(opCtx, forCommand, tenant); status = ops(txn); if (!status.isOK()) { if (!shouldRetryTransaction(status)) { @@ -1857,7 +1915,8 @@ CmdUMCTyped<DropRoleCommand> cmdDropRole; template <> void CmdUMCTyped<DropRoleCommand>::Invocation::typedRun(OperationContext* opCtx) { const auto& cmd = request(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); RoleName roleName(cmd.getCommandParameter(), dbname); uassert(ErrorCodes::BadValue, @@ -1874,7 +1933,7 @@ void CmdUMCTyped<DropRoleCommand>::Invocation::typedRun(OperationContext* opCtx) // From here on, we always want to invalidate the user cache before returning. ScopeGuard invalidateGuard([&] { try { - authzManager->invalidateUserCache(opCtx); + authzManager->invalidateUsersByTenant(opCtx, dbname.tenantId()); } catch (const AssertionException& ex) { LOGV2_WARNING(4907701, "Failed invalidating user cache", "exception"_attr = ex); } @@ -1882,7 +1941,7 @@ void CmdUMCTyped<DropRoleCommand>::Invocation::typedRun(OperationContext* opCtx) const auto dropRoleOps = [&](UMCTransaction& txn) -> Status { // Remove this role from all users - auto swCount = txn.update(AuthorizationManager::usersCollectionNamespace, + auto swCount = txn.update(usersNSS(dbname.tenantId()), BSON("roles" << BSON("$elemMatch" << roleName.toBSON())), BSON("$pull" << BSON("roles" << roleName.toBSON()))); if (!swCount.isOK()) { @@ -1892,7 +1951,7 @@ void CmdUMCTyped<DropRoleCommand>::Invocation::typedRun(OperationContext* opCtx) } // Remove this role from all other roles - swCount = txn.update(AuthorizationManager::rolesCollectionNamespace, + swCount = txn.update(rolesNSS(dbname.tenantId()), BSON("roles" << BSON("$elemMatch" << roleName.toBSON())), BSON("$pull" << BSON("roles" << roleName.toBSON()))); if (!swCount.isOK()) { @@ -1902,7 +1961,7 @@ void CmdUMCTyped<DropRoleCommand>::Invocation::typedRun(OperationContext* opCtx) } // Finally, remove the actual role document - swCount = txn.remove(AuthorizationManager::rolesCollectionNamespace, roleName.toBSON()); + swCount = txn.remove(rolesNSS(dbname.tenantId()), roleName.toBSON()); if (!swCount.isOK()) { return swCount.getStatus().withContext(str::stream() << "Failed to remove role " << roleName); @@ -1911,9 +1970,10 @@ void CmdUMCTyped<DropRoleCommand>::Invocation::typedRun(OperationContext* opCtx) return Status::OK(); }; - auto status = retryTransactionOps(opCtx, DropRoleCommand::kCommandName, dropRoleOps, [&] { - audit::logDropRole(client, roleName); - }); + auto status = retryTransactionOps( + opCtx, roleName.getTenant(), DropRoleCommand::kCommandName, dropRoleOps, [&] { + audit::logDropRole(client, roleName); + }); if (!status.isOK()) { uassertStatusOK(status.withContext("Failed applying dropRole transaction")); } @@ -1924,7 +1984,8 @@ template <> DropAllRolesFromDatabaseReply CmdUMCTyped<DropAllRolesFromDatabaseCommand>::Invocation::typedRun( OperationContext* opCtx) { const auto& cmd = request(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); auto* client = opCtx->getClient(); auto* serviceContext = client->getServiceContext(); @@ -1932,9 +1993,9 @@ DropAllRolesFromDatabaseReply CmdUMCTyped<DropAllRolesFromDatabaseCommand>::Invo auto lk = uassertStatusOK(requireWritableAuthSchema28SCRAM(opCtx, authzManager)); // From here on, we always want to invalidate the user cache before returning. - ScopeGuard invalidateGuard([opCtx, authzManager] { + ScopeGuard invalidateGuard([opCtx, authzManager, &dbname] { try { - authzManager->invalidateUserCache(opCtx); + authzManager->invalidateUsersByTenant(opCtx, dbname.tenantId()); } catch (const AssertionException& ex) { LOGV2_WARNING(4907700, "Failed invalidating user cache", "exception"_attr = ex); } @@ -1942,34 +2003,33 @@ DropAllRolesFromDatabaseReply CmdUMCTyped<DropAllRolesFromDatabaseCommand>::Invo DropAllRolesFromDatabaseReply reply; const auto dropRoleOps = [&](UMCTransaction& txn) -> Status { - auto roleMatch = BSON(AuthorizationManager::ROLE_DB_FIELD_NAME << dbname); + auto roleMatch = BSON(AuthorizationManager::ROLE_DB_FIELD_NAME << dbname.db()); auto rolesMatch = BSON("roles" << roleMatch); // Remove these roles from all users - auto swCount = txn.update(AuthorizationManager::usersCollectionNamespace, - rolesMatch, - BSON("$pull" << rolesMatch)); + auto swCount = + txn.update(usersNSS(dbname.tenantId()), rolesMatch, BSON("$pull" << rolesMatch)); if (!swCount.isOK()) { return useDefaultCode(swCount.getStatus(), ErrorCodes::UserModificationFailed) - .withContext(str::stream() << "Failed to remove roles from \"" << dbname + .withContext(str::stream() << "Failed to remove roles from \"" << dbname.db() << "\" db from all users"); } // Remove these roles from all other roles - swCount = txn.update(AuthorizationManager::rolesCollectionNamespace, - BSON("roles.db" << dbname), + swCount = txn.update(rolesNSS(dbname.tenantId()), + BSON("roles.db" << dbname.db()), BSON("$pull" << rolesMatch)); if (!swCount.isOK()) { return useDefaultCode(swCount.getStatus(), ErrorCodes::RoleModificationFailed) - .withContext(str::stream() << "Failed to remove roles from \"" << dbname + .withContext(str::stream() << "Failed to remove roles from \"" << dbname.db() << "\" db from all roles"); } // Finally, remove the actual role documents - swCount = txn.remove(AuthorizationManager::rolesCollectionNamespace, roleMatch); + swCount = txn.remove(rolesNSS(dbname.tenantId()), roleMatch); if (!swCount.isOK()) { return swCount.getStatus().withContext( - str::stream() << "Removed roles from \"" << dbname + str::stream() << "Removed roles from \"" << dbname.db() << "\" db " " from all users and roles but failed to actually delete" " those roles themselves"); @@ -1979,9 +2039,9 @@ DropAllRolesFromDatabaseReply CmdUMCTyped<DropAllRolesFromDatabaseCommand>::Invo return Status::OK(); }; - auto status = - retryTransactionOps(opCtx, DropAllRolesFromDatabaseCommand::kCommandName, dropRoleOps, [&] { - audit::logDropAllRolesFromDatabase(Client::getCurrent(), dbname); + auto status = retryTransactionOps( + opCtx, dbname.tenantId(), DropAllRolesFromDatabaseCommand::kCommandName, dropRoleOps, [&] { + audit::logDropAllRolesFromDatabase(opCtx->getClient(), dbname.db()); }); if (!status.isOK()) { uassertStatusOK( @@ -2019,7 +2079,8 @@ RolesInfoReply CmdUMCTyped<RolesInfoCommand, UMCInfoParams>::Invocation::typedRu OperationContext* opCtx) { const auto& cmd = request(); const auto& arg = cmd.getCommandParameter(); - const auto& dbname = cmd.getDbName(); + // TODO (SERVER-67516) cmd.getDatabaseName() + DatabaseName dbname(getActiveTenant(opCtx), cmd.getDbName()); auto* authzManager = AuthorizationManager::get(opCtx->getServiceContext()); auto lk = uassertStatusOK(requireReadableAuthSchema26Upgrade(opCtx, authzManager)); @@ -2069,7 +2130,8 @@ void CmdUMCTyped<InvalidateUserCacheCommand, UMCInvalidateUserCacheParams>::Invo OperationContext* opCtx) { auto* authzManager = AuthorizationManager::get(opCtx->getServiceContext()); auto lk = requireReadableAuthSchema26Upgrade(opCtx, authzManager); - authzManager->invalidateUserCache(opCtx); + // TODO (SERVER-67516) cmd.getDatabaseName().tenantId() + authzManager->invalidateUsersByTenant(opCtx, getActiveTenant(opCtx)); } CmdUMCTyped<GetUserCacheGenerationCommand, UMCGetUserCacheGenParams> cmdGetUserCacheGeneration; @@ -2117,7 +2179,7 @@ public: auth::checkAuthForTypedCommand(opCtx, request()); } - NamespaceString ns() const override { + NamespaceString ns() const final { return NamespaceString(request().getDbName(), ""); } }; @@ -2126,9 +2188,14 @@ public: return AllowedOnSecondary::kNever; } - bool adminOnly() const { + bool adminOnly() const final { return true; } + + bool allowedWithSecurityToken() const final { + // TODO (SERVER-TBD) Support mergeAuthzCollections in multitenancy + return false; + } } cmdMergeAuthzCollections; UserName _extractUserNameFromBSON(const BSONObj& userObj) { @@ -2235,6 +2302,28 @@ void _addUser(OperationContext* opCtx, /** + * Finds all documents matching "query" in "collectionName". For each document returned, + * calls the function resultProcessor on it. + * Should only be called on collections with authorization documents in them + * (ie admin.system.users and admin.system.roles). + */ +Status queryAuthzDocument(OperationContext* opCtx, + const NamespaceString& nss, + const BSONObj& query, + const BSONObj& projection, + const std::function<void(const BSONObj&)>& resultProcessor) try { + DBDirectClient client(opCtx); + FindCommandRequest findRequest{patchTenantNSS(nss)}; + findRequest.setFilter(query); + findRequest.setProjection(projection); + client.find(std::move(findRequest), resultProcessor); + return Status::OK(); +} catch (const DBException& e) { + return e.toStatus(); +} + + +/** * Moves all user objects from usersCollName into admin.system.users. If drop is true, * removes any users that were in admin.system.users but not in usersCollName. */ @@ -2282,9 +2371,9 @@ void _processUsers(OperationContext* opCtx, if (drop) { for (const auto& userName : usersToDrop) { - audit::logDropUser(Client::getCurrent(), userName); - std::int64_t numRemoved; - uassertStatusOK(removePrivilegeDocuments(opCtx, userName.toBSON(), &numRemoved)); + audit::logDropUser(opCtx->getClient(), userName); + auto numRemoved = uassertStatusOK( + removePrivilegeDocuments(opCtx, userName.toBSON(), userName.getTenant())); dassert(numRemoved == 1); } } @@ -2350,7 +2439,7 @@ void _addRole(OperationContext* opCtx, } } else { _auditCreateOrUpdateRole(roleObj, true); - Status status = insertRoleDocument(opCtx, roleObj); + Status status = insertRoleDocument(opCtx, roleObj, roleName.getTenant()); if (!status.isOK()) { // Match the behavior of mongorestore to continue on failure LOGV2_WARNING(20513, @@ -2411,8 +2500,8 @@ void _processRoles(OperationContext* opCtx, if (drop) { for (const auto& roleName : rolesToDrop) { audit::logDropRole(Client::getCurrent(), roleName); - std::int64_t numRemoved; - uassertStatusOK(removeRoleDocuments(opCtx, roleName.toBSON(), &numRemoved)); + auto numRemoved = uassertStatusOK( + removeRoleDocuments(opCtx, roleName.toBSON(), roleName.getTenant())); dassert(numRemoved == 1); } } diff --git a/src/mongo/db/commands/user_management_commands_common.cpp b/src/mongo/db/commands/user_management_commands_common.cpp index 4645c22d506..f798b291324 100644 --- a/src/mongo/db/commands/user_management_commands_common.cpp +++ b/src/mongo/db/commands/user_management_commands_common.cpp @@ -79,7 +79,7 @@ Status checkAuthorizedToGrantPrivilege(AuthorizationSession* authzSession, } // namespace std::vector<RoleName> resolveRoleNames(const std::vector<RoleNameOrString>& possibleRoles, - StringData dbname) { + const DatabaseName& dbname) { // De-duplicate as we resolve names by using a set. stdx::unordered_set<RoleName> roles; for (const auto& possibleRole : possibleRoles) { diff --git a/src/mongo/db/commands/user_management_commands_common.h b/src/mongo/db/commands/user_management_commands_common.h index 16ce67035af..0c2af216fb3 100644 --- a/src/mongo/db/commands/user_management_commands_common.h +++ b/src/mongo/db/commands/user_management_commands_common.h @@ -38,6 +38,7 @@ #include "mongo/db/auth/role_name.h" #include "mongo/db/auth/user_name.h" #include "mongo/db/commands/user_management_commands_gen.h" +#include "mongo/db/database_name.h" namespace mongo { @@ -57,7 +58,7 @@ namespace auth { * and normalizes them to a vector of RoleNames using a passed dbname fallback. */ std::vector<RoleName> resolveRoleNames(const std::vector<RoleNameOrString>& possibleRoles, - StringData dbname); + const DatabaseName& dbname); // // checkAuthorizedTo* methods diff --git a/src/mongo/db/namespace_string.cpp b/src/mongo/db/namespace_string.cpp index 399bebab90d..6ad3b027165 100644 --- a/src/mongo/db/namespace_string.cpp +++ b/src/mongo/db/namespace_string.cpp @@ -203,7 +203,23 @@ bool NamespaceString::isCollectionlessAggregateNS() const { bool NamespaceString::isLegalClientSystemNS( const ServerGlobalParams::FeatureCompatibility& currentFCV) const { - if (db() == kAdminDb) { + auto dbname = dbName().db(); + + NamespaceString parsedNSS; + if (gMultitenancySupport && !tenantId()) { + // TODO (SERVER-67423) Remove support for mangled dbname in isLegalClientSystemNS check + // Transitional support for accepting tenantId as a mangled database name. + try { + parsedNSS = parseFromStringExpectTenantIdInMultitenancyMode(ns()); + if (parsedNSS.tenantId()) { + dbname = parsedNSS.dbName().db(); + } + } catch (const DBException&) { + // Swallow exception. + } + } + + if (dbname == kAdminDb) { if (coll() == "system.roles") return true; if (coll() == kServerConfigurationNamespace.coll()) @@ -212,7 +228,7 @@ bool NamespaceString::isLegalClientSystemNS( return true; if (coll() == "system.backup_users") return true; - } else if (db() == kConfigDb) { + } else if (dbname == kConfigDb) { if (coll() == "system.sessions") return true; if (coll() == kIndexBuildEntryNamespace.coll()) @@ -223,7 +239,7 @@ bool NamespaceString::isLegalClientSystemNS( return true; if (coll() == kConfigsvrCoordinatorsNamespace.coll()) return true; - } else if (db() == kLocalDb) { + } else if (dbname == kLocalDb) { if (coll() == kSystemReplSetNamespace.coll()) return true; if (coll() == "system.healthlog") diff --git a/src/mongo/embedded/embedded_auth_manager.cpp b/src/mongo/embedded/embedded_auth_manager.cpp index 5473cbee59a..ec2c7dc9184 100644 --- a/src/mongo/embedded/embedded_auth_manager.cpp +++ b/src/mongo/embedded/embedded_auth_manager.cpp @@ -120,11 +120,11 @@ public: UASSERT_NOT_IMPLEMENTED; } - void invalidateUsersFromDB(OperationContext*, const StringData dbname) override { + void invalidateUsersFromDB(OperationContext*, const DatabaseName& dbname) override { UASSERT_NOT_IMPLEMENTED; } - void invalidateUsersByTenant(OperationContext*, const TenantId&) override { + void invalidateUsersByTenant(OperationContext*, const boost::optional<TenantId>&) override { UASSERT_NOT_IMPLEMENTED; } |