diff options
author | Varun Ravichandran <varun.ravichandran@mongodb.com> | 2023-01-17 01:37:56 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2023-01-23 15:16:44 +0000 |
commit | 23102a1982d8eaccb4bd7aa83a314699892fdf5d (patch) | |
tree | 642ae9f2aba328ed9e3006d7180c979eb970b1e7 | |
parent | 053984fd853e1900c4558006d2ed577789648142 (diff) | |
download | mongo-23102a1982d8eaccb4bd7aa83a314699892fdf5d.tar.gz |
SERVER-71195: Refresh JWKSets periodically and add key management commands
22 files changed, 219 insertions, 85 deletions
diff --git a/jstests/auth/lib/commands_lib.js b/jstests/auth/lib/commands_lib.js index 7a019f71333..3ffeac6d8ba 100644 --- a/jstests/auth/lib/commands_lib.js +++ b/jstests/auth/lib/commands_lib.js @@ -5288,6 +5288,42 @@ export const authCommandsLib = { ] }, { + testname: "oidcListKeys", + command: {oidcListKeys: 1}, + // Only enterprise knows of this command. + skipTest: + (conn) => { + return !getBuildInfo().modules.includes("enterprise") + || !TestData.setParameters.featureFlagOIDC; + }, + testcases: [ + { + runOnDb: adminDbName, + roles: roles_hostManager, + privileges: [{resource: {cluster: true}, actions: ["oidcListKeys"]}], + expectFail: true, // Server isn't configured for MONGODB-OIDC as an auth mechanism. + } + ] + }, + { + testname: "oidcRefreshKeys", + command: {oidcRefreshKeys: 1}, + // Only enterprise knows of this command. + skipTest: + (conn) => { + return !getBuildInfo().modules.includes("enterprise") + || !TestData.setParameters.featureFlagOIDC; + }, + testcases: [ + { + runOnDb: adminDbName, + roles: roles_hostManager, + privileges: [{resource: {cluster: true}, actions: ["oidcRefreshKeys"]}], + expectFail: true, // Server isn't figured for MONGODB-OIDC as an auth mechanism. + } + ] + }, + { testname: "planCacheIndexFilter", command: {planCacheClearFilters: "x"}, skipSharded: true, diff --git a/jstests/core/views/views_all_commands.js b/jstests/core/views/views_all_commands.js index 2eb0d3471ea..0d2d1e88263 100644 --- a/jstests/core/views/views_all_commands.js +++ b/jstests/core/views/views_all_commands.js @@ -529,6 +529,8 @@ let viewsCommandTests = { moveRange: {skip: isUnrelated}, multicast: {skip: isUnrelated}, netstat: {skip: isAnInternalCommand}, + oidcListKeys: {skip: isUnrelated}, + oidcRefreshKeys: {skip: isUnrelated}, pinHistoryReplicated: {skip: isAnInternalCommand}, ping: {command: {ping: 1}}, planCacheClear: {command: {planCacheClear: "view"}, expectFailure: true}, diff --git a/jstests/noPassthrough/comment_field_passthrough.js b/jstests/noPassthrough/comment_field_passthrough.js index 7af96170f23..1492a3ae702 100644 --- a/jstests/noPassthrough/comment_field_passthrough.js +++ b/jstests/noPassthrough/comment_field_passthrough.js @@ -8,15 +8,21 @@ * ] */ -import {authCommandsLib, firstDbName, secondDbName} from "jstests/auth/lib/commands_lib.js"; +import {authCommandsLib} from "jstests/auth/lib/commands_lib.js"; load("jstests/libs/fail_point_util.js"); // Helper to enable/disable failpoints easily. const tests = authCommandsLib.tests; // The following commands require additional start up configuration and hence need to be skipped. -const denylistedTests = - ["startRecordingTraffic", "stopRecordingTraffic", "addShardToZone", "removeShardFromZone"]; +const denylistedTests = [ + "startRecordingTraffic", + "stopRecordingTraffic", + "addShardToZone", + "removeShardFromZone", + "oidcListKeys", + "oidcRefreshKeys" +]; function runTests(tests, conn, impls, options) { for (const test of tests) { diff --git a/jstests/replsets/all_commands_downgrading_to_upgraded.js b/jstests/replsets/all_commands_downgrading_to_upgraded.js index 9cfc50916f9..cc70c7e10a5 100644 --- a/jstests/replsets/all_commands_downgrading_to_upgraded.js +++ b/jstests/replsets/all_commands_downgrading_to_upgraded.js @@ -899,6 +899,12 @@ const allCommands = { skip: isNotImplementedYet, isShardedOnly: true, }, + oidcListKeys: { + skip: isNotImplementedYet, + }, + oidcRefreshKeys: { + skip: isNotImplementedYet, + }, pinHistoryReplicated: { skip: isNotImplementedYet, }, diff --git a/jstests/replsets/db_reads_while_recovering_all_commands.js b/jstests/replsets/db_reads_while_recovering_all_commands.js index 8dd34026cca..cea7fd73b3c 100644 --- a/jstests/replsets/db_reads_while_recovering_all_commands.js +++ b/jstests/replsets/db_reads_while_recovering_all_commands.js @@ -325,6 +325,8 @@ const allCommands = { mergeChunks: {skip: isPrimaryOnly}, moveChunk: {skip: isPrimaryOnly}, moveRange: {skip: isPrimaryOnly}, + oidcListKeys: {skip: isNotAUserDataRead}, + oidcRefreshKeys: {skip: isNotAUserDataRead}, pinHistoryReplicated: {skip: isAnInternalCommand}, ping: {skip: isNotAUserDataRead}, planCacheClear: {skip: isNotAUserDataRead}, diff --git a/jstests/sharding/database_versioning_all_commands.js b/jstests/sharding/database_versioning_all_commands.js index 8248caf83bd..475408a4a4e 100644 --- a/jstests/sharding/database_versioning_all_commands.js +++ b/jstests/sharding/database_versioning_all_commands.js @@ -583,6 +583,8 @@ let testCases = { moveRange: {skip: "does not forward command to primary shard"}, multicast: {skip: "does not forward command to primary shard"}, netstat: {skip: "executes locally on mongos (not sent to any remote node)"}, + oidcListKeys: {skip: "executes locally on mongos (not sent to any remote node)"}, + oidcRefreshKeys: {skip: "executes locally on mongos (not sent to any remote node)"}, ping: {skip: "executes locally on mongos (not sent to any remote node)"}, planCacheClear: { run: { diff --git a/jstests/sharding/libs/last_lts_mongod_commands.js b/jstests/sharding/libs/last_lts_mongod_commands.js index 8b71181063d..134247d8e6e 100644 --- a/jstests/sharding/libs/last_lts_mongod_commands.js +++ b/jstests/sharding/libs/last_lts_mongod_commands.js @@ -31,6 +31,8 @@ const commandsAddedToMongodSinceLastLTS = [ "getChangeStreamState", "getClusterParameter", "listDatabasesForAllTenants", + "oidcListKeys", + "oidcRefreshKeys", "rotateCertificates", "setChangeStreamState", "setClusterParameter", diff --git a/jstests/sharding/libs/last_lts_mongos_commands.js b/jstests/sharding/libs/last_lts_mongos_commands.js index 97ae0116371..56e3870df6c 100644 --- a/jstests/sharding/libs/last_lts_mongos_commands.js +++ b/jstests/sharding/libs/last_lts_mongos_commands.js @@ -31,6 +31,8 @@ const commandsAddedToMongosSinceLastLTS = [ "getClusterParameter", "mergeAllChunksOnShard", "moveRange", + "oidcListKeys", + "oidcRefreshKeys", "reshardCollection", "rotateCertificates", "setAllowMigrations", diff --git a/jstests/sharding/read_write_concern_defaults_application.js b/jstests/sharding/read_write_concern_defaults_application.js index e63b757edfd..db3e3835178 100644 --- a/jstests/sharding/read_write_concern_defaults_application.js +++ b/jstests/sharding/read_write_concern_defaults_application.js @@ -584,6 +584,8 @@ let testCases = { }, multicast: {skip: "does not accept read or write concern"}, netstat: {skip: "internal command"}, + oidcListKeys: {skip: "does not accept read or write concern"}, + oidcRefreshKeys: {skip: "does not accept read or write concern"}, pinHistoryReplicated: {skip: "internal command"}, ping: {skip: "does not accept read or write concern"}, planCacheClear: {skip: "does not accept read or write concern"}, diff --git a/jstests/sharding/safe_secondary_reads_drop_recreate.js b/jstests/sharding/safe_secondary_reads_drop_recreate.js index c89198ff0b4..50cbfbf9c26 100644 --- a/jstests/sharding/safe_secondary_reads_drop_recreate.js +++ b/jstests/sharding/safe_secondary_reads_drop_recreate.js @@ -276,6 +276,8 @@ let testCases = { moveRange: {skip: "primary only"}, multicast: {skip: "does not return user data"}, netstat: {skip: "does not return user data"}, + oidcListKeys: {skip: "does not return user data"}, + oidcRefreshKeys: {skip: "does not return user data"}, ping: {skip: "does not return user data"}, planCacheClear: {skip: "does not return user data"}, planCacheClearFilters: {skip: "does not return user data"}, diff --git a/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js b/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js index c3a5f64b711..3017f7c76a3 100644 --- a/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js +++ b/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js @@ -347,6 +347,8 @@ let testCases = { moveRange: {skip: "primary only"}, multicast: {skip: "does not return user data"}, netstat: {skip: "does not return user data"}, + oidcListKeys: {skip: "does not return user data"}, + oidcRefreshKeys: {skip: "does not return user data"}, ping: {skip: "does not return user data"}, planCacheClear: {skip: "does not return user data"}, planCacheClearFilters: {skip: "does not return user data"}, diff --git a/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js b/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js index 54ca9a2d7c9..790774169cb 100644 --- a/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js +++ b/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js @@ -283,6 +283,8 @@ let testCases = { moveRange: {skip: "primary only"}, multicast: {skip: "does not return user data"}, netstat: {skip: "does not return user data"}, + oidcListKeys: {skip: "does not return user data"}, + oidcRefreshKeys: {skip: "does not return user data"}, ping: {skip: "does not return user data"}, planCacheClear: {skip: "does not return user data"}, planCacheClearFilters: {skip: "does not return user data"}, diff --git a/src/mongo/crypto/jwk_manager.cpp b/src/mongo/crypto/jwk_manager.cpp index 8291108d9f0..2465cf63f2d 100644 --- a/src/mongo/crypto/jwk_manager.cpp +++ b/src/mongo/crypto/jwk_manager.cpp @@ -43,7 +43,7 @@ namespace mongo::crypto { namespace { constexpr auto kMinKeySizeBytes = 2048 >> 3; using SharedValidator = std::shared_ptr<JWSValidator>; -using SharedMap = std::map<std::string, SharedValidator>; +using SharedValidatorMap = std::map<std::string, SharedValidator>; // Strip insignificant leading zeroes to determine the key's true size. StringData reduceInt(StringData value) { @@ -57,51 +57,36 @@ StringData reduceInt(StringData value) { } // namespace JWKManager::JWKManager(StringData source) : _keyURI(source) { - try { - auto httpClient = HttpClient::createWithoutConnectionPool(); - httpClient->setHeaders({"Accept: */*"}); - httpClient->allowInsecureHTTP(getTestCommandsEnabled()); - - auto getJWKs = httpClient->get(source); - - ConstDataRange cdr = getJWKs.getCursor(); - StringData str; - cdr.readInto<StringData>(&str); - - BSONObj data = fromjson(str); - _setAndValidateKeys(data); - } catch (const DBException& ex) { - // throws - uassertStatusOK( - ex.toStatus().withContext(str::stream() << "Failed loading keys from " << source)); - } + _loadKeysFromUri(true /* isInitialLoad */); } JWKManager::JWKManager(BSONObj keys) { - _setAndValidateKeys(keys); + _setAndValidateKeys(keys, true /* isInitialLoad */); } -StatusWith<BSONObj> JWKManager::getKey(StringData keyId) const { - auto it = _keyMaterial.find(keyId.toString()); - if (it == _keyMaterial.end()) { - return {ErrorCodes::NoSuchKey, str::stream() << "Unknown key '" << keyId << "'"}; +StatusWith<SharedValidator> JWKManager::getValidator(StringData keyId) { + auto currentValidators = _validators; + auto it = currentValidators->find(keyId.toString()); + if (it == currentValidators->end()) { + // If the JWKManager has been initialized with an URI, try refreshing. + if (_keyURI) { + _loadKeysFromUri(false /* isInitialLoad */); + currentValidators = _validators; + it = currentValidators->find(keyId.toString()); + } + + // If it still cannot be found, return an error. + if (it == currentValidators->end()) { + return {ErrorCodes::NoSuchKey, str::stream() << "Unknown key '" << keyId << "'"}; + } } return it->second; } -SharedValidator JWKManager::getValidator(StringData keyId) const { - auto it = _validators->find(keyId.toString()); +void JWKManager::_setAndValidateKeys(const BSONObj& keys, bool isInitialLoad) { + auto newValidators = std::make_shared<SharedValidatorMap>(); + auto newKeyMaterial = std::make_shared<KeyMap>(); - // TODO: SERVER-71195, refresh keys from the endpoint and try to get the validator again. - // If still no key is found throw a uassert. - uassert(ErrorCodes::NoSuchKey, - str::stream() << "Unknown key '" << keyId << "'", - it != _validators->end()); - return it->second; -} - -void JWKManager::_setAndValidateKeys(const BSONObj& keys) { - _validators = std::make_shared<SharedMap>(); auto keysParsed = JWKSet::parse(IDLParserContext("JWKSet"), keys); for (const auto& key : keysParsed.getKeys()) { @@ -130,27 +115,66 @@ void JWKManager::_setAndValidateKeys(const BSONObj& keys) { auto keyId = RSAKey.getKeyId().toString(); uassert(ErrorCodes::DuplicateKeyId, str::stream() << "Key IDs must be unique, duplicate '" << keyId << "'", - _keyMaterial.find(keyId) == _keyMaterial.end()); + newKeyMaterial->find(keyId) == newKeyMaterial->end()); - LOGV2_DEBUG(6766000, 5, "Loaded JWK Key", "kid"_attr = RSAKey.getKeyId()); - _keyMaterial.insert({keyId, key.copy()}); + // Does not need to be loaded atomically because isInitialLoad is only true when invoked + // from the constructor, so there will not be any concurrent reads. + if (isInitialLoad) { + _initialKeyMaterial.insert({keyId, key.copy()}); + } + + newKeyMaterial->insert({keyId, key.copy()}); auto swValidator = JWSValidator::create(JWK.getType(), key); uassertStatusOK(swValidator.getStatus()); SharedValidator shValidator = std::move(swValidator.getValue()); - _validators->insert({keyId, shValidator}); + newValidators->insert({keyId, shValidator}); LOGV2_DEBUG(7070202, 3, "Loaded JWK key", "kid"_attr = keyId, "typ"_attr = JWK.getType()); } + + // A mutex is not used here because no single thread consumes both _validators and _keyMaterial. + // _keyMaterial is used purely for reflection of current key state while _validators is used + // for actual signature verification with the keys. Therefore, it's safe to update each + // atomically rather than both under a mutex. + std::atomic_exchange(&_validators, std::move(newValidators)); // NOLINT + std::atomic_exchange(&_keyMaterial, std::move(newKeyMaterial)); // NOLINT } -std::vector<std::string> JWKManager::getKeyIds() const { - std::vector<std::string> ids; - std::transform(_validators->cbegin(), - _validators->cend(), - std::back_inserter(ids), - [](const auto& it) { return it.first; }); - return ids; +void JWKManager::_loadKeysFromUri(bool isInitialLoad) { + try { + auto httpClient = HttpClient::createWithoutConnectionPool(); + httpClient->setHeaders({"Accept: */*"}); + httpClient->allowInsecureHTTP(getTestCommandsEnabled()); + + invariant(_keyURI); + auto getJWKs = httpClient->get(_keyURI.value()); + + ConstDataRange cdr = getJWKs.getCursor(); + StringData str; + cdr.readInto<StringData>(&str); + + BSONObj data = fromjson(str); + _setAndValidateKeys(data, isInitialLoad); + } catch (const DBException& ex) { + // throws + uassertStatusOK(ex.toStatus().withContext(str::stream() << "Failed loading keys from " + << _keyURI.value())); + } +} + +void JWKManager::serialize(BSONObjBuilder* bob) const { + std::vector<BSONObj> keyVector; + keyVector.reserve(size()); + + auto currentKeys = _keyMaterial; + std::transform(currentKeys->begin(), + currentKeys->end(), + std::back_inserter(keyVector), + [](const auto& keyEntry) { return keyEntry.second; }); + + JWKSet jwks(keyVector); + jwks.serialize(bob); } } // namespace mongo::crypto diff --git a/src/mongo/crypto/jwk_manager.h b/src/mongo/crypto/jwk_manager.h index bba9a276388..03a05cb1ee8 100644 --- a/src/mongo/crypto/jwk_manager.h +++ b/src/mongo/crypto/jwk_manager.h @@ -42,6 +42,7 @@ namespace mongo::crypto { class JWKManager { public: using SharedValidator = std::shared_ptr<JWSValidator>; + using KeyMap = std::map<std::string, BSONObj>; /** * Fetch a JWKS file from the specified URL, parse them as keys, @@ -56,36 +57,44 @@ public: explicit JWKManager(BSONObj keys); /** - * Given a unique keyId it will return the matching JWK. - * If no key is found for the given keyId, a - */ - StatusWith<BSONObj> getKey(StringData keyId) const; - - /** * Fetch a specific JWSValidator from the JWKManager by keyId. - * If the keyId does not exist, - * a rate-limited refresh of the JWK endpoint will be - * triggered. - * If the keyId still does not exist, return ptr will be empty. + * If the keyId does not exist, it will refresh _keyMaterial and _validators and retry. + * If it still cannot be found, it will return an error. */ - SharedValidator getValidator(StringData keyId) const; + StatusWith<SharedValidator> getValidator(StringData keyId); std::size_t size() const { return _validators->size(); } /** - * Get the list of keyIds held by this key manager. + * Get a snapshot of the keys the key manager was initialized with. It is not + * modified after initialization and can be used to determine whether + * invalidation is necessary. + * TODO SERVER-73165 Generalize this to better handle invalidation. + */ + const KeyMap& getInitialKeys() const { + return _initialKeyMaterial; + } + + /** + * Serialize the JWKs stored in this key manager into the BSONObjBuilder. */ - std::vector<std::string> getKeyIds() const; + void serialize(BSONObjBuilder* bob) const; private: - void _setAndValidateKeys(const BSONObj& keys); + void _setAndValidateKeys(const BSONObj& keys, bool isInitialLoad); + + void _loadKeysFromUri(bool isInitialLoad); private: - // Map<keyId, JWKRSA> - std::map<std::string, BSONObj> _keyMaterial; // From last load - std::string _keyURI; + // Stores the key material that the manager was initialized with. + KeyMap _initialKeyMaterial; + + // Stores the current key material of the manager, which may have been refreshed. + std::shared_ptr<KeyMap> _keyMaterial; + + boost::optional<std::string> _keyURI; std::shared_ptr<std::map<std::string, SharedValidator>> _validators; }; diff --git a/src/mongo/crypto/jws_validated_token.cpp b/src/mongo/crypto/jws_validated_token.cpp index 6bd0713e0c6..e8a0738e905 100644 --- a/src/mongo/crypto/jws_validated_token.cpp +++ b/src/mongo/crypto/jws_validated_token.cpp @@ -64,7 +64,7 @@ ParsedToken parseSignedToken(StringData token) { } } // namespace -Status JWSValidatedToken::validate(const JWKManager& keyMgr) const { +Status JWSValidatedToken::validate(JWKManager* keyMgr) const { const auto now = Date_t::now(); // Clock times across the network may differ and `nbf` is likely to be @@ -86,16 +86,20 @@ Status JWSValidatedToken::validate(const JWKManager& keyMgr) const { auto signature = base64url::decode(tokenSplit.token[2]); auto payload = tokenSplit.payload; - auto sValidator = keyMgr.getValidator(_header.getKeyId()); - if (!sValidator) { - return Status{ErrorCodes::BadValue, - str::stream() << "Unknown JWT keyId << '" << _header.getKeyId() << "'"}; + auto swValidator = keyMgr->getValidator(_header.getKeyId()); + if (!swValidator.isOK()) { + auto status = swValidator.getStatus(); + if (status.code() == ErrorCodes::NoSuchKey) { + return Status{ErrorCodes::BadValue, + str::stream() << "Unknown JWT keyId << '" << _header.getKeyId() << "'"}; + } + return status; } - return sValidator.get()->validate(_header.getAlgorithm(), payload, signature); + return swValidator.getValue().get()->validate(_header.getAlgorithm(), payload, signature); } -JWSValidatedToken::JWSValidatedToken(const JWKManager& keyMgr, StringData token) +JWSValidatedToken::JWSValidatedToken(JWKManager* keyMgr, StringData token) : _originalToken(token.toString()) { auto tokenSplit = parseSignedToken(token); diff --git a/src/mongo/crypto/jws_validated_token.h b/src/mongo/crypto/jws_validated_token.h index 1f304e37072..58474ec3e17 100644 --- a/src/mongo/crypto/jws_validated_token.h +++ b/src/mongo/crypto/jws_validated_token.h @@ -46,7 +46,7 @@ public: * Upon completion, header and body payloads * and parsed structs are available. */ - JWSValidatedToken(const JWKManager& keyMgr, StringData token); + JWSValidatedToken(JWKManager* keyMgr, StringData token); /** * Extract just the Issuer name ('iss') from the token. @@ -58,7 +58,7 @@ public: * verifies it has a validator matching its keyId and finally * it calls validate from the validator, returning the status. */ - Status validate(const JWKManager& keyMgr) const; + Status validate(JWKManager* keyMgr) const; // General read-only accessors. const BSONObj& getHeaderBSON() const { diff --git a/src/mongo/crypto/jwt_test.cpp b/src/mongo/crypto/jwt_test.cpp index b43b929b08a..ae414ae17f0 100644 --- a/src/mongo/crypto/jwt_test.cpp +++ b/src/mongo/crypto/jwt_test.cpp @@ -53,13 +53,20 @@ TEST(JWKManager, parseJWKSetBasicFromSource) { BSONObj data = fromjson(str); JWKManager manager(source); + BSONObjBuilder bob; + manager.serialize(&bob); + ASSERT_BSONOBJ_EQ(bob.obj(), data); + + const auto& initialKeys = manager.getInitialKeys(); for (const auto& key : data["keys"_sd].Obj()) { - auto keyFromKid = uassertStatusOK(manager.getKey(key["kid"_sd].str())); - ASSERT_BSONOBJ_EQ(key.Obj(), keyFromKid); + auto initialKey = initialKeys.find(key["kid"_sd].str()); + ASSERT(initialKey != initialKeys.end()); + ASSERT_BSONOBJ_EQ(key.Obj(), initialKey->second); } for (const auto& key : data["keys"_sd].Obj()) { - ASSERT(manager.getValidator(key["kid"_sd].str())); + auto validator = uassertStatusOK(manager.getValidator(key["kid"_sd].str())); + ASSERT(validator); } } diff --git a/src/mongo/db/auth/action_type.idl b/src/mongo/db/auth/action_type.idl index 172c6810026..9ef3ca73a98 100644 --- a/src/mongo/db/auth/action_type.idl +++ b/src/mongo/db/auth/action_type.idl @@ -132,6 +132,8 @@ enums: logRotate : "logRotate" moveChunk : "moveChunk" netstat : "netstat" + oidcListKeys : "oidcListKeys" + oidcRefreshKeys : "oidcRefreshKeys" oidReset : "oidReset" # machine ID reset via the features command operationMetrics : "operationMetrics" planCacheIndexFilter : "planCacheIndexFilter" # view/update index filters diff --git a/src/mongo/db/auth/authorization_manager_impl.cpp b/src/mongo/db/auth/authorization_manager_impl.cpp index 0da6eeef244..c7dd4218fce 100644 --- a/src/mongo/db/auth/authorization_manager_impl.cpp +++ b/src/mongo/db/auth/authorization_manager_impl.cpp @@ -569,7 +569,13 @@ StatusWith<UserHandle> AuthorizationManagerImpl::reacquireUser(OperationContext* // Make a good faith effort to acquire an up-to-date user object, since the one // we've cached is marked "out-of-date." - auto swUserHandle = acquireUser(opCtx, UserRequest(userName, boost::none)); + // TODO SERVER-72678 avoid this edge case hack when rearchitecting user acquisition. This is + // necessary now to preserve the mechanismData from the original UserRequest while eliminating + // the roles. If the roles aren't reset to none, it will cause LDAP acquisition to be bypassed + // in favor of reusing the ones from before. + UserRequest requestWithoutRoles(user->getUserRequest()); + requestWithoutRoles.roles = boost::none; + auto swUserHandle = acquireUser(opCtx, requestWithoutRoles); if (!swUserHandle.isOK()) { return swUserHandle.getStatus(); } diff --git a/src/mongo/db/auth/authorization_manager_impl.h b/src/mongo/db/auth/authorization_manager_impl.h index db7fd95b70d..44acfa1def9 100644 --- a/src/mongo/db/auth/authorization_manager_impl.h +++ b/src/mongo/db/auth/authorization_manager_impl.h @@ -31,6 +31,7 @@ #include "mongo/db/auth/authorization_manager.h" #include "mongo/db/auth/privilege.h" +#include "mongo/db/auth/user.h" #include "mongo/platform/atomic_word.h" #include "mongo/platform/mutex.h" #include "mongo/stdx/condition_variable.h" diff --git a/src/mongo/db/auth/authorization_session_impl.cpp b/src/mongo/db/auth/authorization_session_impl.cpp index a95df5f97ea..6c4bea361a3 100644 --- a/src/mongo/db/auth/authorization_session_impl.cpp +++ b/src/mongo/db/auth/authorization_session_impl.cpp @@ -728,11 +728,11 @@ void AuthorizationSessionImpl::_refreshUserInfoAsNeeded(OperationContext* opCtx) auto swUser = getAuthorizationManager().reacquireUser(opCtx, currentUser); if (!swUser.isOK()) { auto& status = swUser.getStatus(); - // If an external user is no longer in the cache and cannot be acquired from the cache's - // backing external service, it should be cleared from _authenticatedUser. This - // guarantees that no operations can be performed until the external authorization - // provider comes back up. - if (name.getDB() == "$external"_sd) { + // If an LDAP user is no longer in the cache and cannot be acquired from the cache's + // backing LDAP host, it should be cleared from _authenticatedUser. This + // guarantees that no operations can be performed until the LDAP host comes back up. + // TODO SERVER-72678 avoid this edge case hack when rearchitecting user acquisition. + if (name.getDB() == "$external"_sd && currentUser->getUserRequest().mechanismData.empty()) { clearUser(); LOGV2(5914804, "Removed external user from session cache of user information because of " @@ -761,6 +761,19 @@ void AuthorizationSessionImpl::_refreshUserInfoAsNeeded(OperationContext* opCtx) "error"_attr = status); return; } + case ErrorCodes::ReauthenticationRequired: { + // An auth subsystem has indicated that client reauthentication is required. The + // session will exist in an expired state to signal this to drivers. + _expiredUserName = _authenticatedUser.value()->getName(); + clearUser(); + LOGV2( + 7119502, + "Removed user from session cache of user information because reauthentication " + "is required", + "user"_attr = name, + "error"_attr = status); + return; + } default: // Unrecognized error; assume that it's transient, and continue working with the // out-of-date privilege data. diff --git a/src/mongo/db/auth/builtin_roles.yml b/src/mongo/db/auth/builtin_roles.yml index dfc1b718ca3..1ddd65b6d7c 100644 --- a/src/mongo/db/auth/builtin_roles.yml +++ b/src/mongo/db/auth/builtin_roles.yml @@ -304,6 +304,8 @@ roles: - resync # clusterManager gets this also - trafficRecord - rotateCertificates + - oidcListKeys + - oidcRefreshKeys - matchType: any_normal actions: &hostManagerRoleDatabaseActions - killCursors |