diff options
author | Sara Golemon <sara.golemon@mongodb.com> | 2019-12-04 17:12:10 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2020-02-12 19:16:45 +0000 |
commit | 812c8338f496da3f43174330e37f07f0aad442d3 (patch) | |
tree | 80baa88c0eb7aec60fe1d199b27308deae87d49c /src/mongo | |
parent | 37d1ef0d02582ac95a2adf835a341e0ead12abb3 (diff) | |
download | mongo-812c8338f496da3f43174330e37f07f0aad442d3.tar.gz |
SERVER-44858 Implement speculative sasl auth
create mode 100644 jstests/auth/speculative-auth-replset.js
create mode 100644 jstests/auth/speculative-sasl-start.js
create mode 100644 jstests/ssl/speculative-auth-replset.js
create mode 100644 jstests/ssl/speculative-authenticate.js
create mode 100644 src/mongo/db/auth/sasl_commands.h
create mode 100644 src/mongo/db/s/balancer/core_options_stub.cpp
Diffstat (limited to 'src/mongo')
33 files changed, 902 insertions, 252 deletions
diff --git a/src/mongo/client/SConscript b/src/mongo/client/SConscript index 4d767b99135..6707bfa3494 100644 --- a/src/mongo/client/SConscript +++ b/src/mongo/client/SConscript @@ -139,6 +139,9 @@ env.Library( '$BUILD_DIR/mongo/executor/remote_command', 'sasl_client' ], + LIBDEPS_PRIVATE=[ + 'connection_string', + ], ) env.Library( diff --git a/src/mongo/client/async_client.cpp b/src/mongo/client/async_client.cpp index 10155cf5657..cd7a3f78bd9 100644 --- a/src/mongo/client/async_client.cpp +++ b/src/mongo/client/async_client.cpp @@ -37,7 +37,9 @@ #include "mongo/bson/bsonobjbuilder.h" #include "mongo/client/authenticate.h" +#include "mongo/client/sasl_client_authenticate.h" #include "mongo/config.h" +#include "mongo/db/auth/sasl_command_constants.h" #include "mongo/db/commands/test_commands_enabled.h" #include "mongo/db/server_options.h" #include "mongo/db/wire_version.h" @@ -172,6 +174,42 @@ Future<void> AsyncDBClient::authenticateInternal(boost::optional<std::string> me return auth::authenticateInternalClient(clientName, mechanismHint, _makeAuthRunCommandHook()); } +Future<bool> AsyncDBClient::completeSpeculativeAuth(std::shared_ptr<SaslClientSession> session, + std::string authDB, + BSONObj specAuth, + auth::SpeculativeAuthType speculativeAuthType) { + if (specAuth.isEmpty()) { + // No reply could mean failed auth, or old server. + // A false reply will result in an explicit auth later. + return false; + } + + if (speculativeAuthType == auth::SpeculativeAuthType::kNone) { + return Status(ErrorCodes::BadValue, + str::stream() << "Received unexpected isMaster." + << auth::kSpeculativeAuthenticate << " reply"); + } + + if (speculativeAuthType == auth::SpeculativeAuthType::kAuthenticate) { + return specAuth.hasField(saslCommandUserFieldName); + } + + invariant(speculativeAuthType == auth::SpeculativeAuthType::kSaslStart); + invariant(session); + + return asyncSaslConversation(_makeAuthRunCommandHook(), + session, + BSON(saslContinueCommandName << 1), + specAuth, + std::move(authDB), + kSaslClientLogLevelDefault) + // Swallow failure even if the initial saslStart was okay. + // It's possible for our speculative authentication to fail + // while explicit auth succeeds if we're in a keyfile rollover state. + // The first passphrase can fail, but later ones may be okay. + .onCompletion([](Status status) { return status.isOK(); }); +} + Future<void> AsyncDBClient::initWireVersion(const std::string& appName, executor::NetworkConnectionHook* const hook) { auto requestObj = _buildIsMasterRequest(appName, hook); diff --git a/src/mongo/client/async_client.h b/src/mongo/client/async_client.h index 9a66b49eb54..28e0bc2e9da 100644 --- a/src/mongo/client/async_client.h +++ b/src/mongo/client/async_client.h @@ -87,6 +87,11 @@ public: Future<void> authenticateInternal(boost::optional<std::string> mechanismHint); + Future<bool> completeSpeculativeAuth(std::shared_ptr<SaslClientSession> session, + std::string authDB, + BSONObj specAuth, + auth::SpeculativeAuthType speculativeAuthtype); + Future<void> initWireVersion(const std::string& appName, executor::NetworkConnectionHook* const hook); diff --git a/src/mongo/client/authenticate.cpp b/src/mongo/client/authenticate.cpp index e76f72035a3..71c9aa3cbc6 100644 --- a/src/mongo/client/authenticate.cpp +++ b/src/mongo/client/authenticate.cpp @@ -323,5 +323,135 @@ StringData getSaslCommandUserFieldName() { return saslCommandUserFieldName; } +namespace { + +StatusWith<std::shared_ptr<SaslClientSession>> _speculateSaslStart(BSONObjBuilder* isMaster, + const std::string& mechanism, + const HostAndPort& host, + StringData authDB, + BSONObj params) { + if (mechanism == kMechanismSaslPlain) { + return {ErrorCodes::BadValue, "PLAIN mechanism not supported with speculativeSaslStart"}; + } + + std::shared_ptr<SaslClientSession> session(SaslClientSession::create(mechanism)); + auto status = saslConfigureSession(session.get(), host, authDB, params); + if (!status.isOK()) { + return status; + } + + std::string payload; + status = session->step("", &payload); + if (!status.isOK()) { + return status; + } + + BSONObjBuilder saslStart; + saslStart.append("saslStart", 1); + saslStart.append("mechanism", mechanism); + saslStart.appendBinData("payload", int(payload.size()), BinDataGeneral, payload.c_str()); + saslStart.append("db", authDB); + isMaster->append(kSpeculativeAuthenticate, saslStart.obj()); + + return session; +} + +StatusWith<SpeculativeAuthType> _speculateAuth( + BSONObjBuilder* isMaster, + const std::string& mechanism, + const HostAndPort& host, + StringData authDB, + BSONObj params, + std::shared_ptr<SaslClientSession>* saslClientSession) { + if (mechanism == kMechanismMongoX509) { + // MONGODB-X509 + isMaster->append(kSpeculativeAuthenticate, + BSON(kAuthenticateCommand << "1" << saslCommandMechanismFieldName + << mechanism << saslCommandUserDBFieldName + << "$external")); + return SpeculativeAuthType::kAuthenticate; + } + + // Proceed as if this is a SASL mech and we either have a password, + // or we don't need one (e.g. MONGODB-AWS). + // Failure is absolutely an option. + auto swSaslClientSession = _speculateSaslStart(isMaster, mechanism, host, authDB, params); + if (!swSaslClientSession.isOK()) { + return swSaslClientSession.getStatus(); + } + + // It's okay to fail, the non-speculative auth flow will try again. + *saslClientSession = std::move(swSaslClientSession.getValue()); + return SpeculativeAuthType::kSaslStart; +} + +std::string getBSONString(BSONObj container, StringData field) { + auto elem = container[field]; + uassert(ErrorCodes::BadValue, + str::stream() << "Field '" << field << "' must be of type string", + elem.type() == String); + return elem.String(); +} +} // namespace + +SpeculativeAuthType speculateAuth(BSONObjBuilder* isMasterRequest, + const MongoURI& uri, + std::shared_ptr<SaslClientSession>* saslClientSession) { + auto mechanism = uri.getOption("authMechanism").get_value_or(kMechanismScramSha256.toString()); + + auto optParams = uri.makeAuthObjFromOptions(LATEST_WIRE_VERSION, {mechanism}); + if (!optParams) { + return SpeculativeAuthType::kNone; + } + + auto params = std::move(optParams.get()); + + auto ret = _speculateAuth(isMasterRequest, + mechanism, + uri.getServers().front(), + uri.getAuthenticationDatabase(), + params, + saslClientSession); + if (!ret.isOK()) { + // Ignore error, fallback on explicit auth. + return SpeculativeAuthType::kNone; + } + + return ret.getValue(); +} + +SpeculativeAuthType speculateInternalAuth( + BSONObjBuilder* isMasterRequest, std::shared_ptr<SaslClientSession>* saslClientSession) try { + auto params = getInternalAuthParams(0, kMechanismScramSha256.toString()); + if (params.isEmpty()) { + return SpeculativeAuthType::kNone; + } + + auto mechanism = getBSONString(params, saslCommandMechanismFieldName); + auto authDB = getBSONString(params, saslCommandUserDBFieldName); + + auto ret = _speculateAuth( + isMasterRequest, mechanism, HostAndPort(), authDB, params, saslClientSession); + if (!ret.isOK()) { + return SpeculativeAuthType::kNone; + } + + return ret.getValue(); +} catch (...) { + // Swallow any exception and fallback on explicit auth. + return SpeculativeAuthType::kNone; +} + +std::string getInternalAuthDB() { + stdx::lock_guard<Latch> lk(internalAuthKeysMutex); + + if (!internalAuthParams.isEmpty()) { + return getBSONString(internalAuthParams, saslCommandUserDBFieldName); + } + + auto isu = internalSecurity.user; + return isu ? isu->getName().getDB().toString() : "admin"; +} + } // namespace auth } // namespace mongo diff --git a/src/mongo/client/authenticate.h b/src/mongo/client/authenticate.h index 5578cb27f53..326b3c1fc5d 100644 --- a/src/mongo/client/authenticate.h +++ b/src/mongo/client/authenticate.h @@ -30,11 +30,14 @@ #pragma once #include <functional> +#include <memory> #include <string> #include "mongo/base/status_with.h" #include "mongo/base/string_data.h" #include "mongo/bson/bsonobj.h" +#include "mongo/client/mongo_uri.h" +#include "mongo/client/sasl_client_session.h" #include "mongo/db/auth/user_name.h" #include "mongo/executor/remote_command_response.h" #include "mongo/rpc/op_msg.h" @@ -67,6 +70,9 @@ constexpr auto kMechanismScramSha256 = "SCRAM-SHA-256"_sd; constexpr auto kMechanismMongoAWS = "MONGODB-AWS"_sd; constexpr auto kInternalAuthFallbackMechanism = kMechanismScramSha1; +constexpr auto kSpeculativeAuthenticate = "speculativeAuthenticate"_sd; +constexpr auto kAuthenticateCommand = "authenticate"_sd; + /** * Authenticate a user. * @@ -168,5 +174,34 @@ StringData getSaslCommandUserDBFieldName(); */ StringData getSaslCommandUserFieldName(); +/** + * Which type of speculative authentication was performed (if any). + */ +enum class SpeculativeAuthType { + kNone, + kAuthenticate, + kSaslStart, +}; + +/** + * Constructs a "speculativeAuthenticate" or "speculativeSaslStart" + * payload for an isMaster request based on a given URI. + */ +SpeculativeAuthType speculateAuth(BSONObjBuilder* isMasterRequest, + const MongoURI& uri, + std::shared_ptr<SaslClientSession>* saslClientSession); + +/** + * Constructs a "speculativeAuthenticate" or "speculativeSaslStart" + * payload for an isMaster request using internal (intracluster) authentication. + */ +SpeculativeAuthType speculateInternalAuth(BSONObjBuilder* isMasterRequest, + std::shared_ptr<SaslClientSession>* saslClientSession); + +/** + * Returns the AuthDB used by internal authentication. + */ +std::string getInternalAuthDB(); + } // namespace auth } // namespace mongo diff --git a/src/mongo/client/dbclient_base.h b/src/mongo/client/dbclient_base.h index b9c2f60b28f..ac4d302d172 100644 --- a/src/mongo/client/dbclient_base.h +++ b/src/mongo/client/dbclient_base.h @@ -681,6 +681,10 @@ public: virtual bool isMongos() const = 0; + virtual bool authenticatedDuringConnect() const { + return false; + } + /** * Parses command replies and runs them through the metadata reader. * This is virtual and non-const to allow subclasses to act on failures. diff --git a/src/mongo/client/dbclient_connection.cpp b/src/mongo/client/dbclient_connection.cpp index d43c788affa..5a9d29859cc 100644 --- a/src/mongo/client/dbclient_connection.cpp +++ b/src/mongo/client/dbclient_connection.cpp @@ -50,7 +50,10 @@ #include "mongo/client/constants.h" #include "mongo/client/dbclient_cursor.h" #include "mongo/client/replica_set_monitor.h" +#include "mongo/client/sasl_client_authenticate.h" +#include "mongo/client/sasl_client_session.h" #include "mongo/config.h" +#include "mongo/db/auth/sasl_command_constants.h" #include "mongo/db/auth/user_name.h" #include "mongo/db/client.h" #include "mongo/db/commands.h" @@ -108,78 +111,137 @@ private: const rpc::ProtocolSet _oldProtos; }; +StatusWith<bool> completeSpeculativeAuth(DBClientConnection* conn, + auth::SpeculativeAuthType speculativeAuthType, + std::shared_ptr<SaslClientSession> session, + const MongoURI& uri, + BSONObj isMaster) { + auto specAuthElem = isMaster[auth::kSpeculativeAuthenticate]; + if (specAuthElem.eoo()) { + return false; + } + + if (speculativeAuthType == auth::SpeculativeAuthType::kNone) { + return {ErrorCodes::BadValue, + str::stream() << "Unexpected isMaster." << auth::kSpeculativeAuthenticate + << " reply"}; + } + + if (specAuthElem.type() != Object) { + return {ErrorCodes::BadValue, + str::stream() << "isMaster." << auth::kSpeculativeAuthenticate + << " reply must be an object"}; + } + + auto specAuth = specAuthElem.Obj(); + if (specAuth.isEmpty()) { + return {ErrorCodes::BadValue, + str::stream() << "isMaster." << auth::kSpeculativeAuthenticate + << " reply must be a non-empty obejct"}; + } + + if (speculativeAuthType == auth::SpeculativeAuthType::kAuthenticate) { + return specAuth.hasField(saslCommandUserFieldName); + } + + invariant(speculativeAuthType == auth::SpeculativeAuthType::kSaslStart); + + const auto hook = [conn](OpMsgRequest request) -> Future<BSONObj> { + try { + auto ret = conn->runCommand(std::move(request)); + auto status = getStatusFromCommandResult(ret->getCommandReply()); + if (!status.isOK()) { + return status; + } + return ret->getCommandReply(); + } catch (const DBException& e) { + return e.toStatus(); + } + }; + + return asyncSaslConversation(hook, + session, + BSON(saslContinueCommandName << 1), + specAuth, + uri.getAuthenticationDatabase(), + kSaslClientLogLevelDefault) + .getNoThrow() + .isOK(); +} + /** * Initializes the wire version of conn, and returns the isMaster reply. */ -executor::RemoteCommandResponse initWireVersion(DBClientConnection* conn, - StringData applicationName, - const MongoURI& uri, - std::vector<std::string>* saslMechsForAuth) { - try { - // We need to force the usage of OP_QUERY on this command, even if we have previously - // detected support for OP_MSG on a connection. This is necessary to handle the case - // where we reconnect to an older version of MongoDB running at the same host/port. - ScopedForceOpQuery forceOpQuery{conn}; - - BSONObjBuilder bob; - bob.append("isMaster", 1); - - if (!uri.getUser().empty()) { - const auto authDatabase = uri.getAuthenticationDatabase(); - UserName user(uri.getUser(), authDatabase); - bob.append("saslSupportedMechs", user.getUnambiguousName()); - } +executor::RemoteCommandResponse initWireVersion( + DBClientConnection* conn, + StringData applicationName, + const MongoURI& uri, + std::vector<std::string>* saslMechsForAuth, + auth::SpeculativeAuthType* speculativeAuthType, + std::shared_ptr<SaslClientSession>* saslClientSession) try { + // We need to force the usage of OP_QUERY on this command, even if we have previously + // detected support for OP_MSG on a connection. This is necessary to handle the case + // where we reconnect to an older version of MongoDB running at the same host/port. + ScopedForceOpQuery forceOpQuery{conn}; - if (getTestCommandsEnabled()) { - // Only include the host:port of this process in the isMaster command request if test - // commands are enabled. mongobridge uses this field to identify the process opening a - // connection to it. - StringBuilder sb; - sb << getHostName() << ':' << serverGlobalParams.port; - bob.append("hostInfo", sb.str()); - } + BSONObjBuilder bob; + bob.append("isMaster", 1); - auto versionString = VersionInfoInterface::instance().version(); + *speculativeAuthType = auth::speculateAuth(&bob, uri, saslClientSession); + if (!uri.getUser().empty()) { + UserName user(uri.getUser(), uri.getAuthenticationDatabase()); + bob.append("saslSupportedMechs", user.getUnambiguousName()); + } - Status serializeStatus = ClientMetadata::serialize( - "MongoDB Internal Client", versionString, applicationName, &bob); - if (!serializeStatus.isOK()) { - return serializeStatus; - } + if (getTestCommandsEnabled()) { + // Only include the host:port of this process in the isMaster command request if test + // commands are enabled. mongobridge uses this field to identify the process opening a + // connection to it. + StringBuilder sb; + sb << getHostName() << ':' << serverGlobalParams.port; + bob.append("hostInfo", sb.str()); + } - conn->getCompressorManager().clientBegin(&bob); + auto versionString = VersionInfoInterface::instance().version(); - if (WireSpec::instance().isInternalClient) { - WireSpec::appendInternalClientWireVersion(WireSpec::instance().outgoing, &bob); - } + Status serializeStatus = + ClientMetadata::serialize("MongoDB Internal Client", versionString, applicationName, &bob); + if (!serializeStatus.isOK()) { + return serializeStatus; + } - Date_t start{Date_t::now()}; - auto result = conn->runCommand(OpMsgRequest::fromDBAndBody("admin", bob.obj())); - Date_t finish{Date_t::now()}; + conn->getCompressorManager().clientBegin(&bob); - BSONObj isMasterObj = result->getCommandReply().getOwned(); + if (WireSpec::instance().isInternalClient) { + WireSpec::appendInternalClientWireVersion(WireSpec::instance().outgoing, &bob); + } - if (isMasterObj.hasField("minWireVersion") && isMasterObj.hasField("maxWireVersion")) { - int minWireVersion = isMasterObj["minWireVersion"].numberInt(); - int maxWireVersion = isMasterObj["maxWireVersion"].numberInt(); - conn->setWireVersions(minWireVersion, maxWireVersion); - } + Date_t start{Date_t::now()}; + auto result = conn->runCommand(OpMsgRequest::fromDBAndBody("admin", bob.obj())); + Date_t finish{Date_t::now()}; - if (isMasterObj.hasField("saslSupportedMechs") && - isMasterObj["saslSupportedMechs"].type() == Array) { - auto array = isMasterObj["saslSupportedMechs"].Array(); - for (const auto& elem : array) { - saslMechsForAuth->push_back(elem.checkAndGetStringData().toString()); - } + BSONObj isMasterObj = result->getCommandReply().getOwned(); + + if (isMasterObj.hasField("minWireVersion") && isMasterObj.hasField("maxWireVersion")) { + int minWireVersion = isMasterObj["minWireVersion"].numberInt(); + int maxWireVersion = isMasterObj["maxWireVersion"].numberInt(); + conn->setWireVersions(minWireVersion, maxWireVersion); + } + + if (isMasterObj.hasField("saslSupportedMechs") && + isMasterObj["saslSupportedMechs"].type() == Array) { + auto array = isMasterObj["saslSupportedMechs"].Array(); + for (const auto& elem : array) { + saslMechsForAuth->push_back(elem.checkAndGetStringData().toString()); } + } - conn->getCompressorManager().clientFinish(isMasterObj); + conn->getCompressorManager().clientFinish(isMasterObj); - return executor::RemoteCommandResponse{std::move(isMasterObj), finish - start}; + return executor::RemoteCommandResponse{std::move(isMasterObj), finish - start}; - } catch (...) { - return exceptionToStatus(); - } +} catch (...) { + return exceptionToStatus(); } } // namespace @@ -231,7 +293,10 @@ Status DBClientConnection::connect(const HostAndPort& serverAddress, StringData // access the application name, do it through the _applicationName member. _applicationName = applicationName.toString(); - auto swIsMasterReply = initWireVersion(this, _applicationName, _uri, &_saslMechsForAuth); + auto speculativeAuthType = auth::SpeculativeAuthType::kNone; + std::shared_ptr<SaslClientSession> saslClientSession; + auto swIsMasterReply = initWireVersion( + this, _applicationName, _uri, &_saslMechsForAuth, &speculativeAuthType, &saslClientSession); if (!swIsMasterReply.isOK()) { _markFailed(kSetFlag); return swIsMasterReply.status; @@ -299,6 +364,18 @@ Status DBClientConnection::connect(const HostAndPort& serverAddress, StringData } } + { + auto swAuth = completeSpeculativeAuth( + this, speculativeAuthType, saslClientSession, _uri, swIsMasterReply.data); + if (!swAuth.isOK()) { + return swAuth.getStatus(); + } + + if (swAuth.getValue()) { + _authenticatedDuringConnect = true; + } + } + return Status::OK(); } diff --git a/src/mongo/client/dbclient_connection.h b/src/mongo/client/dbclient_connection.h index 9dccef0ea42..5d76701bc59 100644 --- a/src/mongo/client/dbclient_connection.h +++ b/src/mongo/client/dbclient_connection.h @@ -293,6 +293,10 @@ public: Status authenticateInternalUser() override; + bool authenticatedDuringConnect() const override { + return _authenticatedDuringConnect; + } + protected: int _minWireVersion{0}; int _maxWireVersion{0}; @@ -349,6 +353,8 @@ private: MessageCompressorManager _compressorManager; MongoURI _uri; + + bool _authenticatedDuringConnect = false; }; BSONElement getErrField(const BSONObj& result); diff --git a/src/mongo/client/dbclient_rs.cpp b/src/mongo/client/dbclient_rs.cpp index e241efc05ea..25fd973fc8f 100644 --- a/src/mongo/client/dbclient_rs.cpp +++ b/src/mongo/client/dbclient_rs.cpp @@ -741,7 +741,9 @@ DBClientConnection* DBClientReplicaSet::selectNodeUsingTags( _lastSlaveOkConn->setReplyMetadataReader(getReplyMetadataReader()); if (_authPooledSecondaryConn) { - _authConnection(_lastSlaveOkConn.get()); + if (!_lastSlaveOkConn->authenticatedDuringConnect()) { + _authConnection(_lastSlaveOkConn.get()); + } } else { // Mongos pooled connections are authenticated through ShardingConnectionHook::onCreate() } diff --git a/src/mongo/client/mongo_uri.cpp b/src/mongo/client/mongo_uri.cpp index 433c33c8e61..73c8ed8de76 100644 --- a/src/mongo/client/mongo_uri.cpp +++ b/src/mongo/client/mongo_uri.cpp @@ -39,11 +39,13 @@ #include <boost/algorithm/string/classification.hpp> #include <boost/algorithm/string/find_iterator.hpp> #include <boost/algorithm/string/predicate.hpp> +#include <boost/algorithm/string/split.hpp> #include <boost/range/algorithm/count.hpp> #include "mongo/base/status_with.h" #include "mongo/bson/bsonobjbuilder.h" #include "mongo/client/sasl_client_authenticate.h" +#include "mongo/db/auth/sasl_command_constants.h" #include "mongo/db/namespace_string.h" #include "mongo/stdx/utility.h" #include "mongo/util/dns_name.h" @@ -61,6 +63,7 @@ constexpr std::array<char, 16> hexits{ // a `std::map<std::string, std::string>` as the other parameter. const std::vector<std::pair<std::string, std::string>> permittedTXTOptions = {{"authSource"s, ""s}, {"replicaSet"s, ""s}}; + } // namespace /** @@ -571,4 +574,128 @@ std::string MongoURI::canonicalizeURIAsString() const { } return uri.str(); } + +namespace { +constexpr auto kAuthMechanismPropertiesKey = "mechanism_properties"_sd; + +constexpr auto kAuthServiceName = "SERVICE_NAME"_sd; +constexpr auto kAuthServiceRealm = "SERVICE_REALM"_sd; +constexpr auto kAuthAwsSessionToken = "AWS_SESSION_TOKEN"_sd; + +constexpr std::array<StringData, 3> kSupportedAuthMechanismProperties = { + kAuthServiceName, kAuthServiceRealm, kAuthAwsSessionToken}; + +BSONObj parseAuthMechanismProperties(const std::string& propStr) { + BSONObjBuilder bob; + std::vector<std::string> props; + boost::algorithm::split(props, propStr, boost::algorithm::is_any_of(",:")); + for (std::vector<std::string>::const_iterator it = props.begin(); it != props.end(); ++it) { + std::string prop((boost::algorithm::to_upper_copy(*it))); // normalize case + uassert(ErrorCodes::FailedToParse, + str::stream() << "authMechanismProperty: " << *it << " is not supported", + std::count(std::begin(kSupportedAuthMechanismProperties), + std::end(kSupportedAuthMechanismProperties), + StringData(prop))); + ++it; + uassert(ErrorCodes::FailedToParse, + str::stream() << "authMechanismProperty: " << prop << " must have a value", + it != props.end()); + bob.append(prop, *it); + } + return bob.obj(); +} +} // namespace + +boost::optional<BSONObj> MongoURI::makeAuthObjFromOptions( + int maxWireVersion, const std::vector<std::string>& saslMechsForAuth) const { + // Usually, a username is required to authenticate. + // However X509 based authentication may, and typically does, + // omit the username, inferring it from the client certificate instead. + bool usernameRequired = true; + + BSONObjBuilder bob; + if (!_password.empty()) { + bob.append(saslCommandPasswordFieldName, _password); + } + + auto it = _options.find("authSource"); + if (it != _options.end()) { + bob.append(saslCommandUserDBFieldName, it->second); + } else if (!_database.empty()) { + bob.append(saslCommandUserDBFieldName, _database); + } else { + bob.append(saslCommandUserDBFieldName, "admin"); + } + + it = _options.find("authMechanism"); + if (it != _options.end()) { + bob.append(saslCommandMechanismFieldName, it->second); + if (it->second == auth::kMechanismMongoX509 || it->second == auth::kMechanismMongoAWS) { + usernameRequired = false; + } + } else if (!saslMechsForAuth.empty()) { + if (std::find(saslMechsForAuth.begin(), + saslMechsForAuth.end(), + auth::kMechanismScramSha256) != saslMechsForAuth.end()) { + bob.append(saslCommandMechanismFieldName, auth::kMechanismScramSha256); + } else { + bob.append(saslCommandMechanismFieldName, auth::kMechanismScramSha1); + } + } else if (maxWireVersion >= 3) { + bob.append(saslCommandMechanismFieldName, auth::kMechanismScramSha1); + } else { + bob.append(saslCommandMechanismFieldName, auth::kMechanismMongoCR); + } + + if (usernameRequired && _user.empty()) { + return boost::none; + } + + std::string username(_user); // may have to tack on service realm before we append + + it = _options.find("authMechanismProperties"); + if (it != _options.end()) { + BSONObj parsed(parseAuthMechanismProperties(it->second)); + + bool hasNameProp = parsed.hasField(kAuthServiceName); + bool hasRealmProp = parsed.hasField(kAuthServiceRealm); + + uassert(ErrorCodes::FailedToParse, + "Cannot specify both gssapiServiceName and SERVICE_NAME", + !(hasNameProp && _options.count("gssapiServiceName"))); + // we append the parsed object so that mechanisms that don't accept it can assert. + bob.append(kAuthMechanismPropertiesKey, parsed); + // we still append using the old way the SASL code expects it + if (hasNameProp) { + bob.append(saslCommandServiceNameFieldName, parsed[kAuthServiceName].String()); + } + // if we specified a realm, we just append it to the username as the SASL code + // expects it that way. + if (hasRealmProp) { + if (username.empty()) { + // In practice, this won't actually occur since + // this block corresponds to GSSAPI, while username + // may only be omitted with MOGNODB-X509. + return boost::none; + } + username.append("@").append(parsed[kAuthServiceRealm].String()); + } + + if (parsed.hasField(kAuthAwsSessionToken)) { + bob.append(saslCommandIamSessionToken, parsed[kAuthAwsSessionToken].String()); + } + } + + it = _options.find("gssapiServiceName"); + if (it != _options.end()) { + bob.append(saslCommandServiceNameFieldName, it->second); + } + + if (!username.empty()) { + bob.append("user", username); + } + + return bob.obj(); +} + } // namespace mongo diff --git a/src/mongo/client/mongo_uri.h b/src/mongo/client/mongo_uri.h index c69b5e555c7..5b9f73039f8 100644 --- a/src/mongo/client/mongo_uri.h +++ b/src/mongo/client/mongo_uri.h @@ -45,6 +45,7 @@ #include "mongo/util/net/hostandport.h" namespace mongo { + /** * Encode a string for embedding in a URI. * Replaces reserved bytes with %xx sequences. @@ -261,6 +262,9 @@ public: friend StringBuilder& operator<<(StringBuilder&, const MongoURI&); + boost::optional<BSONObj> makeAuthObjFromOptions( + int maxWireVersion, const std::vector<std::string>& saslMechsForAuth) const; + private: MongoURI(ConnectionString connectString, const std::string& user, @@ -277,9 +281,6 @@ private: _sslMode(sslMode), _options(std::move(options)) {} - boost::optional<BSONObj> _makeAuthObjFromOptions( - int maxWireVersion, const std::vector<std::string>& saslMechsForAuth) const; - static MongoURI parseImpl(const std::string& url); ConnectionString _connectString; diff --git a/src/mongo/client/mongo_uri_connect.cpp b/src/mongo/client/mongo_uri_connect.cpp index 70109df8499..ae556d20aa9 100644 --- a/src/mongo/client/mongo_uri_connect.cpp +++ b/src/mongo/client/mongo_uri_connect.cpp @@ -31,150 +31,12 @@ #include "mongo/client/mongo_uri.h" -#include "mongo/base/status_with.h" -#include "mongo/bson/bsonobjbuilder.h" #include "mongo/client/authenticate.h" #include "mongo/client/dbclient_base.h" -#include "mongo/db/auth/sasl_command_constants.h" -#include "mongo/util/password_digest.h" #include "mongo/util/str.h" -#include <boost/algorithm/string/case_conv.hpp> -#include <boost/algorithm/string/classification.hpp> -#include <boost/algorithm/string/predicate.hpp> -#include <boost/algorithm/string/split.hpp> - -#include <iterator> - namespace mongo { -namespace { -const char kAuthMechanismPropertiesKey[] = "mechanism_properties"; - -// CANONICALIZE_HOST_NAME is currently unsupported -const char kAuthServiceName[] = "SERVICE_NAME"; -const char kAuthServiceRealm[] = "SERVICE_REALM"; -const char kAuthAwsSessionToken[] = "AWS_SESSION_TOKEN"; - -const char kAuthMechDefault[] = "DEFAULT"; - -const char* const kSupportedAuthMechanismProperties[] = { - kAuthServiceName, kAuthServiceRealm, kAuthAwsSessionToken}; - -BSONObj parseAuthMechanismProperties(const std::string& propStr) { - BSONObjBuilder bob; - std::vector<std::string> props; - boost::algorithm::split(props, propStr, boost::algorithm::is_any_of(",:")); - for (std::vector<std::string>::const_iterator it = props.begin(); it != props.end(); ++it) { - std::string prop((boost::algorithm::to_upper_copy(*it))); // normalize case - uassert(ErrorCodes::FailedToParse, - str::stream() << "authMechanismProperty: " << *it << " is not supported", - std::count(kSupportedAuthMechanismProperties, - std::end(kSupportedAuthMechanismProperties), - prop)); - ++it; - uassert(ErrorCodes::FailedToParse, - str::stream() << "authMechanismProperty: " << prop << " must have a value", - it != props.end()); - bob.append(prop, *it); - } - return bob.obj(); -} - -} // namespace - -boost::optional<BSONObj> MongoURI::_makeAuthObjFromOptions( - int maxWireVersion, const std::vector<std::string>& saslMechsForAuth) const { - // Usually, a username is required to authenticate. - // However X509 based authentication may, and typically does, - // omit the username, inferring it from the client certificate instead. - bool usernameRequired = true; - - BSONObjBuilder bob; - if (!_password.empty()) { - bob.append(saslCommandPasswordFieldName, _password); - } - - auto it = _options.find("authSource"); - if (it != _options.end()) { - bob.append(saslCommandUserDBFieldName, it->second); - } else if (!_database.empty()) { - bob.append(saslCommandUserDBFieldName, _database); - } else { - bob.append(saslCommandUserDBFieldName, "admin"); - } - - it = _options.find("authMechanism"); - if (it != _options.end()) { - bob.append(saslCommandMechanismFieldName, it->second); - if (it->second == auth::kMechanismMongoX509 || it->second == auth::kMechanismMongoAWS) { - usernameRequired = false; - } - } else if (!saslMechsForAuth.empty()) { - if (std::find(saslMechsForAuth.begin(), - saslMechsForAuth.end(), - auth::kMechanismScramSha256) != saslMechsForAuth.end()) { - bob.append(saslCommandMechanismFieldName, auth::kMechanismScramSha256); - } else { - bob.append(saslCommandMechanismFieldName, auth::kMechanismScramSha1); - } - } else if (maxWireVersion >= 3) { - bob.append(saslCommandMechanismFieldName, auth::kMechanismScramSha1); - } else { - bob.append(saslCommandMechanismFieldName, auth::kMechanismMongoCR); - } - - if (usernameRequired && _user.empty()) { - return boost::none; - } - - std::string username(_user); // may have to tack on service realm before we append - - it = _options.find("authMechanismProperties"); - if (it != _options.end()) { - BSONObj parsed(parseAuthMechanismProperties(it->second)); - - bool hasNameProp = parsed.hasField(kAuthServiceName); - bool hasRealmProp = parsed.hasField(kAuthServiceRealm); - - uassert(ErrorCodes::FailedToParse, - "Cannot specify both gssapiServiceName and SERVICE_NAME", - !(hasNameProp && _options.count("gssapiServiceName"))); - // we append the parsed object so that mechanisms that don't accept it can assert. - bob.append(kAuthMechanismPropertiesKey, parsed); - // we still append using the old way the SASL code expects it - if (hasNameProp) { - bob.append(saslCommandServiceNameFieldName, parsed[kAuthServiceName].String()); - } - // if we specified a realm, we just append it to the username as the SASL code - // expects it that way. - if (hasRealmProp) { - if (username.empty()) { - // In practice, this won't actually occur since - // this block corresponds to GSSAPI, while username - // may only be omitted with MOGNODB-X509. - return boost::none; - } - username.append("@").append(parsed[kAuthServiceRealm].String()); - } - - if (parsed.hasField(kAuthAwsSessionToken)) { - bob.append(saslCommandIamSessionToken, parsed[kAuthAwsSessionToken].String()); - } - } - - it = _options.find("gssapiServiceName"); - if (it != _options.end()) { - bob.append(saslCommandServiceNameFieldName, it->second); - } - - if (!username.empty()) { - bob.append("user", username); - } - - return bob.obj(); -} - DBClientBase* MongoURI::connect(StringData applicationName, std::string& errmsg, boost::optional<double> socketTimeoutSecs) const { @@ -200,10 +62,12 @@ DBClientBase* MongoURI::connect(StringData applicationName, return ret.release(); } - auto optAuthObj = - _makeAuthObjFromOptions(ret->getMaxWireVersion(), ret->getIsMasterSaslMechanisms()); - if (optAuthObj) { - ret->auth(optAuthObj.get()); + if (!ret->authenticatedDuringConnect()) { + auto optAuthObj = + makeAuthObjFromOptions(ret->getMaxWireVersion(), ret->getIsMasterSaslMechanisms()); + if (optAuthObj) { + ret->auth(optAuthObj.get()); + } } return ret.release(); diff --git a/src/mongo/client/sasl_client_authenticate.h b/src/mongo/client/sasl_client_authenticate.h index 4b342a41a99..7ac1419308f 100644 --- a/src/mongo/client/sasl_client_authenticate.h +++ b/src/mongo/client/sasl_client_authenticate.h @@ -29,14 +29,19 @@ #pragma once +#include <memory> +#include <string> + #include "mongo/base/status.h" #include "mongo/bson/bsontypes.h" #include "mongo/client/authenticate.h" #include "mongo/executor/remote_command_request.h" #include "mongo/executor/remote_command_response.h" +#include "mongo/util/future.h" namespace mongo { class BSONObj; +class SaslClientSession; /** * Attempts to authenticate "client" using the SASL protocol. @@ -83,4 +88,30 @@ extern Future<void> (*saslClientAuthenticate)(auth::RunCommandHook runCommand, * into "*payload". In all other cases, returns */ Status saslExtractPayload(const BSONObj& cmdObj, std::string* payload, BSONType* type); + +// Default log level on the client for SASL log messages. +constexpr int kSaslClientLogLevelDefault = 4; + +/** + * Configures and initializes "session" to perform the client side of a + * SASL conversation over connection "client". + * + * "saslParameters" is a BSON document providing the necessary configuration information. + * + * Returns Status::OK() on success. + */ +Status saslConfigureSession(SaslClientSession* session, + const HostAndPort& hostname, + StringData targetDatabase, + const BSONObj& saslParameters); + +/** + * Continue a previously started sasl session and proceed until completion. + */ +Future<void> asyncSaslConversation(auth::RunCommandHook runCommand, + const std::shared_ptr<SaslClientSession>& session, + const BSONObj& saslCommandPrefix, + const BSONObj& inputObj, + std::string targetDatabase, + int saslLogLevel); } // namespace mongo diff --git a/src/mongo/client/sasl_client_authenticate_impl.cpp b/src/mongo/client/sasl_client_authenticate_impl.cpp index 61dfbefd446..860f913971b 100644 --- a/src/mongo/client/sasl_client_authenticate_impl.cpp +++ b/src/mongo/client/sasl_client_authenticate_impl.cpp @@ -62,18 +62,20 @@ using std::endl; namespace { -// Default log level on the client for SASL log messages. -const int defaultSaslClientLogLevel = 4; - -const char* const saslClientLogFieldName = "clientLogLevel"; +constexpr auto saslClientLogFieldName = "clientLogLevel"_sd; int getSaslClientLogLevel(const BSONObj& saslParameters) { - int saslLogLevel = defaultSaslClientLogLevel; + int saslLogLevel = kSaslClientLogLevelDefault; BSONElement saslLogElement = saslParameters[saslClientLogFieldName]; - if (saslLogElement.trueValue()) + + if (saslLogElement.trueValue()) { saslLogLevel = 1; - if (saslLogElement.isNumber()) + } + + if (saslLogElement.isNumber()) { saslLogLevel = saslLogElement.numberInt(); + } + return saslLogLevel; } @@ -109,19 +111,12 @@ Status extractPassword(const BSONObj& saslParameters, } return Status::OK(); } +} // namespace -/** - * Configures "session" to perform the client side of a SASL conversation over connection - * "client". - * - * "saslParameters" is a BSON document providing the necessary configuration information. - * - * Returns Status::OK() on success. - */ -Status configureSession(SaslClientSession* session, - const HostAndPort& hostname, - StringData targetDatabase, - const BSONObj& saslParameters) { +Status saslConfigureSession(SaslClientSession* session, + const HostAndPort& hostname, + StringData targetDatabase, + const BSONObj& saslParameters) { std::string mechanism; Status status = bsonExtractStringField(saslParameters, saslCommandMechanismFieldName, &mechanism); @@ -241,6 +236,7 @@ Future<void> asyncSaslConversation(auth::RunCommandHook runCommand, }); } +namespace { /** * Driver for the client side of a sasl authentication session, conducted synchronously over * "client". @@ -270,7 +266,7 @@ Future<void> saslClientAuthenticateImpl(auth::RunCommandHook runCommand, // Come C++14, we should be able to do this in a nicer way. std::shared_ptr<SaslClientSession> session(SaslClientSession::create(mechanism)); - status = configureSession(session.get(), hostname, targetDatabase, saslParameters); + status = saslConfigureSession(session.get(), hostname, targetDatabase, saslParameters); if (!status.isOK()) return status; diff --git a/src/mongo/db/auth/SConscript b/src/mongo/db/auth/SConscript index 6c579a188eb..a7cce5850bb 100644 --- a/src/mongo/db/auth/SConscript +++ b/src/mongo/db/auth/SConscript @@ -245,6 +245,7 @@ env.Library( '$BUILD_DIR/mongo/db/commands', '$BUILD_DIR/mongo/db/commands/authentication_commands', '$BUILD_DIR/mongo/db/commands/test_commands_enabled', + '$BUILD_DIR/mongo/db/stats/counters', 'sasl_options_init', ], ) @@ -271,6 +272,7 @@ env.Library( ], LIBDEPS=[ '$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/db/stats/counters', '$BUILD_DIR/mongo/idl/server_parameter', ], ) diff --git a/src/mongo/db/auth/authentication_session.h b/src/mongo/db/auth/authentication_session.h index 5a26c46efd4..ff86ea7ca08 100644 --- a/src/mongo/db/auth/authentication_session.h +++ b/src/mongo/db/auth/authentication_session.h @@ -45,8 +45,8 @@ class AuthenticationSession { AuthenticationSession& operator=(const AuthenticationSession&) = delete; public: - explicit AuthenticationSession(std::unique_ptr<ServerMechanismBase> mech) - : _mech(std::move(mech)) {} + explicit AuthenticationSession(std::unique_ptr<ServerMechanismBase> mech, bool speculative) + : _mech(std::move(mech)), _speculative(speculative) {} /** * Sets the authentication session for the given "client" to "newSession". @@ -72,8 +72,13 @@ public: return _mech->setOptions(options); } + bool isSpeculative() const { + return _speculative; + } + private: std::unique_ptr<ServerMechanismBase> _mech; + bool _speculative{false}; }; } // namespace mongo diff --git a/src/mongo/db/auth/sasl_commands.cpp b/src/mongo/db/auth/sasl_commands.cpp index 1932b7e9a4e..a6276d3fec9 100644 --- a/src/mongo/db/auth/sasl_commands.cpp +++ b/src/mongo/db/auth/sasl_commands.cpp @@ -39,6 +39,7 @@ #include "mongo/bson/mutable/algorithm.h" #include "mongo/bson/mutable/document.h" #include "mongo/bson/util/bson_extract.h" +#include "mongo/client/authenticate.h" #include "mongo/client/sasl_client_authenticate.h" #include "mongo/db/audit.h" #include "mongo/db/auth/authentication_session.h" @@ -51,6 +52,7 @@ #include "mongo/db/commands.h" #include "mongo/db/commands/authentication_commands.h" #include "mongo/db/server_options.h" +#include "mongo/db/stats/counters.h" #include "mongo/util/base64.h" #include "mongo/util/log.h" #include "mongo/util/sequence_util.h" @@ -125,6 +127,7 @@ public: CmdSaslStart cmdSaslStart; CmdSaslContinue cmdSaslContinue; + Status buildResponse(const AuthenticationSession* session, const std::string& responsePayload, BSONType responsePayloadType, @@ -212,6 +215,9 @@ Status doSaslStep(OperationContext* opCtx, << " on " << mechanism.getAuthenticationDatabase() << " from client " << opCtx->getClient()->session()->remote(); } + if (session->isSpeculative()) { + authCounter.incSpeculativeAuthenticateSuccessful(mechanism.mechanismName().toString()); + } } return Status::OK(); } @@ -220,7 +226,8 @@ StatusWith<std::unique_ptr<AuthenticationSession>> doSaslStart(OperationContext* const std::string& db, const BSONObj& cmdObj, BSONObjBuilder* result, - std::string* principalName) { + std::string* principalName, + bool speculative) { bool autoAuthorize = false; Status status = bsonExtractBooleanFieldWithDefault( cmdObj, saslCommandAutoAuthorizeFieldName, autoAuthorizeDefault, &autoAuthorize); @@ -240,7 +247,16 @@ StatusWith<std::unique_ptr<AuthenticationSession>> doSaslStart(OperationContext* return swMech.getStatus(); } - auto session = std::make_unique<AuthenticationSession>(std::move(swMech.getValue())); + auto session = + std::make_unique<AuthenticationSession>(std::move(swMech.getValue()), speculative); + + if (speculative && + !session->getMechanism().properties().hasAllProperties( + SecurityPropertySet({SecurityProperty::kNoPlainText}))) { + return {ErrorCodes::BadValue, + "Plaintext mechanisms may not be used with speculativeSaslStart"}; + } + auto options = cmdObj["options"]; if (!options.eoo()) { if (options.type() != Object) { @@ -280,17 +296,11 @@ Status doSaslContinue(OperationContext* opCtx, return doSaslStep(opCtx, session, cmdObj, result); } -CmdSaslStart::CmdSaslStart() : BasicCommand(saslStartCommandName) {} -CmdSaslStart::~CmdSaslStart() {} - -std::string CmdSaslStart::help() const { - return "First step in a SASL authentication conversation."; -} - -bool CmdSaslStart::run(OperationContext* opCtx, - const std::string& db, - const BSONObj& cmdObj, - BSONObjBuilder& result) { +bool runSaslStart(OperationContext* opCtx, + const std::string& db, + const BSONObj& cmdObj, + BSONObjBuilder& result, + bool speculative) { opCtx->markKillOnClientDisconnect(); Client* client = opCtx->getClient(); AuthenticationSession::set(client, std::unique_ptr<AuthenticationSession>()); @@ -301,7 +311,7 @@ bool CmdSaslStart::run(OperationContext* opCtx, } std::string principalName; - auto swSession = doSaslStart(opCtx, db, cmdObj, &result, &principalName); + auto swSession = doSaslStart(opCtx, db, cmdObj, &result, &principalName, speculative); if (!swSession.isOK() || swSession.getValue()->getMechanism().isSuccess()) { audit::logAuthentication( @@ -315,6 +325,20 @@ bool CmdSaslStart::run(OperationContext* opCtx, return true; } +CmdSaslStart::CmdSaslStart() : BasicCommand(saslStartCommandName) {} +CmdSaslStart::~CmdSaslStart() {} + +std::string CmdSaslStart::help() const { + return "First step in a SASL authentication conversation."; +} + +bool CmdSaslStart::run(OperationContext* opCtx, + const std::string& db, + const BSONObj& cmdObj, + BSONObjBuilder& result) { + return runSaslStart(opCtx, db, cmdObj, result, false); +} + CmdSaslContinue::CmdSaslContinue() : BasicCommand(saslContinueCommandName) {} CmdSaslContinue::~CmdSaslContinue() {} @@ -371,4 +395,26 @@ MONGO_INITIALIZER(PreSaslCommands) } } // namespace + +void doSpeculativeSaslStart(OperationContext* opCtx, BSONObj cmdObj, BSONObjBuilder* result) try { + auto mechElem = cmdObj["mechanism"]; + if (mechElem.type() != String) { + return; + } + + authCounter.incSpeculativeAuthenticateReceived(mechElem.String()); + + auto dbElement = cmdObj["db"]; + if (dbElement.type() != String) { + return; + } + + BSONObjBuilder saslStartResult; + if (runSaslStart(opCtx, dbElement.String(), cmdObj, saslStartResult, true)) { + result->append(auth::kSpeculativeAuthenticate, saslStartResult.obj()); + } +} catch (...) { + // Treat failure like we never even got a speculative start. +} + } // namespace mongo diff --git a/src/mongo/db/auth/sasl_commands.h b/src/mongo/db/auth/sasl_commands.h new file mode 100644 index 00000000000..394e6034b0c --- /dev/null +++ b/src/mongo/db/auth/sasl_commands.h @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/bson/bsonobj.h" +#include "mongo/bson/bsonobjbuilder.h" + +namespace mongo { +class OperationContext; + +/** + * Handle isMaster: { speculativeAuthenticate: {...} } + */ +void doSpeculativeSaslStart(OperationContext* opCtx, BSONObj cmdObj, BSONObjBuilder* result); +} // namespace mongo diff --git a/src/mongo/db/auth/sasl_options.cpp b/src/mongo/db/auth/sasl_options.cpp index ac23ebf6618..d390ad99524 100644 --- a/src/mongo/db/auth/sasl_options.cpp +++ b/src/mongo/db/auth/sasl_options.cpp @@ -29,6 +29,7 @@ #include "mongo/db/auth/sasl_options.h" #include "mongo/db/auth/sasl_options_gen.h" +#include "mongo/db/stats/counters.h" #include "mongo/util/text.h" @@ -46,4 +47,13 @@ SASLGlobalParams::SASLGlobalParams() { // Default value for auth failed delay authFailedDelay.store(0); } + +namespace { +MONGO_INITIALIZER_WITH_PREREQUISITES(InitSpeculativeCounters, ("EndStartupOptionStorage")) +(InitializerContext*) { + authCounter.initializeMechanismMap(saslGlobalParams.authenticationMechanisms); + return Status::OK(); +} +} // namespace + } // namespace mongo diff --git a/src/mongo/db/auth/sasl_options.idl b/src/mongo/db/auth/sasl_options.idl index 5c6070e9701..c3753ff8d95 100644 --- a/src/mongo/db/auth/sasl_options.idl +++ b/src/mongo/db/auth/sasl_options.idl @@ -36,6 +36,8 @@ global: server_parameters: authenticationMechanisms: + # Note: mongo/db/stats/counter.cpp makes the assumption that this + # setting will never be changed at runtime. description: "The set of accepted authentication mechanisms" set_at: startup default: diff --git a/src/mongo/db/commands/SConscript b/src/mongo/db/commands/SConscript index 568e23c6272..679571a308f 100644 --- a/src/mongo/db/commands/SConscript +++ b/src/mongo/db/commands/SConscript @@ -192,6 +192,7 @@ env.Library( '$BUILD_DIR/mongo/db/auth/sasl_options', '$BUILD_DIR/mongo/db/auth/user_document_parser', '$BUILD_DIR/mongo/db/commands', + '$BUILD_DIR/mongo/db/stats/counters', '$BUILD_DIR/mongo/util/net/ssl_manager', ] ) diff --git a/src/mongo/db/commands/authentication_commands.cpp b/src/mongo/db/commands/authentication_commands.cpp index dd273d77faa..d51d9815d08 100644 --- a/src/mongo/db/commands/authentication_commands.cpp +++ b/src/mongo/db/commands/authentication_commands.cpp @@ -40,6 +40,7 @@ #include "mongo/base/status.h" #include "mongo/bson/mutable/algorithm.h" #include "mongo/bson/mutable/document.h" +#include "mongo/client/authenticate.h" #include "mongo/client/sasl_client_authenticate.h" #include "mongo/config.h" #include "mongo/db/audit.h" @@ -52,6 +53,7 @@ #include "mongo/db/commands.h" #include "mongo/db/commands/test_commands_enabled.h" #include "mongo/db/operation_context.h" +#include "mongo/db/stats/counters.h" #include "mongo/platform/random.h" #include "mongo/rpc/metadata/client_metadata.h" #include "mongo/rpc/metadata/client_metadata_ismaster.h" @@ -360,4 +362,24 @@ void disableAuthMechanism(StringData authMechanism) { } } +void doSpeculativeAuthenticate(OperationContext* opCtx, + BSONObj cmdObj, + BSONObjBuilder* result) try { + auto mechElem = cmdObj["mechanism"]; + if (mechElem.type() != String) { + return; + } + + auto mechanism = mechElem.String(); + authCounter.incSpeculativeAuthenticateReceived(mechanism); + + BSONObjBuilder authResult; + if (cmdAuthenticate.run(opCtx, "$external", cmdObj, authResult)) { + authCounter.incSpeculativeAuthenticateSuccessful(mechanism); + result->append(auth::kSpeculativeAuthenticate, authResult.obj()); + } +} catch (...) { + // Treat failure like we never even got a speculative start. +} + } // namespace mongo diff --git a/src/mongo/db/commands/authentication_commands.h b/src/mongo/db/commands/authentication_commands.h index d6193a8f4ac..c211b799ef2 100644 --- a/src/mongo/db/commands/authentication_commands.h +++ b/src/mongo/db/commands/authentication_commands.h @@ -30,11 +30,16 @@ #pragma once #include "mongo/base/string_data.h" +#include "mongo/bson/bsonobj.h" +#include "mongo/bson/bsonobjbuilder.h" namespace mongo { +class OperationContext; constexpr StringData kX509AuthMechanism = "MONGODB-X509"_sd; void disableAuthMechanism(StringData authMechanism); +void doSpeculativeAuthenticate(OperationContext* opCtx, BSONObj isMaster, BSONObjBuilder* result); + } // namespace mongo diff --git a/src/mongo/db/commands/server_status_servers.cpp b/src/mongo/db/commands/server_status_servers.cpp index e85301b7b7a..a3d23061823 100644 --- a/src/mongo/db/commands/server_status_servers.cpp +++ b/src/mongo/db/commands/server_status_servers.cpp @@ -95,7 +95,6 @@ public: } network; -#ifdef MONGO_CONFIG_SSL class Security : public ServerStatusSection { public: Security() : ServerStatusSection("security") {} @@ -106,15 +105,23 @@ public: BSONObj generateSection(OperationContext* opCtx, const BSONElement& configElement) const override { - BSONObj result; + BSONObjBuilder result; + + BSONObjBuilder auth; + authCounter.append(&auth); + result.append("authentication", auth.obj()); + +#ifdef MONGO_CONFIG_SSL if (getSSLManager()) { - result = getSSLManager()->getSSLConfiguration().getServerStatusBSON(); + getSSLManager()->getSSLConfiguration().getServerStatusBSON(&result); } +#endif - return result; + return result.obj(); } } security; +#ifdef MONGO_CONFIG_SSL /** * Status section of which tls versions connected to MongoDB and completed an SSL handshake. * Note: Clients are only not counted if they try to connect to the server with a unsupported TLS diff --git a/src/mongo/db/repl/SConscript b/src/mongo/db/repl/SConscript index 6efb849fc51..204e8042cd2 100644 --- a/src/mongo/db/repl/SConscript +++ b/src/mongo/db/repl/SConscript @@ -1128,6 +1128,8 @@ env.Library( 'replica_set_messages', ], LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/db/auth/authservercommon', + '$BUILD_DIR/mongo/db/commands/authentication_commands', '$BUILD_DIR/mongo/db/commands/server_status', '$BUILD_DIR/mongo/db/stats/counters', '$BUILD_DIR/mongo/transport/message_compressor', diff --git a/src/mongo/db/repl/replication_info.cpp b/src/mongo/db/repl/replication_info.cpp index 1f1d071e29a..6d65ffb2bd4 100644 --- a/src/mongo/db/repl/replication_info.cpp +++ b/src/mongo/db/repl/replication_info.cpp @@ -36,8 +36,11 @@ #include "mongo/bson/util/bson_extract.h" #include "mongo/client/connpool.h" #include "mongo/client/dbclient_connection.h" +#include "mongo/db/auth/sasl_command_constants.h" +#include "mongo/db/auth/sasl_commands.h" #include "mongo/db/auth/sasl_mechanism_registry.h" #include "mongo/db/client.h" +#include "mongo/db/commands/authentication_commands.h" #include "mongo/db/commands/server_status.h" #include "mongo/db/db_raii.h" #include "mongo/db/dbhelpers.h" @@ -516,6 +519,30 @@ public: } } + if (auto sae = cmdObj[auth::kSpeculativeAuthenticate]; !sae.eoo()) { + uassert(ErrorCodes::BadValue, + str::stream() << "isMaster." << auth::kSpeculativeAuthenticate + << " must be an Object", + sae.type() == Object); + auto specAuth = sae.Obj(); + + uassert(ErrorCodes::BadValue, + str::stream() << "isMaster." << auth::kSpeculativeAuthenticate + << " must be a non-empty Object", + !specAuth.isEmpty()); + auto specCmd = specAuth.firstElementFieldNameStringData(); + + if (specCmd == saslStartCommandName) { + doSpeculativeSaslStart(opCtx, specAuth, &result); + } else if (specCmd == auth::kAuthenticateCommand) { + doSpeculativeAuthenticate(opCtx, specAuth, &result); + } else { + uasserted(51769, + str::stream() << "isMaster." << auth::kSpeculativeAuthenticate + << " unknown command: " << specCmd); + } + } + return true; } } cmdismaster; diff --git a/src/mongo/db/s/SConscript b/src/mongo/db/s/SConscript index 976282cf4c3..d7bf39ae0d9 100644 --- a/src/mongo/db/s/SConscript +++ b/src/mongo/db/s/SConscript @@ -352,6 +352,7 @@ env.CppUnitTest( 'balancer/balancer_chunk_selection_policy_test.cpp', 'balancer/balancer_policy_test.cpp', 'balancer/cluster_statistics_test.cpp', + 'balancer/core_options_stub.cpp', 'balancer/migration_manager_test.cpp', 'balancer/migration_test_fixture.cpp', 'balancer/scoped_migration_request_test.cpp', diff --git a/src/mongo/db/s/balancer/core_options_stub.cpp b/src/mongo/db/s/balancer/core_options_stub.cpp new file mode 100644 index 00000000000..2b9c5d8b837 --- /dev/null +++ b/src/mongo/db/s/balancer/core_options_stub.cpp @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2020-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/base/init.h" +#include "mongo/base/status.h" + +namespace mongo { +namespace { +MONGO_INITIALIZER_GENERAL(CoreOptions_Store, + ("BeginStartupOptionStorage"), + ("EndStartupOptionStorage")) +(InitializerContext* context) { + return Status::OK(); +} +} // namespace +} // namespace mongo diff --git a/src/mongo/db/stats/counters.cpp b/src/mongo/db/stats/counters.cpp index 6cc06589d78..8e4458db7a7 100644 --- a/src/mongo/db/stats/counters.cpp +++ b/src/mongo/db/stats/counters.cpp @@ -33,6 +33,7 @@ #include "mongo/db/stats/counters.h" +#include "mongo/client/authenticate.h" #include "mongo/db/jsobj.h" #include "mongo/util/log.h" @@ -168,7 +169,65 @@ void NetworkCounter::append(BSONObjBuilder& b) { b.append("tcpFastOpen", tfo.obj()); } +void AuthCounter::initializeMechanismMap(const std::vector<std::string>& mechanisms) { + invariant(_mechanisms.empty()); + + for (const auto& mech : mechanisms) { + _mechanisms.emplace( + std::piecewise_construct, std::forward_as_tuple(mech), std::forward_as_tuple()); + } +} + +void AuthCounter::incSpeculativeAuthenticateReceived(const std::string& mechanism) try { + _mechanisms.at(mechanism).speculativeAuthenticate.received.fetchAndAddRelaxed(1); +} catch (const std::out_of_range&) { + uasserted(51767, + str::stream() << "Received " << auth::kSpeculativeAuthenticate << " for mechanism " + << mechanism << " which is unknown or not enabled"); +} + +void AuthCounter::incSpeculativeAuthenticateSuccessful(const std::string& mechanism) try { + _mechanisms.at(mechanism).speculativeAuthenticate.successful.fetchAndAddRelaxed(1); +} catch (const std::out_of_range&) { + // Should never actually occur since it'd mean we succeeded at a mechanism + // we're not configured for. + uasserted(51768, + str::stream() << "Unexpectedly succeeded at " << auth::kSpeculativeAuthenticate + << " for " << mechanism << " which is not enabled"); +} + +/** + * authentication: { + * "mechanisms": { + * "SCRAM-SHA-256": { + * "speculativeAuthenticate": { received: ###, successful: ### }, + * }, + * "MONGODB-X509": { + * "speculativeAuthenticate": { received: ###, successful: ### }, + * }, + * }, + * } + */ +void AuthCounter::append(BSONObjBuilder* b) { + BSONObjBuilder mechsBuilder(b->subobjStart("mechanisms")); + + for (const auto& it : _mechanisms) { + const auto received = it.second.speculativeAuthenticate.received.load(); + const auto successful = it.second.speculativeAuthenticate.successful.load(); + + BSONObjBuilder mechBuilder(mechsBuilder.subobjStart(it.first)); + BSONObjBuilder specAuthBuilder(mechBuilder.subobjStart(auth::kSpeculativeAuthenticate)); + specAuthBuilder.append("received", received); + specAuthBuilder.append("successful", successful); + specAuthBuilder.done(); + mechBuilder.done(); + } + + mechsBuilder.done(); +} + OpCounters globalOpCounters; OpCounters replOpCounters; NetworkCounter networkCounter; +AuthCounter authCounter; } // namespace mongo diff --git a/src/mongo/db/stats/counters.h b/src/mongo/db/stats/counters.h index 0afb3c6756c..edc17eee2bd 100644 --- a/src/mongo/db/stats/counters.h +++ b/src/mongo/db/stats/counters.h @@ -29,6 +29,8 @@ #pragma once +#include <map> + #include "mongo/db/jsobj.h" #include "mongo/platform/atomic_word.h" #include "mongo/platform/basic.h" @@ -165,4 +167,29 @@ private: }; extern NetworkCounter networkCounter; + +class AuthCounter { +public: + void incSpeculativeAuthenticateReceived(const std::string& mechanism); + void incSpeculativeAuthenticateSuccessful(const std::string& mechanism); + + void append(BSONObjBuilder*); + + void initializeMechanismMap(const std::vector<std::string>&); + +private: + struct MechanismData { + struct { + AtomicWord<long long> received; + AtomicWord<long long> successful; + } speculativeAuthenticate; + }; + using MechanismMap = std::map<std::string, MechanismData>; + + // Mechanism maps are initialized at startup to contain all + // mechanisms known to authenticationMechanisms setParam. + // After that they are kept to a fixed size. + MechanismMap _mechanisms; +}; +extern AuthCounter authCounter; } // namespace mongo diff --git a/src/mongo/executor/connection_pool_tl.cpp b/src/mongo/executor/connection_pool_tl.cpp index c3816eab43c..6d3150d1218 100644 --- a/src/mongo/executor/connection_pool_tl.cpp +++ b/src/mongo/executor/connection_pool_tl.cpp @@ -157,6 +157,7 @@ public: if (internalSecurity.user) { bob.append("saslSupportedMechs", internalSecurity.user->getName().getUnambiguousName()); } + _speculativeAuthType = auth::speculateInternalAuth(&bob, &_session); return bob.obj(); } @@ -164,7 +165,9 @@ public: Status validateHost(const HostAndPort& remoteHost, const BSONObj& isMasterRequest, const RemoteCommandResponse& isMasterReply) override try { - const auto saslMechsElem = isMasterReply.data.getField("saslSupportedMechs"); + const auto& reply = isMasterReply.data; + + const auto saslMechsElem = reply.getField("saslSupportedMechs"); if (saslMechsElem.type() == Array) { auto array = saslMechsElem.Array(); for (const auto& elem : array) { @@ -172,6 +175,11 @@ public: } } + const auto specAuth = reply.getField(auth::kSpeculativeAuthenticate); + if (specAuth.type() == Object) { + _speculativeAuthenticate = specAuth.Obj().getOwned(); + } + if (!_wrappedHook) { return Status::OK(); } else { @@ -202,8 +210,23 @@ public: return _saslMechsForInternalAuth; } + std::shared_ptr<SaslClientSession> getSession() { + return _session; + } + + auth::SpeculativeAuthType getSpeculativeAuthType() const { + return _speculativeAuthType; + } + + BSONObj getSpeculativeAuthenticateReply() { + return _speculativeAuthenticate; + } + private: std::vector<std::string> _saslMechsForInternalAuth; + std::shared_ptr<SaslClientSession> _session; + auth::SpeculativeAuthType _speculativeAuthType; + BSONObj _speculativeAuthenticate; executor::NetworkConnectionHook* const _wrappedHook = nullptr; }; @@ -240,8 +263,18 @@ void TLConnection::setup(Milliseconds timeout, SetupCallback cb) { _client = std::move(client); return _client->initWireVersion("NetworkInterfaceTL", isMasterHook.get()); }) - .then([this, isMasterHook] { + .then([this, isMasterHook]() -> Future<bool> { if (_skipAuth) { + return false; + } + + return _client->completeSpeculativeAuth(isMasterHook->getSession(), + auth::getInternalAuthDB(), + isMasterHook->getSpeculativeAuthenticateReply(), + isMasterHook->getSpeculativeAuthType()); + }) + .then([this, isMasterHook](bool authenticatedDuringConnect) { + if (_skipAuth || authenticatedDuringConnect) { return Future<void>::makeReady(); } diff --git a/src/mongo/util/net/ssl_manager.cpp b/src/mongo/util/net/ssl_manager.cpp index 6831d4c9daa..d0b6ae3988f 100644 --- a/src/mongo/util/net/ssl_manager.cpp +++ b/src/mongo/util/net/ssl_manager.cpp @@ -692,12 +692,10 @@ bool SSLConfiguration::isClusterMember(StringData subjectName) const { return !canonicalClient.empty() && (canonicalClient == _canonicalServerSubjectName); } -BSONObj SSLConfiguration::getServerStatusBSON() const { - BSONObjBuilder security; - security.append("SSLServerSubjectName", _serverSubjectName.toString()); - security.appendBool("SSLServerHasCertificateAuthority", hasCA); - security.appendDate("SSLServerCertificateExpirationDate", serverCertificateExpirationDate); - return security.obj(); +void SSLConfiguration::getServerStatusBSON(BSONObjBuilder* security) const { + security->append("SSLServerSubjectName", _serverSubjectName.toString()); + security->appendBool("SSLServerHasCertificateAuthority", hasCA); + security->appendDate("SSLServerCertificateExpirationDate", serverCertificateExpirationDate); } SSLManagerInterface::~SSLManagerInterface() {} diff --git a/src/mongo/util/net/ssl_manager.h b/src/mongo/util/net/ssl_manager.h index 499cdd85b7e..37a0bdf72b1 100644 --- a/src/mongo/util/net/ssl_manager.h +++ b/src/mongo/util/net/ssl_manager.h @@ -119,7 +119,7 @@ class SSLConfiguration { public: bool isClusterMember(StringData subjectName) const; bool isClusterMember(SSLX509Name subjectName) const; - BSONObj getServerStatusBSON() const; + void getServerStatusBSON(BSONObjBuilder*) const; Status setServerSubjectName(SSLX509Name name); const SSLX509Name& serverSubjectName() const { |