diff options
author | Sara Golemon <sara.golemon@mongodb.com> | 2021-10-13 21:04:55 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-12-07 00:23:48 +0000 |
commit | d7845457fd30cd1798f70444c2a66d725ab361b4 (patch) | |
tree | 335bd948371be032d209a26c564a140b4ea215a4 | |
parent | 4a915072ab5279480305a6023db6671e3f32cfd0 (diff) | |
download | mongo-d7845457fd30cd1798f70444c2a66d725ab361b4.tar.gz |
SERVER-61615 Parse authenticated user from security token and add to authorization session
-rw-r--r-- | jstests/auth/security_token.js | 156 | ||||
-rw-r--r-- | src/mongo/db/auth/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_session.h | 11 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_session_impl.cpp | 69 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_session_impl.h | 8 | ||||
-rw-r--r-- | src/mongo/db/auth/security_token.cpp | 40 | ||||
-rw-r--r-- | src/mongo/db/auth/security_token.h | 11 | ||||
-rw-r--r-- | src/mongo/db/auth/security_token.idl | 12 | ||||
-rw-r--r-- | src/mongo/db/commands.cpp | 3 | ||||
-rw-r--r-- | src/mongo/db/commands/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/db/commands/user_management_commands.cpp | 25 | ||||
-rw-r--r-- | src/mongo/db/commands/user_management_commands.idl | 6 | ||||
-rw-r--r-- | src/mongo/db/commands/user_management_commands_common.cpp | 13 | ||||
-rw-r--r-- | src/mongo/db/multitenancy.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/service_entry_point_common.cpp | 4 | ||||
-rw-r--r-- | src/mongo/embedded/embedded_auth_session.cpp | 8 | ||||
-rw-r--r-- | src/mongo/rpc/metadata/security_token_metadata_test.cpp | 28 | ||||
-rw-r--r-- | src/mongo/shell/shell_utils.cpp | 27 |
18 files changed, 342 insertions, 83 deletions
diff --git a/jstests/auth/security_token.js b/jstests/auth/security_token.js index c6558d1d9bb..a1a0c6c2d3d 100644 --- a/jstests/auth/security_token.js +++ b/jstests/auth/security_token.js @@ -4,9 +4,12 @@ (function() { 'use strict'; -const kLogLevelForToken = 4; +const tenantID = ObjectId(); +const kLogLevelForToken = 5; const kAcceptedSecurityTokenID = 5838100; const kLogMessageID = 5060500; +const kLogoutMessageID = 6161506; +const kStaleAuthenticationMessageID = 6161507; const isMongoStoreEnabled = TestData.setParameters.featureFlagMongoStore; if (!isMongoStoreEnabled) { @@ -22,78 +25,123 @@ function assertNoTokensProcessedYet(conn) { 'Unexpected security token has been processed'); } -function runTest(conn, enabled) { +function makeTokenAndExpect(user, db) { + const authUser = {user: user, db: db, tenant: tenantID}; + + const token = _createSecurityToken(authUser); + jsTest.log('Using security token: ' + tojson(token)); + + // Clone and rewrite OID and BinData fields to be roundtrip-safe. + const expect = Object.assign({}, token); + expect.authenticatedUser = Object.assign({}, token.authenticatedUser); + expect.authenticatedUser.tenant = {'$oid': tenantID.str}; + expect.sig = {'$binary': {base64: token.sig.base64(), subType: '0'}}; + + return [token, {token: expect}]; +} + +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')); + // Create a tenant-local user. + const createUserCmd = + {createUser: 'user1', "$tenant": tenantID, pwd: 'pwd', roles: ['readWriteAnyDatabase']}; + if (enabled) { + assert.commandWorked(admin.runCommand(createUserCmd)); + + // Confirm the user exists on the tenant authz collection only, and not the global + // collection. + assert.eq(admin.system.users.count({user: 'user1'}), + 0, + 'user1 should not exist on global users collection'); + assert.eq(tenantAdmin.system.users.count({user: 'user1'}), + 1, + 'user1 should exist on tenant users collection'); + } else { + assert.commandFailed(admin.runCommand(createUserCmd)); + } + + if (rst) { + rst.awaitReplication(); + } + // Dial up the logging to watch for tenant ID being processed. const originalLogLevel = assert.commandWorked(admin.setLogLevel(kLogLevelForToken)).was.verbosity; + const tokenConn = new Mongo(conn.host); + const tokenDB = tokenConn.getDB('admin'); + // Basic OP_MSG command. - conn._setSecurityToken({}); - assert.commandWorked(admin.runCommand({ping: 1})); + tokenConn._setSecurityToken({}); + assert.commandWorked(tokenDB.runCommand({ping: 1})); assertNoTokensProcessedYet(conn); + // Test that no token equates to unauthenticated. + assert.commandFailed(tokenDB.runCommand({features: 1})); + // Passing a security token with unknown fields will always fail. - conn._setSecurityToken({invalid: 1}); - assert.commandFailed(admin.runCommand({ping: 1})); - conn._setSecurityToken({}); // clear so check-log can work + tokenConn._setSecurityToken({invalid: 1}); + assert.commandFailed(tokenDB.runCommand({ping: 1})); assertNoTokensProcessedYet(conn); - const tenantID = ObjectId(); - conn._setSecurityToken({tenant: tenantID}); + const [token, expect] = makeTokenAndExpect('user1', 'admin'); + tokenConn._setSecurityToken(token); + if (enabled) { // Basic use. - assert.commandWorked(admin.runCommand({logMessage: 'This is a test'})); + assert.commandWorked(tokenDB.runCommand({features: 1})); + + // Connection status, verify that the user/role info is returned without serializing tenant. + const authInfo = assert.commandWorked(tokenDB.runCommand({connectionStatus: 1})).authInfo; + jsTest.log(authInfo); + assert.eq(authInfo.authenticatedUsers.length, 1); + assert(0 === bsonWoCompare(authInfo.authenticatedUsers[0], {user: 'user1', db: 'admin'})); + assert.eq(authInfo.authenticatedUserRoles.length, 1); + assert(0 === + bsonWoCompare(authInfo.authenticatedUserRoles[0], + {role: 'readWriteAnyDatabase', db: 'admin'})); // Look for "Accepted Security Token" message with explicit tenant logging. - // Log line will contain {"$oid": "12345..."} rather than ObjectId. - const expect = {token: {tenant: {"$oid": tenantID.str}}}; jsTest.log('Checking for: ' + tojson(expect)); checkLog.containsJson(conn, kAcceptedSecurityTokenID, expect, 'Security Token not logged'); - // Now look for logMessage log line with implicit logging. - const logMessages = checkLog.getGlobalLog(conn) - .map((l) => JSON.parse(l)) - .filter((l) => l.id === kLogMessageID); - jsTest.log(logMessages); - assert.eq(logMessages.length, 1, 'Unexpected number of entries'); - assert.eq(logMessages[0].tenant, tenantID.str, 'Unable to find tenant ID'); + // Negative test, logMessage requires logMessage privilege on cluster (not granted) + assert.commandFailed(tokenDB.runCommand({logMessage: 'This is a test'})); + + // Positive test, writing to a new collection. + assert.writeOK(tokenConn.getDB('test').coll1.insert({x: 1})); + + const log = checkLog.getGlobalLog(conn).map((l) => JSON.parse(l)); + + // We performed 4 commands as a token auth'd user. + // We should see four post-operation logout events. + const logoutMessages = log.filter((l) => (l.id === kLogoutMessageID)); + assert.eq(logoutMessages.length, + 4, + 'Unexpected number of logout messages: ' + tojson(logoutMessages)); + + // None of those authorization sessions should remain active into their next requests. + const staleMessages = log.filter((l) => (l.id === kStaleAuthenticationMessageID)); + assert.eq( + staleMessages.length, 0, 'Unexpected stale authentications: ' + tojson(staleMessages)); } else { + assert.commandWorked( + admin.runCommand({createUser: 'user1', pwd: 'pwd', roles: ['readWriteAnyDatabase']})); // Attempting to pass a valid looking security token will fail if not enabled. - assert.commandFailed(admin.runCommand({logMessage: 'This is a test'})); + assert.commandFailed(tokenDB.runCommand({features: 1})); } // Restore logging and conn token before shutting down. - conn._setSecurityToken({}); assert.commandWorked(admin.setLogLevel(originalLogLevel)); } -function runShardTest(mongos, mongod, command) { - const db1 = mongos.getDB('db1'); - const tenantID = ObjectId(); - - const mongodOrig = - assert.commandWorked(mongod.getDB('admin').setLogLevel(kLogLevelForToken)).was.verbosity; - const mongosOrig = - assert.commandWorked(mongos.getDB('admin').setLogLevel(kLogLevelForToken)).was.verbosity; - mongos._setSecurityToken({tenant: tenantID}); - assert.commandWorked(db1.runCommand(command)); - mongos._setSecurityToken({}); - assert.commandWorked(mongos.getDB('admin').setLogLevel(mongosOrig)); - assert.commandWorked(mongod.getDB('admin').setLogLevel(mongodOrig)); - - const expect = {token: {tenant: {"$oid": tenantID.str}}}; - checkLog.containsJson( - mongos, kAcceptedSecurityTokenID, expect, 'Security Token not logged on mongos'); - checkLog.containsJson( - mongod, kAcceptedSecurityTokenID, expect, 'Security Token not logged on mongod'); -} - function runTests(enabled) { const opts = { + auth: '', setParameter: "multitenancySupport=" + (enabled ? 'true' : 'false'), }; { @@ -104,28 +152,14 @@ function runTests(enabled) { } { const rst = new ReplSetTest({nodes: 2, nodeOptions: opts}); - rst.startSet(); + rst.startSet({keyFile: 'jstests/libs/key1'}); rst.initiate(); - runTest(rst.getPrimary(), enabled); + runTest(rst.getPrimary(), enabled, rst); rst.stopSet(); } - { - const st = new ShardingTest({ - shards: 1, - mongos: 1, - config: 1, - other: {shardOptions: opts, configOptions: opts, mongosOptions: opts} - }); - runTest(st.s0, enabled); - - if (enabled) { - // Check for passthroughs to config/data shards. - runShardTest(st.s0, st.config0, {createUser: 'user1', pwd: 'user', roles: []}); - runShardTest(st.s0, st.shard0, {insert: 'coll1', documents: [{_id: ObjectId(), x: 1}]}); - } - - st.stop(); - } + // Do not test sharding since mongos must have an authenticated connection to + // all mongod nodes, and this conflicts with proxying tokens which we'll be + // performing in mongoq. } runTests(true); diff --git a/src/mongo/db/auth/SConscript b/src/mongo/db/auth/SConscript index 844b1b76db7..3ef6beaf34d 100644 --- a/src/mongo/db/auth/SConscript +++ b/src/mongo/db/auth/SConscript @@ -14,6 +14,7 @@ env.Library( '$BUILD_DIR/mongo/base', ], LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/db/auth/auth', '$BUILD_DIR/mongo/db/multitenancy_params', '$BUILD_DIR/mongo/db/server_feature_flags', '$BUILD_DIR/mongo/db/service_context', diff --git a/src/mongo/db/auth/authorization_session.h b/src/mongo/db/auth/authorization_session.h index 3631c8049b4..b23fbfcf171 100644 --- a/src/mongo/db/auth/authorization_session.h +++ b/src/mongo/db/auth/authorization_session.h @@ -169,6 +169,9 @@ public: // Gets an iterator over the roles of all authenticated users stored in this manager. virtual RoleNameIterator getAuthenticatedRoleNames() = 0; + // Removes all authenticated principals while in kSecurityToken authentication mode. + virtual void logoutSecurityTokenUser(Client* client) = 0; + // Removes any authenticated principals and revokes any privileges that were granted via those // principals. This function modifies state. Synchronizes with the Client lock. virtual void logoutAllDatabases(Client* client, StringData reason) = 0; @@ -178,6 +181,14 @@ public: // modifies state. Synchronizes with the Client lock. virtual void logoutDatabase(Client* client, StringData dbname, StringData reason) = 0; + // How the active session is authenticated. + enum class AuthenticationMode { + kNone, // Not authenticated. + kConnection, // For the duration of the connection, or until logged out. + kSecurityToken, // By operation scoped security token. + }; + virtual AuthenticationMode getAuthenticationMode() const = 0; + // Adds the internalSecurity user to the set of authenticated users. // Used to grant internal threads full access. Takes in the Client // as a parameter so it can take out a lock on the client. diff --git a/src/mongo/db/auth/authorization_session_impl.cpp b/src/mongo/db/auth/authorization_session_impl.cpp index 01ec890270d..be2ad897b27 100644 --- a/src/mongo/db/auth/authorization_session_impl.cpp +++ b/src/mongo/db/auth/authorization_session_impl.cpp @@ -44,6 +44,7 @@ #include "mongo/db/auth/action_type.h" #include "mongo/db/auth/authz_session_external_state.h" #include "mongo/db/auth/privilege.h" +#include "mongo/db/auth/security_token.h" #include "mongo/db/bson/dotted_path_support.h" #include "mongo/db/client.h" #include "mongo/db/namespace_string.h" @@ -101,6 +102,20 @@ AuthorizationManager& AuthorizationSessionImpl::getAuthorizationManager() { void AuthorizationSessionImpl::startRequest(OperationContext* opCtx) { _externalState->startRequest(opCtx); _refreshUserInfoAsNeeded(opCtx); + if (_authenticationMode == AuthenticationMode::kSecurityToken) { + // Previously authenticated using SecurityToken, + // clear that user and reset to unauthenticated state. + invariant(_authenticatedUsers.count() <= 1); + if (auto users = std::exchange(_authenticatedUsers, {}); users.count()) { + LOGV2_DEBUG(6161507, + 3, + "security token based user still authenticated at start of request, " + "clearing from authentication state", + "user"_attr = users.getNames().get().toBSON(true /* encode tenant */)); + _buildAuthenticatedRolesVector(); + } + _authenticationMode = AuthenticationMode::kNone; + } } void AuthorizationSessionImpl::startContractTracking() { @@ -177,6 +192,19 @@ Status AuthorizationSessionImpl::addAndAuthorizeUser(OperationContext* opCtx, } stdx::lock_guard<Client> lk(*opCtx->getClient()); + + if (auto token = auth::getSecurityToken(opCtx)) { + uassert( + 6161501, + "Attempt to authorize via security token on connection with established authentication", + _authenticationMode != AuthenticationMode::kConnection); + uassert(6161502, + "Attempt to authorize a user other than that present in the security token", + token->getAuthenticatedUser() == userName); + _authenticationMode = AuthenticationMode::kSecurityToken; + } else { + _authenticationMode = AuthenticationMode::kConnection; + } _authenticatedUsers.add(std::move(user)); // If there are any users and roles in the impersonation data, clear it out. @@ -214,9 +242,35 @@ User* AuthorizationSessionImpl::getSingleUser() { return lookupUser(userName); } +void AuthorizationSessionImpl::logoutSecurityTokenUser(Client* client) { + stdx::lock_guard<Client> lk(*client); + + uassert(6161503, + "Attempted to deauth a security token user while using standard login", + _authenticationMode != AuthenticationMode::kConnection); + + auto users = std::exchange(_authenticatedUsers, {}); + invariant(users.count() <= 1); + if (users.count() == 1) { + LOGV2_DEBUG(6161506, + 5, + "security token based user explicitly logged out", + "user"_attr = users.getNames().get().toBSON(true /* encode tenant */)); + } + + // Explicitly skip auditing the logout event, + // security tokens don't represent a permanent login. + clearImpersonatedUserData(); + _buildAuthenticatedRolesVector(); +} + void AuthorizationSessionImpl::logoutAllDatabases(Client* client, StringData reason) { stdx::lock_guard<Client> lk(*client); + uassert(6161504, + "May not log out while using a security token based authentication", + _authenticationMode != AuthenticationMode::kSecurityToken); + auto users = std::exchange(_authenticatedUsers, {}); if (users.count() == 0) { return; @@ -234,6 +288,10 @@ void AuthorizationSessionImpl::logoutDatabase(Client* client, StringData reason) { stdx::lock_guard<Client> lk(*client); + uassert(6161505, + "May not log out while using a security token based authentication", + _authenticationMode != AuthenticationMode::kSecurityToken); + // Emit logout audit event and then remove all users logged into dbname. UserSet updatedUsers(_authenticatedUsers); updatedUsers.removeByDBName(dbname); @@ -410,7 +468,7 @@ bool AuthorizationSessionImpl::isAuthorizedForActionsOnNamespace(const Namespace return isAuthorizedForPrivilege(Privilege(ResourcePattern::forExactNamespace(ns), actions)); } -static const int resourceSearchListCapacity = 7; +constexpr int resourceSearchListCapacity = 7; /** * Builds from "target" an exhaustive list of all ResourcePatterns that match "target". * @@ -664,14 +722,17 @@ void AuthorizationSessionImpl::_refreshUserInfoAsNeeded(OperationContext* opCtx) void AuthorizationSessionImpl::_buildAuthenticatedRolesVector() { _authenticatedRoleNames.clear(); - for (UserSet::iterator it = _authenticatedUsers.begin(); it != _authenticatedUsers.end(); - ++it) { - RoleNameIterator roles = (*it)->getIndirectRoles(); + for (const auto& userHandle : _authenticatedUsers) { + RoleNameIterator roles = userHandle->getIndirectRoles(); while (roles.more()) { RoleName roleName = roles.next(); _authenticatedRoleNames.push_back(RoleName(roleName.getRole(), roleName.getDB())); } } + + if (_authenticatedUsers.count() == 0) { + _authenticationMode = AuthenticationMode::kNone; + } } bool AuthorizationSessionImpl::isAuthorizedForAnyActionOnAnyResourceInDB(StringData db) { diff --git a/src/mongo/db/auth/authorization_session_impl.h b/src/mongo/db/auth/authorization_session_impl.h index 71c07ab949a..2ec581af5c2 100644 --- a/src/mongo/db/auth/authorization_session_impl.h +++ b/src/mongo/db/auth/authorization_session_impl.h @@ -91,9 +91,14 @@ public: RoleNameIterator getAuthenticatedRoleNames() override; + void logoutSecurityTokenUser(Client* client) override; void logoutAllDatabases(Client* client, StringData reason) override; void logoutDatabase(Client* client, StringData dbname, StringData reason) override; + AuthenticationMode getAuthenticationMode() const override { + return _authenticationMode; + } + void grantInternalAuthorization(Client* client) override; void grantInternalAuthorization(OperationContext* opCtx) override; @@ -161,6 +166,9 @@ protected: // All Users who have been authenticated on this connection. UserSet _authenticatedUsers; + // What authentication mode we're currently operating in. + AuthenticationMode _authenticationMode = AuthenticationMode::kNone; + // The roles of the authenticated users. This vector is generated when the authenticated // users set is changed. std::vector<RoleName> _authenticatedRoleNames; diff --git a/src/mongo/db/auth/security_token.cpp b/src/mongo/db/auth/security_token.cpp index 5ebdfeda60d..b38fe1e0b9b 100644 --- a/src/mongo/db/auth/security_token.cpp +++ b/src/mongo/db/auth/security_token.cpp @@ -35,6 +35,7 @@ #include "mongo/base/init.h" #include "mongo/bson/oid.h" +#include "mongo/db/auth/authorization_session.h" #include "mongo/db/multitenancy_gen.h" #include "mongo/db/server_feature_flags_gen.h" #include "mongo/logv2/log.h" @@ -54,15 +55,42 @@ MONGO_INITIALIZER(SecurityTokenOptionValidate)(InitializerContext*) { auto* opCtx = client ? client->getOperationContext() : nullptr; auto token = getSecurityToken(opCtx); if (token) { - return token->getTenant(); + return token->getAuthenticatedUser().getTenant(); } else { return boost::none; } }); } } + +// Placeholder algorithm. +void validateSecurityTokenSignature(BSONObj authUser, const SHA256Block& sig) { + auto computed = + SHA256Block::computeHash({ConstDataRange(authUser.objdata(), authUser.objsize())}); + uassert(ErrorCodes::Unauthorized, "Token signature invalid", computed == sig); +} } // namespace +SecurityTokenAuthenticationGuard::SecurityTokenAuthenticationGuard(OperationContext* opCtx) { + auto token = getSecurityToken(opCtx); + if (token == boost::none) { + _client = nullptr; + return; + } + + auto client = opCtx->getClient(); + uassertStatusOK(AuthorizationSession::get(client)->addAndAuthorizeUser( + opCtx, token->getAuthenticatedUser())); + _client = client; +} + +SecurityTokenAuthenticationGuard::~SecurityTokenAuthenticationGuard() { + if (_client) { + // SecurityToken based users are "logged out" at the end of their request. + AuthorizationSession::get(_client)->logoutSecurityTokenUser(_client); + } +} + void readSecurityTokenMetadata(OperationContext* opCtx, BSONObj securityToken) try { if (securityToken.nFields() == 0) { return; @@ -70,7 +98,15 @@ void readSecurityTokenMetadata(OperationContext* opCtx, BSONObj securityToken) t uassert(ErrorCodes::BadValue, "Multitenancy not enabled", gMultitenancySupport); - securityTokenDecoration(opCtx) = SecurityToken::parse({"Security Token"}, securityToken); + auto token = SecurityToken::parse({"Security Token"}, securityToken); + auto authenticatedUser = token.getAuthenticatedUser(); + uassert(ErrorCodes::BadValue, + "Security token authenticated user requires a valid Tenant ID", + authenticatedUser.getTenant()); + + validateSecurityTokenSignature(securityToken["authenticatedUser"].Obj(), token.getSig()); + + securityTokenDecoration(opCtx) = std::move(token); LOGV2_DEBUG(5838100, 4, "Accepted security token", "token"_attr = securityToken); } catch (const DBException& ex) { diff --git a/src/mongo/db/auth/security_token.h b/src/mongo/db/auth/security_token.h index 52e57e2625e..7a30424e78b 100644 --- a/src/mongo/db/auth/security_token.h +++ b/src/mongo/db/auth/security_token.h @@ -33,11 +33,22 @@ #include "mongo/bson/bsonobj.h" #include "mongo/db/auth/security_token_gen.h" +#include "mongo/db/client.h" #include "mongo/db/operation_context.h" namespace mongo { namespace auth { +class SecurityTokenAuthenticationGuard { +public: + SecurityTokenAuthenticationGuard() = delete; + SecurityTokenAuthenticationGuard(OperationContext* opCtx); + ~SecurityTokenAuthenticationGuard(); + +private: + Client* _client; +}; + /** * Parse any SecurityToken from the OpMsg and place it as a decoration * on OperationContext diff --git a/src/mongo/db/auth/security_token.idl b/src/mongo/db/auth/security_token.idl index 03d401ecf65..62076ac8b31 100644 --- a/src/mongo/db/auth/security_token.idl +++ b/src/mongo/db/auth/security_token.idl @@ -30,6 +30,8 @@ global: cpp_namespace: "mongo::auth" imports: + - "mongo/db/auth/auth_types.idl" + - "mongo/crypto/sha256_block.idl" - "mongo/idl/basic_types.idl" structs: @@ -37,7 +39,11 @@ structs: description: "Security Token as passed in OP_MSG" strict: true fields: - tenant: - description: Tenant identifier - type: objectid + authenticatedUser: + description: Authenticated user for which this token grants authorizations + type: UserName + sig: + # WIP This is temporarily a SHA256 hash of the authenticatedUser BSON object. + description: Validated signature on this security token + type: sha256Block diff --git a/src/mongo/db/commands.cpp b/src/mongo/db/commands.cpp index 851c0f98bef..23967c70d60 100644 --- a/src/mongo/db/commands.cpp +++ b/src/mongo/db/commands.cpp @@ -109,7 +109,8 @@ bool checkAuthorizationImplPreParse(OperationContext* opCtx, return false; // Still can't decide on auth because of the localhost bypass. uassert(ErrorCodes::Unauthorized, str::stream() << "command " << command->getName() << " requires authentication", - !command->requiresAuth() || authzSession->isAuthenticated()); + !command->requiresAuth() || authzSession->isAuthenticated() || + request.securityToken.nFields()); return false; } diff --git a/src/mongo/db/commands/SConscript b/src/mongo/db/commands/SConscript index b10e156fa71..c40622a2f36 100644 --- a/src/mongo/db/commands/SConscript +++ b/src/mongo/db/commands/SConscript @@ -161,6 +161,7 @@ env.Library( '$BUILD_DIR/mongo/db/logical_session_id', '$BUILD_DIR/mongo/db/logical_session_id_helpers', '$BUILD_DIR/mongo/db/multitenancy', + '$BUILD_DIR/mongo/db/multitenancy_params', '$BUILD_DIR/mongo/db/pipeline/change_stream_pipeline', '$BUILD_DIR/mongo/db/pipeline/pipeline', '$BUILD_DIR/mongo/db/repl/isself', diff --git a/src/mongo/db/commands/user_management_commands.cpp b/src/mongo/db/commands/user_management_commands.cpp index 199b127126c..178bd5c27f5 100644 --- a/src/mongo/db/commands/user_management_commands.cpp +++ b/src/mongo/db/commands/user_management_commands.cpp @@ -206,6 +206,16 @@ Status checkOkayToGrantPrivilegesToRole(const RoleName& role, const PrivilegeVec return Status::OK(); } +// Temporary placeholder pending availability of NamespaceWithTenant. +NamespaceString getNamespaceWithTenant(const NamespaceString& nss, + const boost::optional<OID>& tenant) { + if (tenant) { + return NamespaceString(str::stream() << tenant.get() << '_' << nss.db(), nss.coll()); + } else { + return nss; + } +} + /** * Finds all documents matching "query" in "collectionName". For each document returned, * calls the function resultProcessor on it. @@ -439,9 +449,11 @@ Status removeRoleDocuments(OperationContext* opCtx, /** * Creates the given user object in the given database. */ -Status insertPrivilegeDocument(OperationContext* opCtx, const BSONObj& userObj) { - Status status = - insertAuthzDocument(opCtx, AuthorizationManager::usersCollectionNamespace, userObj); +Status insertPrivilegeDocument(OperationContext* opCtx, + const BSONObj& userObj, + const boost::optional<OID>& tenant = boost::none) { + auto nss = getNamespaceWithTenant(AuthorizationManager::usersCollectionNamespace, tenant); + Status status = insertAuthzDocument(opCtx, nss, userObj); if (status.isOK()) { return status; } @@ -1005,7 +1017,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); + UserName userName(cmd.getCommandParameter(), dbname, cmd.getTenantOverride()); uassert(ErrorCodes::BadValue, "Must provide a 'pwd' field for all user documents, except those" @@ -1042,8 +1054,7 @@ void CmdUMCTyped<CreateUserCommand>::Invocation::typedRun(OperationContext* opCt BSONObjBuilder userObjBuilder; userObjBuilder.append("_id", userName.getUnambiguousName()); UUID::gen().appendToBuilder(&userObjBuilder, AuthorizationManager::USERID_FIELD_NAME); - userObjBuilder.append(AuthorizationManager::USER_NAME_FIELD_NAME, userName.getUser()); - userObjBuilder.append(AuthorizationManager::USER_DB_FIELD_NAME, userName.getDB()); + userName.appendToBSON(&userObjBuilder); auto* serviceContext = opCtx->getClient()->getServiceContext(); auto* authzManager = AuthorizationManager::get(serviceContext); @@ -1089,7 +1100,7 @@ void CmdUMCTyped<CreateUserCommand>::Invocation::typedRun(OperationContext* opCt authRestrictionsArray); // Must invalidate even on bad status - auto status = insertPrivilegeDocument(opCtx, userObj); + auto status = insertPrivilegeDocument(opCtx, userObj, userName.getTenant()); authzManager->invalidateUserByName(opCtx, userName); uassertStatusOK(status); } diff --git a/src/mongo/db/commands/user_management_commands.idl b/src/mongo/db/commands/user_management_commands.idl index 86911ba5528..720e8663ab5 100644 --- a/src/mongo/db/commands/user_management_commands.idl +++ b/src/mongo/db/commands/user_management_commands.idl @@ -127,6 +127,12 @@ commands: description: "List of valid authentication mechanisms for the user" type: array<string> optional: true + "$tenant": + # Only available with enableTestCommands and multitenancySupport + description: "Associate this user with a specific tenant" + type: objectid + cpp_name: tenantOverride + optional: true updateUser: description: "Modify a user" diff --git a/src/mongo/db/commands/user_management_commands_common.cpp b/src/mongo/db/commands/user_management_commands_common.cpp index bc89b9efed2..e6c4539a724 100644 --- a/src/mongo/db/commands/user_management_commands_common.cpp +++ b/src/mongo/db/commands/user_management_commands_common.cpp @@ -41,11 +41,14 @@ #include "mongo/db/auth/action_type.h" #include "mongo/db/auth/authorization_session.h" #include "mongo/db/auth/resource_pattern.h" +#include "mongo/db/auth/security_token_gen.h" #include "mongo/db/auth/user.h" #include "mongo/db/auth/user_management_commands_parser.h" +#include "mongo/db/commands/test_commands_enabled.h" #include "mongo/db/commands/user_management_commands_gen.h" #include "mongo/db/jsobj.h" #include "mongo/db/multitenancy.h" +#include "mongo/db/multitenancy_gen.h" #include "mongo/util/sequence_util.h" #include "mongo/util/str.h" @@ -188,6 +191,16 @@ 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/multitenancy.cpp b/src/mongo/db/multitenancy.cpp index 2db3089e091..fbe5d6e651a 100644 --- a/src/mongo/db/multitenancy.cpp +++ b/src/mongo/db/multitenancy.cpp @@ -44,7 +44,7 @@ boost::optional<OID> getActiveTenant(OperationContext* opCtx) { return boost::none; } - return token->getTenant(); + return token->getAuthenticatedUser().getTenant(); } } // namespace mongo diff --git a/src/mongo/db/service_entry_point_common.cpp b/src/mongo/db/service_entry_point_common.cpp index 5acaa1e2614..8fa9892aacf 100644 --- a/src/mongo/db/service_entry_point_common.cpp +++ b/src/mongo/db/service_entry_point_common.cpp @@ -44,6 +44,7 @@ #include "mongo/db/auth/authorization_session.h" #include "mongo/db/auth/impersonation_session.h" #include "mongo/db/auth/ldap_cumulative_operation_stats.h" +#include "mongo/db/auth/security_token.h" #include "mongo/db/client.h" #include "mongo/db/command_can_run_here.h" #include "mongo/db/commands.h" @@ -634,6 +635,7 @@ private: OperationSessionInfoFromClient _sessionOptions; boost::optional<ResourceConsumption::ScopedMetricsCollector> _scopedMetrics; boost::optional<ImpersonationSessionGuard> _impersonationSessionGuard; + boost::optional<auth::SecurityTokenAuthenticationGuard> _tokenAuthorizationSessionGuard; std::unique_ptr<PolymorphicScoped> _scoped; bool _refreshedDatabase = false; bool _refreshedCollection = false; @@ -1288,6 +1290,8 @@ void ExecCommandDatabase::_initiateCommand() { }); rpc::readRequestMetadata(opCtx, request, command->requiresAuth()); + _tokenAuthorizationSessionGuard.emplace(opCtx); + rpc::TrackingMetadata::get(opCtx).initWithOperName(command->getName()); auto const replCoord = repl::ReplicationCoordinator::get(opCtx); diff --git a/src/mongo/embedded/embedded_auth_session.cpp b/src/mongo/embedded/embedded_auth_session.cpp index c26f16c3781..9b7f07c1658 100644 --- a/src/mongo/embedded/embedded_auth_session.cpp +++ b/src/mongo/embedded/embedded_auth_session.cpp @@ -215,6 +215,14 @@ public: // Do nothing } + AuthenticationMode getAuthenticationMode() const override { + return AuthenticationMode::kNone; + } + + void logoutSecurityTokenUser(Client* client) override { + UASSERT_NOT_IMPLEMENTED; + } + protected: std::tuple<std::vector<UserName>*, std::vector<RoleName>*> _getImpersonations() override { UASSERT_NOT_IMPLEMENTED; diff --git a/src/mongo/rpc/metadata/security_token_metadata_test.cpp b/src/mongo/rpc/metadata/security_token_metadata_test.cpp index 00c664c4fc7..1319dae55c7 100644 --- a/src/mongo/rpc/metadata/security_token_metadata_test.cpp +++ b/src/mongo/rpc/metadata/security_token_metadata_test.cpp @@ -30,6 +30,7 @@ #include "mongo/platform/basic.h" #include "mongo/bson/oid.h" +#include "mongo/crypto/sha256_block.h" #include "mongo/db/auth/security_token.h" #include "mongo/db/auth/security_token_gen.h" #include "mongo/db/client.h" @@ -43,14 +44,28 @@ namespace rpc { namespace test { namespace { +constexpr auto kAuthenticatedUserFieldName = "authenticatedUser"_sd; constexpr auto kPingFieldName = "ping"_sd; -constexpr auto kTenantFieldName = "tenant"_sd; +constexpr auto kSigFieldName = "sig"_sd; + +BSONObj makeSecurityToken(const UserName& userName) { + auto authUser = userName.toBSON(true /* serialize token */); + ASSERT_EQ(authUser["tenant"_sd].type(), jstOID); + + BSONObjBuilder token; + token.append(kAuthenticatedUserFieldName, authUser); + + auto block = SHA256Block::computeHash({ConstDataRange(authUser.objdata(), authUser.objsize())}); + token.appendBinData(kSigFieldName, block.size(), BinDataGeneral, block.data()); + + return token.obj(); +} class SecurityTokenMetadataTest : public LockerNoopServiceContextTest {}; TEST_F(SecurityTokenMetadataTest, SecurityTokenNotAccepted) { const auto kPingBody = BSON(kPingFieldName << 1); - const auto kTokenBody = BSON(kTenantFieldName << OID::gen()); + const auto kTokenBody = makeSecurityToken(UserName("user", "admin", OID::gen())); gMultitenancySupport = false; auto msgBytes = OpMsgBytes{0, kBodySection, kPingBody, kSecurityTokenSection, kTokenBody}; @@ -63,7 +78,7 @@ TEST_F(SecurityTokenMetadataTest, SecurityTokenNotAccepted) { TEST_F(SecurityTokenMetadataTest, BasicSuccess) { const auto kOid = OID::gen(); const auto kPingBody = BSON(kPingFieldName << 1); - const auto kTokenBody = BSON(kTenantFieldName << kOid); + const auto kTokenBody = makeSecurityToken(UserName("user", "admin", kOid)); gMultitenancySupport = true; auto msg = OpMsgBytes{0, kBodySection, kPingBody, kSecurityTokenSection, kTokenBody}.parse(); @@ -77,7 +92,12 @@ TEST_F(SecurityTokenMetadataTest, BasicSuccess) { auth::readSecurityTokenMetadata(opCtx.get(), msg.securityToken); auto token = auth::getSecurityToken(opCtx.get()); ASSERT(token != boost::none); - ASSERT_EQ(token->getTenant(), kOid); + + auto authedUser = token->getAuthenticatedUser(); + ASSERT_EQ(authedUser.getUser(), "user"); + ASSERT_EQ(authedUser.getDB(), "admin"); + ASSERT_TRUE(authedUser.getTenant() != boost::none); + ASSERT_EQ(authedUser.getTenant().get(), kOid); } } // namespace diff --git a/src/mongo/shell/shell_utils.cpp b/src/mongo/shell/shell_utils.cpp index aec44e0c723..79f89b28c57 100644 --- a/src/mongo/shell/shell_utils.cpp +++ b/src/mongo/shell/shell_utils.cpp @@ -415,6 +415,32 @@ BSONObj convertShardKeyToHashed(const BSONObj& a, void* data) { return BSON("" << key); } +/** + * Generate a security token suitable for passing in an OpMsg payload token field. + * + * @param user object - { user: 'name', db: 'dbname', tenant: OID } + * @return object - { authenticatedUser: {...user object...}, sig: BinDataGeneral(Signature) } + */ +BSONObj _createSecurityToken(const BSONObj& args, void* data) { + uassert(6161500, + "_createSecurityToken requires a single object argument", + (args.nFields() == 1) && (args.firstElement().type() == Object)); + auto authUser = args.firstElement().Obj(); + + // Temporary algorithm. + auto digest = + SHA256Block::computeHash({ConstDataRange(authUser.objdata(), authUser.objsize())}); + + BSONObjBuilder ret; + { + BSONObjBuilder token(ret.subobjStart(""_sd)); + token.append("authenticatedUser"_sd, authUser); + token.appendBinData("sig"_sd, digest.size(), BinDataGeneral, digest.data()); + token.doneFast(); + } + return ret.obj(); +} + BSONObj replMonitorStats(const BSONObj& a, void* data) { uassert(17134, "replMonitorStats requires a single string argument (the ReplSet name)", @@ -478,6 +504,7 @@ BSONObj numberDecimalsEqual(const BSONObj& input, void*) { void installShellUtils(Scope& scope) { scope.injectNative("getMemInfo", JSGetMemInfo); + scope.injectNative("_createSecurityToken", _createSecurityToken); scope.injectNative("_replMonitorStats", replMonitorStats); scope.injectNative("_srand", JSSrand); scope.injectNative("_rand", JSRand); |