SERVER-126468: Support client authentication in NetworkInterfaceTL (#53705)
GitOrigin-RevId: 5fe2a8c85ef5238b26a909a2d254813abfe574fa
This commit is contained in:
parent
4617bedb9a
commit
0f2f1ffc39
@ -12,6 +12,20 @@ exports_files(
|
||||
# Contains only the core ConnectionString functionality, *not* the ability to
|
||||
# call connect() and return a DBClientBase* back. For that you need to link
|
||||
# against the 'clientdriver_network' library.
|
||||
mongo_cc_library(
|
||||
name = "credential",
|
||||
srcs = [
|
||||
"credential.cpp",
|
||||
],
|
||||
hdrs = [
|
||||
"credential.h",
|
||||
],
|
||||
deps = [
|
||||
"//src/mongo/bson/util:bson_extract",
|
||||
"//src/mongo/db/auth:auth_mechanism",
|
||||
],
|
||||
)
|
||||
|
||||
mongo_cc_library(
|
||||
name = "connection_string",
|
||||
srcs = [
|
||||
@ -19,6 +33,7 @@ mongo_cc_library(
|
||||
"mongo_uri.cpp",
|
||||
],
|
||||
deps = [
|
||||
":credential",
|
||||
"//src/mongo/util:dns_query",
|
||||
"//src/mongo/util/net:network",
|
||||
"//src/mongo/util/options_parser",
|
||||
@ -127,6 +142,7 @@ mongo_cc_library(
|
||||
],
|
||||
deps = [
|
||||
":connection_string",
|
||||
":credential",
|
||||
":internal_auth",
|
||||
":native_sasl_client",
|
||||
"//src/mongo/bson/util:bson_extract",
|
||||
@ -492,6 +508,7 @@ mongo_cc_unit_test(
|
||||
"authenticate_test.cpp",
|
||||
"backoff_with_jitter_test.cpp",
|
||||
"connection_string_test.cpp",
|
||||
"credential_test.cpp",
|
||||
"dbclient_cursor_test.cpp",
|
||||
"fetcher_test.cpp",
|
||||
"index_spec_test.cpp",
|
||||
@ -670,3 +687,15 @@ mongo_cc_library(
|
||||
":backoff_with_jitter",
|
||||
],
|
||||
)
|
||||
|
||||
mongo_cc_unit_test(
|
||||
name = "sasl_client_authenticate_impl_test",
|
||||
srcs = [
|
||||
"sasl_client_authenticate_impl_test.cpp",
|
||||
],
|
||||
tags = ["mongo_unittest_fourth_group"],
|
||||
deps = [
|
||||
":native_sasl_client",
|
||||
"//src/mongo/db:server_base",
|
||||
],
|
||||
)
|
||||
|
||||
@ -196,7 +196,7 @@ auth::RunCommandHook AsyncDBClient::_makeAuthRunCommandHook() {
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> AsyncDBClient::authenticate(const BSONObj& params) {
|
||||
Future<void> AsyncDBClient::authenticate(const auth::Credential& credential) {
|
||||
// We will only have a valid clientName if SSL is enabled.
|
||||
std::string clientName;
|
||||
#ifdef MONGO_CONFIG_SSL
|
||||
@ -205,7 +205,7 @@ Future<void> AsyncDBClient::authenticate(const BSONObj& params) {
|
||||
}
|
||||
#endif
|
||||
|
||||
return auth::authenticateClient(params, remote(), clientName, _makeAuthRunCommandHook());
|
||||
return auth::authenticateClient(credential, remote(), clientName, _makeAuthRunCommandHook());
|
||||
}
|
||||
|
||||
Future<void> AsyncDBClient::authenticateInternal(
|
||||
|
||||
@ -117,7 +117,7 @@ public:
|
||||
const BatonHandle& baton = nullptr,
|
||||
const CancellationToken& token = CancellationToken::uncancelable());
|
||||
|
||||
Future<void> authenticate(const BSONObj& params);
|
||||
Future<void> authenticate(const auth::Credential& credential);
|
||||
|
||||
Future<void> authenticateInternal(
|
||||
boost::optional<std::string> mechanismHint,
|
||||
|
||||
@ -34,13 +34,10 @@
|
||||
#include "mongo/base/status.h"
|
||||
#include "mongo/base/status_with.h"
|
||||
#include "mongo/bson/bsonelement.h"
|
||||
#include "mongo/bson/bsonmisc.h"
|
||||
#include "mongo/bson/bsonobj.h"
|
||||
#include "mongo/bson/bsontypes.h"
|
||||
#include "mongo/bson/util/bson_extract.h"
|
||||
#include "mongo/bson/util/builder.h"
|
||||
#include "mongo/bson/util/builder_fwd.h"
|
||||
#include "mongo/client/internal_auth.h"
|
||||
#include "mongo/client/mongo_uri.h"
|
||||
#include "mongo/client/sasl_client_authenticate.h"
|
||||
#include "mongo/config.h" // IWYU pragma: keep
|
||||
#include "mongo/db/auth/authorization_manager.h"
|
||||
@ -77,85 +74,55 @@ using AuthRequest = StatusWith<RemoteCommandRequest>;
|
||||
|
||||
namespace {
|
||||
|
||||
const char* const kUserSourceFieldName = "userSource";
|
||||
const BSONObj kGetNonceCmd = BSON("getnonce" << 1);
|
||||
|
||||
StatusWith<std::string> extractDBField(const BSONObj& params) {
|
||||
std::string db;
|
||||
if (params.hasField(kUserSourceFieldName)) {
|
||||
if (!bsonExtractStringField(params, kUserSourceFieldName, &db).isOK()) {
|
||||
return {ErrorCodes::AuthenticationFailed, "userSource field must contain a string"};
|
||||
}
|
||||
} else {
|
||||
if (!bsonExtractStringField(params, saslCommandUserDBFieldName, &db).isOK()) {
|
||||
return {ErrorCodes::AuthenticationFailed, "db field must contain a string"};
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
return std::move(db);
|
||||
}
|
||||
namespace {
|
||||
|
||||
//
|
||||
// X-509
|
||||
//
|
||||
|
||||
StatusWith<OpMsgRequest> createX509AuthCmd(const BSONObj& params, StringData clientName) {
|
||||
StatusWith<OpMsgRequest> createX509AuthCmd(const Credential& cred, StringData clientName) {
|
||||
if (clientName.empty()) {
|
||||
return {ErrorCodes::AuthenticationFailed,
|
||||
"Please enable SSL on the client-side to use the MONGODB-X509 authentication "
|
||||
"mechanism."};
|
||||
}
|
||||
auto db = extractDBField(params);
|
||||
if (!db.isOK())
|
||||
return db.getStatus();
|
||||
|
||||
std::string username;
|
||||
auto response = bsonExtractStringFieldWithDefault(
|
||||
params, saslCommandUserFieldName, std::string{clientName}, &username);
|
||||
if (!response.isOK()) {
|
||||
return response;
|
||||
}
|
||||
const std::string& username =
|
||||
(cred.username && !cred.username->empty()) ? *cred.username : std::string{clientName};
|
||||
if (username != std::string{clientName}) {
|
||||
StringBuilder message;
|
||||
message << "Username \"";
|
||||
message << params[saslCommandUserFieldName].valueStringData();
|
||||
message << "\" does not match the provided client certificate user \"";
|
||||
message << std::string{clientName} << "\"";
|
||||
return {ErrorCodes::AuthenticationFailed, message.str()};
|
||||
return {ErrorCodes::AuthenticationFailed,
|
||||
str::stream() << "Username \"" << username
|
||||
<< "\" does not match the provided client certificate user \""
|
||||
<< clientName << "\""};
|
||||
}
|
||||
|
||||
return OpMsgRequestBuilder::create(
|
||||
auth::ValidatedTenancyScope::kNotRequired /* db is not tenanted */,
|
||||
AuthDatabaseNameUtil::deserialize(db.getValue()),
|
||||
BSON("authenticate" << 1 << "mechanism"
|
||||
<< "MONGODB-X509"
|
||||
<< "user" << username));
|
||||
AuthDatabaseNameUtil::deserialize(cred.db.value_or("$external")),
|
||||
BSON("authenticate" << 1 << "mechanism" << kMechanismMongoX509 << "user" << username));
|
||||
}
|
||||
|
||||
// Use the MONGODB-X509 protocol to authenticate as "username." The certificate details
|
||||
// have already been communicated automatically as part of the connect call.
|
||||
Future<void> authX509(RunCommandHook runCommand,
|
||||
const HostAndPort& hostname,
|
||||
const BSONObj& params,
|
||||
const Credential& cred,
|
||||
StringData clientName) {
|
||||
invariant(runCommand);
|
||||
|
||||
// Just 1 step: send authenticate command, receive response
|
||||
auto swAuthRequest = createX509AuthCmd(params, clientName);
|
||||
auto swAuthRequest = createX509AuthCmd(cred, clientName);
|
||||
if (!swAuthRequest.isOK())
|
||||
return swAuthRequest.getStatus();
|
||||
|
||||
auto swTargetDb = extractDBField(params);
|
||||
if (!swTargetDb.isOK()) {
|
||||
return swTargetDb.getStatus();
|
||||
}
|
||||
auto targetDb = std::move(swTargetDb.getValue());
|
||||
|
||||
auto mechCounter = authCounter.getEgressMechanismCounter(kMechanismMongoX509);
|
||||
mechCounter.incAuthenticateSent();
|
||||
|
||||
auto argsBlock =
|
||||
std::make_tuple(hostname, params, std::string(clientName), targetDb, mechCounter);
|
||||
auto argsBlock = std::make_tuple(
|
||||
hostname, cred, std::string(clientName), cred.db.value_or("$external"), mechCounter);
|
||||
auto sharedBlock = std::make_shared<decltype(argsBlock)>(std::move(argsBlock));
|
||||
|
||||
auto metricsRecorder = std::make_shared<AuthMetricsRecorder>();
|
||||
@ -166,13 +133,13 @@ Future<void> authX509(RunCommandHook runCommand,
|
||||
return runCommand(swAuthRequest.getValue())
|
||||
.then([metricsRecorder, sharedBlock](const BSONObj& obj) {
|
||||
BSONObj metrics = metricsRecorder->captureEgress();
|
||||
auto [hostname, params, clientName, targetDb, mechCounter] = *sharedBlock.get();
|
||||
auto [hostname, cred, clientName, targetDb, mechCounter] = *sharedBlock.get();
|
||||
mechCounter.incEgressAuthenticateSuccessful();
|
||||
if (gEnableDetailedConnectionHealthMetricLogLines.load()) {
|
||||
LOGV2(10748708,
|
||||
"Authentication to remote host succeeded using MONGODB-X509",
|
||||
"hostname"_attr = hostname,
|
||||
"params"_attr = params,
|
||||
"username"_attr = cred.username.value_or(""),
|
||||
"subjectName"_attr = clientName,
|
||||
"targetDatabase"_attr = targetDb,
|
||||
"mechanism"_attr = kMechanismMongoX509,
|
||||
@ -182,12 +149,12 @@ Future<void> authX509(RunCommandHook runCommand,
|
||||
})
|
||||
.onError([metricsRecorder, sharedBlock](const Status& status) {
|
||||
BSONObj metrics = metricsRecorder->captureEgress();
|
||||
auto [hostname, params, clientName, targetDb, _] = *sharedBlock.get();
|
||||
auto [hostname, cred, clientName, targetDb, _] = *sharedBlock.get();
|
||||
if (gEnableDetailedConnectionHealthMetricLogLines.load()) {
|
||||
LOGV2(10748707,
|
||||
"Authentication to remote host failed using MONGODB-X509",
|
||||
"hostname"_attr = hostname,
|
||||
"params"_attr = params,
|
||||
"username"_attr = cred.username.value_or(""),
|
||||
"subjectName"_attr = clientName,
|
||||
"targetDatabase"_attr = targetDb,
|
||||
"mechanism"_attr = kMechanismMongoX509,
|
||||
@ -204,7 +171,7 @@ class DefaultInternalAuthParametersProvider : public InternalAuthParametersProvi
|
||||
public:
|
||||
~DefaultInternalAuthParametersProvider() override = default;
|
||||
|
||||
BSONObj get(size_t index, StringData mechanism) final {
|
||||
boost::optional<Credential> get(size_t index, StringData mechanism) final {
|
||||
return getInternalAuthParams(index, mechanism);
|
||||
}
|
||||
};
|
||||
@ -220,7 +187,7 @@ std::shared_ptr<InternalAuthParametersProvider> createDefaultInternalAuthProvide
|
||||
// General Auth
|
||||
//
|
||||
|
||||
Future<void> authenticateClient(const BSONObj& params,
|
||||
Future<void> authenticateClient(const Credential& credential,
|
||||
const HostAndPort& hostname,
|
||||
const std::string& clientName,
|
||||
RunCommandHook runCommand) {
|
||||
@ -237,26 +204,17 @@ Future<void> authenticateClient(const BSONObj& params,
|
||||
return status;
|
||||
};
|
||||
|
||||
std::string mechanism;
|
||||
auto response = bsonExtractStringField(params, saslCommandMechanismFieldName, &mechanism);
|
||||
if (!response.isOK())
|
||||
return response;
|
||||
|
||||
if (params.hasField(saslCommandUserDBFieldName) && params.hasField(kUserSourceFieldName)) {
|
||||
return Status(ErrorCodes::AuthenticationFailed,
|
||||
"You cannot specify both 'db' and 'userSource'. Please use only 'db'.");
|
||||
}
|
||||
|
||||
#ifdef MONGO_CONFIG_SSL
|
||||
else if (mechanism == kMechanismMongoX509)
|
||||
return authX509(runCommand, hostname, params, clientName).onError(errorHandler);
|
||||
if (credential.mechanism == AuthMechanism::kMongoX509)
|
||||
return authX509(runCommand, hostname, credential, clientName).onError(errorHandler);
|
||||
#endif
|
||||
|
||||
else if (saslClientAuthenticate != nullptr)
|
||||
return saslClientAuthenticate(runCommand, hostname, params).onError(errorHandler);
|
||||
if (saslClientAuthenticate != nullptr)
|
||||
return saslClientAuthenticate(runCommand, hostname, credential).onError(errorHandler);
|
||||
|
||||
return Status(ErrorCodes::AuthenticationFailed,
|
||||
mechanism + " mechanism support not compiled into client library.");
|
||||
str::stream() << toString(credential.mechanism)
|
||||
<< " mechanism support not compiled into client library.");
|
||||
};
|
||||
|
||||
Future<std::string> negotiateSaslMechanism(RunCommandHook runCommand,
|
||||
@ -316,21 +274,19 @@ Future<void> authenticateInternalClient(
|
||||
runCommand, (*systemUser)->getName(), mechanismHint, stepDownBehavior)
|
||||
.then([runCommand, clientSubjectName, remote, internalParamsProvider](
|
||||
std::string mechanism) -> Future<void> {
|
||||
auto params = internalParamsProvider->get(0, mechanism);
|
||||
if (params.isEmpty()) {
|
||||
auto cred = internalParamsProvider->get(0, mechanism);
|
||||
if (!cred) {
|
||||
return Status(ErrorCodes::BadValue,
|
||||
"Missing authentication parameters for internal user auth");
|
||||
}
|
||||
return authenticateClient(params, remote, clientSubjectName, runCommand)
|
||||
return authenticateClient(*cred, remote, clientSubjectName, runCommand)
|
||||
.onError<ErrorCodes::AuthenticationFailed>(
|
||||
[runCommand, clientSubjectName, remote, mechanism, internalParamsProvider](
|
||||
Status status) -> Future<void> {
|
||||
auto altCreds = internalParamsProvider->get(1, mechanism);
|
||||
if (!altCreds.isEmpty()) {
|
||||
return authenticateClient(
|
||||
altCreds, remote, clientSubjectName, runCommand);
|
||||
}
|
||||
return status;
|
||||
auto altCred = internalParamsProvider->get(1, mechanism);
|
||||
if (!altCred)
|
||||
return status;
|
||||
return authenticateClient(*altCred, remote, clientSubjectName, runCommand);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -361,30 +317,22 @@ StringData getSaslCommandUserFieldName() {
|
||||
namespace {
|
||||
|
||||
StatusWith<std::shared_ptr<SaslClientSession>> _speculateSaslStart(
|
||||
BSONObjBuilder* helloRequestBuilder,
|
||||
const std::string& mechanism,
|
||||
const HostAndPort& host,
|
||||
StringData authDB,
|
||||
BSONObj params) {
|
||||
if (mechanism == kMechanismSaslPlain) {
|
||||
BSONObjBuilder* helloRequestBuilder, const Credential& credential, const HostAndPort& host) {
|
||||
if (credential.mechanism == AuthMechanism::kSaslPlain) {
|
||||
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 username;
|
||||
status = bsonExtractStringFieldWithDefault(params, saslCommandUserFieldName, ""_sd, &username);
|
||||
const auto mechStr = toString(credential.mechanism);
|
||||
const auto authDB = credential.db.value_or(std::string{saslDefaultDBName});
|
||||
std::shared_ptr<SaslClientSession> session(SaslClientSession::create(std::string{mechStr}));
|
||||
auto status = saslConfigureSession(session.get(), host, credential);
|
||||
if (!status.isOK()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
std::string payload;
|
||||
|
||||
auto mechCounter = authCounter.getEgressMechanismCounter(mechanism);
|
||||
auto mechCounter = authCounter.getEgressMechanismCounter(mechStr);
|
||||
mechCounter.incAuthenticateSent();
|
||||
mechCounter.incSpeculativeAuthenticateSent();
|
||||
|
||||
@ -396,10 +344,9 @@ StatusWith<std::shared_ptr<SaslClientSession>> _speculateSaslStart(
|
||||
LOGV2(10748709,
|
||||
"Speculative authentication to remote host failed",
|
||||
"hostname"_attr = host,
|
||||
"saslParameters"_attr = params,
|
||||
"username"_attr = username,
|
||||
"username"_attr = credential.username.value_or(""),
|
||||
"targetDatabase"_attr = authDB,
|
||||
"mechanism"_attr = mechanism,
|
||||
"mechanism"_attr = mechStr,
|
||||
"error"_attr = redact(status),
|
||||
"result"_attr = status.code(),
|
||||
"metrics"_attr = metrics);
|
||||
@ -413,17 +360,16 @@ StatusWith<std::shared_ptr<SaslClientSession>> _speculateSaslStart(
|
||||
LOGV2(10748710,
|
||||
"Speculative authentication to remote host succeeded",
|
||||
"hostname"_attr = host,
|
||||
"saslParameters"_attr = params,
|
||||
"username"_attr = username,
|
||||
"username"_attr = credential.username.value_or(""),
|
||||
"targetDatabase"_attr = authDB,
|
||||
"mechanism"_attr = mechanism,
|
||||
"mechanism"_attr = mechStr,
|
||||
"result"_attr = Status::OK().code(),
|
||||
"metrics"_attr = metrics);
|
||||
}
|
||||
|
||||
BSONObjBuilder saslStart;
|
||||
saslStart.append("saslStart", 1);
|
||||
saslStart.append("mechanism", mechanism);
|
||||
saslStart.append("mechanism", mechStr);
|
||||
saslStart.appendBinData("payload", int(payload.size()), BinDataGeneral, payload.c_str());
|
||||
saslStart.append("db", authDB);
|
||||
helloRequestBuilder->append(kSpeculativeAuthenticate, saslStart.obj());
|
||||
@ -433,25 +379,23 @@ StatusWith<std::shared_ptr<SaslClientSession>> _speculateSaslStart(
|
||||
|
||||
StatusWith<SpeculativeAuthType> _speculateAuth(
|
||||
BSONObjBuilder* helloRequestBuilder,
|
||||
const std::string& mechanism,
|
||||
const Credential& credential,
|
||||
const HostAndPort& host,
|
||||
StringData authDB,
|
||||
BSONObj params,
|
||||
std::shared_ptr<SaslClientSession>* saslClientSession) {
|
||||
if (mechanism == kMechanismMongoX509) {
|
||||
if (credential.mechanism == AuthMechanism::kMongoX509) {
|
||||
// MONGODB-X509
|
||||
helloRequestBuilder->append(kSpeculativeAuthenticate,
|
||||
BSON(kAuthenticateCommand
|
||||
<< "1" << saslCommandMechanismFieldName << mechanism
|
||||
<< saslCommandUserDBFieldName << "$external"));
|
||||
helloRequestBuilder->append(
|
||||
kSpeculativeAuthenticate,
|
||||
BSON(kAuthenticateCommand
|
||||
<< "1" << saslCommandMechanismFieldName << toString(credential.mechanism)
|
||||
<< saslCommandUserDBFieldName << credential.db.value_or("$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(helloRequestBuilder, mechanism, host, authDB, params);
|
||||
auto swSaslClientSession = _speculateSaslStart(helloRequestBuilder, credential, host);
|
||||
if (!swSaslClientSession.isOK()) {
|
||||
return swSaslClientSession.getStatus();
|
||||
}
|
||||
@ -461,34 +405,25 @@ StatusWith<SpeculativeAuthType> _speculateAuth(
|
||||
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() == BSONType::string);
|
||||
return elem.String();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SpeculativeAuthType speculateAuth(BSONObjBuilder* helloRequestBuilder,
|
||||
const MongoURI& uri,
|
||||
std::shared_ptr<SaslClientSession>* saslClientSession) {
|
||||
auto mechanism =
|
||||
uri.getOption("authMechanism").get_value_or(std::string{kMechanismScramSha256});
|
||||
auto mechStr = uri.getOption("authMechanism").get_value_or(std::string{kMechanismScramSha256});
|
||||
|
||||
auto optParams = uri.makeAuthObjFromOptions(LATEST_WIRE_VERSION, {mechanism});
|
||||
auto optParams = uri.makeAuthObjFromOptions(LATEST_WIRE_VERSION, {mechStr});
|
||||
if (!optParams) {
|
||||
return SpeculativeAuthType::kNone;
|
||||
}
|
||||
|
||||
auto params = std::move(optParams.value());
|
||||
auto swCred = Credential::fromBSON(optParams.value());
|
||||
if (!swCred.isOK()) {
|
||||
return SpeculativeAuthType::kNone;
|
||||
}
|
||||
|
||||
auto ret = _speculateAuth(helloRequestBuilder,
|
||||
mechanism,
|
||||
uri.getServers().front(),
|
||||
uri.getAuthenticationDatabase(),
|
||||
params,
|
||||
saslClientSession);
|
||||
auto ret = _speculateAuth(
|
||||
helloRequestBuilder, swCred.getValue(), uri.getServers().front(), saslClientSession);
|
||||
if (!ret.isOK()) {
|
||||
// Ignore error, fallback on explicit auth.
|
||||
return SpeculativeAuthType::kNone;
|
||||
@ -501,16 +436,12 @@ SpeculativeAuthType speculateInternalAuth(
|
||||
const HostAndPort& remoteHost,
|
||||
BSONObjBuilder* helloRequestBuilder,
|
||||
std::shared_ptr<SaslClientSession>* saslClientSession) try {
|
||||
auto params = getInternalAuthParams(0, std::string{kMechanismScramSha256});
|
||||
if (params.isEmpty()) {
|
||||
auto cred = getInternalAuthParams(0, std::string{kMechanismScramSha256});
|
||||
if (!cred) {
|
||||
return SpeculativeAuthType::kNone;
|
||||
}
|
||||
|
||||
auto mechanism = getBSONString(params, saslCommandMechanismFieldName);
|
||||
auto authDB = getBSONString(params, saslCommandUserDBFieldName);
|
||||
|
||||
auto ret = _speculateAuth(
|
||||
helloRequestBuilder, mechanism, remoteHost, authDB, params, saslClientSession);
|
||||
auto ret = _speculateAuth(helloRequestBuilder, *cred, remoteHost, saslClientSession);
|
||||
if (!ret.isOK()) {
|
||||
return SpeculativeAuthType::kNone;
|
||||
}
|
||||
@ -521,5 +452,6 @@ SpeculativeAuthType speculateInternalAuth(
|
||||
return SpeculativeAuthType::kNone;
|
||||
}
|
||||
|
||||
|
||||
} // namespace auth
|
||||
} // namespace mongo
|
||||
|
||||
@ -33,12 +33,11 @@
|
||||
#include "mongo/base/string_data.h"
|
||||
#include "mongo/bson/bsonobj.h"
|
||||
#include "mongo/bson/bsonobjbuilder.h"
|
||||
#include "mongo/client/credential.h"
|
||||
#include "mongo/client/internal_auth.h"
|
||||
#include "mongo/client/mongo_uri.h"
|
||||
#include "mongo/client/sasl_client_session.h"
|
||||
#include "mongo/db/auth/user_name.h"
|
||||
#include "mongo/db/database_name.h"
|
||||
#include "mongo/executor/remote_command_response.h"
|
||||
#include "mongo/rpc/op_msg.h"
|
||||
#include "mongo/util/future.h"
|
||||
#include "mongo/util/modules.h"
|
||||
@ -55,24 +54,12 @@
|
||||
namespace mongo {
|
||||
|
||||
class BSONObj;
|
||||
class MongoURI;
|
||||
|
||||
namespace MONGO_MOD_PUBLIC auth {
|
||||
|
||||
using RunCommandHook = std::function<Future<BSONObj>(OpMsgRequest request)>;
|
||||
|
||||
/**
|
||||
* Names for supported authentication mechanisms.
|
||||
*/
|
||||
|
||||
constexpr auto kMechanismMongoX509 = "MONGODB-X509"_sd;
|
||||
constexpr auto kMechanismSaslPlain = "PLAIN"_sd;
|
||||
constexpr auto kMechanismGSSAPI = "GSSAPI"_sd;
|
||||
constexpr auto kMechanismScramSha1 = "SCRAM-SHA-1"_sd;
|
||||
constexpr auto kMechanismScramSha256 = "SCRAM-SHA-256"_sd;
|
||||
constexpr auto kMechanismMongoAWS = "MONGODB-AWS"_sd;
|
||||
constexpr auto kMechanismMongoOIDC = "MONGODB-OIDC"_sd;
|
||||
constexpr auto kInternalAuthFallbackMechanism = kMechanismScramSha1;
|
||||
|
||||
constexpr auto kSaslSupportedMechanisms = "saslSupportedMechs"_sd;
|
||||
constexpr auto kSpeculativeAuthenticate = "speculativeAuthenticate"_sd;
|
||||
constexpr auto kClusterAuthenticate = "clusterAuthenticate"_sd;
|
||||
@ -91,12 +78,12 @@ public:
|
||||
virtual ~InternalAuthParametersProvider() = default;
|
||||
|
||||
/**
|
||||
* Get the information for a given SASL mechanism.
|
||||
* Get the credential for a given SASL mechanism.
|
||||
*
|
||||
* If there are multiple entries for a mechanism, suppots retrieval by index. Used when rotating
|
||||
* the security key.
|
||||
* If there are multiple entries for a mechanism, supports retrieval by index. Used when
|
||||
* rotating the security key. Returns boost::none if no credential is available at that index.
|
||||
*/
|
||||
virtual BSONObj get(size_t index, StringData mechanism) = 0;
|
||||
virtual boost::optional<Credential> get(size_t index, StringData mechanism) = 0;
|
||||
};
|
||||
|
||||
std::shared_ptr<InternalAuthParametersProvider> createDefaultInternalAuthProvider();
|
||||
@ -108,27 +95,17 @@ std::shared_ptr<InternalAuthParametersProvider> createDefaultInternalAuthProvide
|
||||
* there is a stored client subject name, pass that through the "clientSubjectName" parameter.
|
||||
* Otherwise, "clientSubjectName" will be silently ignored, pass in any string.
|
||||
*
|
||||
* The "params" BSONObj should be initialized with some of the fields below. Which fields
|
||||
* are required depends on the mechanism, which is mandatory.
|
||||
*
|
||||
* "mechanism": The std::string name of the sasl mechanism to use. Mandatory.
|
||||
* "user": The std::string name of the user to authenticate. Mandatory.
|
||||
* "db": The database target of the auth command, which identifies the location
|
||||
* of the credential information for the user. May be "$external" if
|
||||
* credential information is stored outside of the mongo cluster. Mandatory.
|
||||
* "pwd": The password data.
|
||||
* "digestPassword": Boolean, set to true if the "pwd" is undigested (default).
|
||||
* "serviceName": The GSSAPI service name to use. Defaults to "mongodb".
|
||||
* "serviceHostname": The GSSAPI hostname to use. Defaults to the name of the remote
|
||||
* host.
|
||||
*
|
||||
* Other fields in "params" are silently ignored. A "params" object can be constructed
|
||||
* using the buildAuthParams() method.
|
||||
* The "credential" struct must have "mechanism" set. Other fields are mechanism-dependent:
|
||||
* - "username": required for SCRAM, PLAIN, GSSAPI; omitted for X.509, AWS, OIDC.
|
||||
* - "db": auth-source database; uses mechanism default ($external or admin) when absent.
|
||||
* - "password": required for SCRAM and PLAIN; absent for other mechanisms.
|
||||
* - "mechanismProperties": mechanism-specific options such as serviceName, serviceHostname,
|
||||
* awsIamSessionToken, oidcAccessToken, digestPassword.
|
||||
*
|
||||
* This function will return a future that will be filled with the final result of the
|
||||
* authentication command on success or a Status on error.
|
||||
*/
|
||||
Future<void> authenticateClient(const BSONObj& params,
|
||||
Future<void> authenticateClient(const Credential& credential,
|
||||
const HostAndPort& hostname,
|
||||
const std::string& clientSubjectName,
|
||||
RunCommandHook runCommand);
|
||||
@ -212,5 +189,6 @@ SpeculativeAuthType speculateInternalAuth(const HostAndPort& remoteHost,
|
||||
BSONObjBuilder* helloRequestBuilder,
|
||||
std::shared_ptr<SaslClientSession>* saslClientSession);
|
||||
|
||||
|
||||
} // namespace MONGO_MOD_PUBLIC auth
|
||||
} // namespace mongo
|
||||
|
||||
@ -150,12 +150,19 @@ public:
|
||||
#ifdef MONGO_CONFIG_SSL
|
||||
TEST_F(AuthClientTest, X509) {
|
||||
auto params = loadX509Conversation();
|
||||
auth::authenticateClient(params, HostAndPort(), _username, _runCommandCallback).get();
|
||||
auth::authenticateClient(uassertStatusOK(auth::Credential::fromBSON(params)),
|
||||
HostAndPort(),
|
||||
_username,
|
||||
_runCommandCallback)
|
||||
.get();
|
||||
}
|
||||
|
||||
TEST_F(AuthClientTest, asyncX509) {
|
||||
auto params = loadX509Conversation();
|
||||
ASSERT_OK(auth::authenticateClient(params, HostAndPort(), _username, _runCommandCallback)
|
||||
ASSERT_OK(auth::authenticateClient(uassertStatusOK(auth::Credential::fromBSON(params)),
|
||||
HostAndPort(),
|
||||
_username,
|
||||
_runCommandCallback)
|
||||
.getNoThrow());
|
||||
}
|
||||
#endif
|
||||
|
||||
120
src/mongo/client/credential.cpp
Normal file
120
src/mongo/client/credential.cpp
Normal file
@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright (C) 2026-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/client/credential.h"
|
||||
|
||||
#include "mongo/base/error_codes.h"
|
||||
#include "mongo/bson/bsonelement.h"
|
||||
#include "mongo/bson/bsonobj.h"
|
||||
#include "mongo/bson/bsonobjbuilder.h"
|
||||
#include "mongo/bson/util/bson_extract.h"
|
||||
#include "mongo/stdx/unordered_set.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <boost/optional/optional.hpp>
|
||||
|
||||
namespace mongo {
|
||||
namespace auth {
|
||||
|
||||
namespace {
|
||||
// BSON field names used in auth parameter documents.
|
||||
constexpr StringData kMechField = "mechanism"_sd;
|
||||
constexpr StringData kUserDBField = "db"_sd;
|
||||
constexpr StringData kUserSourceField = "userSource"_sd;
|
||||
constexpr StringData kUserField = "user"_sd;
|
||||
constexpr StringData kPasswordField = "pwd"_sd;
|
||||
} // namespace
|
||||
|
||||
StatusWith<Credential> Credential::fromBSON(const BSONObj& params) {
|
||||
if (params.hasField(kUserDBField) && params.hasField(kUserSourceField)) {
|
||||
return Status(ErrorCodes::AuthenticationFailed,
|
||||
"You cannot specify both 'db' and 'userSource'. Please use only 'db'.");
|
||||
}
|
||||
|
||||
std::string mechStr;
|
||||
if (auto s = bsonExtractStringField(params, kMechField, &mechStr); !s.isOK())
|
||||
return s;
|
||||
auto swMech = authMechanismFromString(mechStr);
|
||||
if (!swMech.isOK())
|
||||
return swMech.getStatus();
|
||||
|
||||
boost::optional<std::string> db;
|
||||
{
|
||||
std::string dbStr;
|
||||
if (params.hasField(kUserSourceField)) {
|
||||
if (auto s = bsonExtractStringField(params, kUserSourceField, &dbStr); !s.isOK())
|
||||
return s;
|
||||
if (!dbStr.empty())
|
||||
db = std::move(dbStr);
|
||||
} else if (params.hasField(kUserDBField)) {
|
||||
if (auto s = bsonExtractStringField(params, kUserDBField, &dbStr); !s.isOK())
|
||||
return s;
|
||||
if (!dbStr.empty())
|
||||
db = std::move(dbStr);
|
||||
}
|
||||
}
|
||||
|
||||
boost::optional<std::string> username;
|
||||
{
|
||||
std::string usernameStr;
|
||||
if (params.hasField(kUserField)) {
|
||||
if (auto s = bsonExtractStringField(params, kUserField, &usernameStr); !s.isOK())
|
||||
return s;
|
||||
if (!usernameStr.empty())
|
||||
username = std::move(usernameStr);
|
||||
}
|
||||
}
|
||||
|
||||
boost::optional<std::string> password;
|
||||
{
|
||||
std::string passwordStr;
|
||||
if (params.hasField(kPasswordField)) {
|
||||
if (auto s = bsonExtractStringField(params, kPasswordField, &passwordStr); !s.isOK())
|
||||
return s;
|
||||
if (!passwordStr.empty())
|
||||
password = std::move(passwordStr);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect mechanism-specific properties (everything except the 5 core fields).
|
||||
static const stdx::unordered_set<StringData> kCoreFields = {
|
||||
kMechField, kUserDBField, kUserSourceField, kUserField, kPasswordField};
|
||||
BSONObjBuilder props;
|
||||
for (const auto& elem : params) {
|
||||
if (!kCoreFields.count(elem.fieldNameStringData()))
|
||||
props.append(elem);
|
||||
}
|
||||
|
||||
return Credential{
|
||||
swMech.getValue(), std::move(db), std::move(username), std::move(password), props.obj()};
|
||||
}
|
||||
|
||||
} // namespace auth
|
||||
} // namespace mongo
|
||||
@ -26,34 +26,43 @@
|
||||
* exception statement from all source files in the program, then also delete
|
||||
* it in the license file.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "mongo/base/status.h"
|
||||
#include "mongo/client/authenticate.h"
|
||||
#include "mongo/base/status_with.h"
|
||||
#include "mongo/bson/bsonobj.h"
|
||||
#include "mongo/db/auth/auth_mechanism.h"
|
||||
#include "mongo/util/modules.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#include <boost/optional/optional.hpp>
|
||||
|
||||
namespace mongo {
|
||||
namespace MONGO_MOD_PUBLIC auth {
|
||||
|
||||
inline Status validateAuthMechanism(const std::string& value) {
|
||||
static constexpr std::array<StringData, 7> kValidMechanisms = {
|
||||
"MONGODB-X509"_sd,
|
||||
"PLAIN"_sd,
|
||||
"GSSAPI"_sd,
|
||||
"SCRAM-SHA-1"_sd,
|
||||
"SCRAM-SHA-256"_sd,
|
||||
"MONGODB-AWS"_sd,
|
||||
"MONGODB-OIDC"_sd,
|
||||
};
|
||||
if (std::find(kValidMechanisms.begin(), kValidMechanisms.end(), value) ==
|
||||
kValidMechanisms.end()) {
|
||||
return {ErrorCodes::BadValue,
|
||||
str::stream() << "Unknown authentication mechanism '" << value
|
||||
<< "'. Supported mechanisms: MONGODB-X509, PLAIN, GSSAPI, "
|
||||
"SCRAM-SHA-1, SCRAM-SHA-256, MONGODB-AWS, MONGODB-OIDC"};
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
/** Typed representation of client authentication credentials. */
|
||||
struct Credential {
|
||||
AuthMechanism mechanism;
|
||||
|
||||
/** Auth-source database. Uses mechanism default ($external or admin) when absent. */
|
||||
boost::optional<std::string> db;
|
||||
|
||||
/** Required for SCRAM, PLAIN, GSSAPI; absent for X.509 / AWS / OIDC. */
|
||||
boost::optional<std::string> username;
|
||||
|
||||
/** Required for SCRAM and PLAIN; absent for others. */
|
||||
boost::optional<std::string> password;
|
||||
|
||||
/** Mechanism-specific options. Not required, but not marked as optional for ergonomics. */
|
||||
BSONObj mechanismProperties;
|
||||
|
||||
/**
|
||||
* Parse and validate BSON into a Credential. Returns an error if a required field is missing or
|
||||
* if both "db" and the legacy "userSource" field are present.
|
||||
*/
|
||||
static StatusWith<Credential> fromBSON(const BSONObj& params);
|
||||
};
|
||||
|
||||
} // namespace MONGO_MOD_PUBLIC auth
|
||||
} // namespace mongo
|
||||
324
src/mongo/client/credential_test.cpp
Normal file
324
src/mongo/client/credential_test.cpp
Normal file
@ -0,0 +1,324 @@
|
||||
/**
|
||||
* Copyright (C) 2026-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/client/credential.h"
|
||||
|
||||
#include "mongo/base/error_codes.h"
|
||||
#include "mongo/base/string_data.h"
|
||||
#include "mongo/bson/bson_matcher.h"
|
||||
#include "mongo/bson/bsonmisc.h"
|
||||
#include "mongo/bson/bsonobj.h"
|
||||
#include "mongo/db/auth/auth_mechanism.h"
|
||||
#include "mongo/unittest/unittest.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <boost/optional/optional.hpp>
|
||||
|
||||
namespace mongo {
|
||||
namespace auth {
|
||||
namespace {
|
||||
|
||||
struct ParseSuccessCase {
|
||||
StringData name;
|
||||
BSONObj params;
|
||||
Credential expected;
|
||||
};
|
||||
|
||||
struct ParseFailureCase {
|
||||
StringData name;
|
||||
BSONObj params;
|
||||
boost::optional<ErrorCodes::Error> expectedCode;
|
||||
};
|
||||
|
||||
void assertCredentialEq(StringData name, const Credential& actual, const Credential& expected) {
|
||||
ASSERT_EQ(actual.mechanism, expected.mechanism) << name;
|
||||
ASSERT_EQ(actual.db, expected.db) << name << ": db";
|
||||
ASSERT_EQ(actual.username, expected.username) << name << ": username";
|
||||
ASSERT_EQ(actual.password, expected.password) << name << ": password";
|
||||
ASSERT_BSONOBJ_EQ_UNORDERED(actual.mechanismProperties, expected.mechanismProperties);
|
||||
}
|
||||
|
||||
TEST(CredentialTest, ParseSuccessCases) {
|
||||
const std::vector<ParseSuccessCase> cases = {
|
||||
{.name = "ParseValidScramSha256Credential",
|
||||
.params = BSON("mechanism" << "SCRAM-SHA-256"
|
||||
<< "user"
|
||||
<< "testuser"
|
||||
<< "db"
|
||||
<< "admin"
|
||||
<< "pwd"
|
||||
<< "secret"),
|
||||
.expected = {.mechanism = AuthMechanism::kScramSha256,
|
||||
.db = std::string{"admin"},
|
||||
.username = std::string{"testuser"},
|
||||
.password = std::string{"secret"}}},
|
||||
{.name = "ParseX509Credential",
|
||||
.params = BSON("mechanism" << "MONGODB-X509"
|
||||
<< "db"
|
||||
<< "$external"
|
||||
<< "user"
|
||||
<< "CN=test"),
|
||||
.expected = {.mechanism = AuthMechanism::kMongoX509,
|
||||
.db = std::string{"$external"},
|
||||
.username = std::string{"CN=test"}}},
|
||||
{.name = "ParseLegacyUserSourceField",
|
||||
.params = BSON("mechanism" << "SCRAM-SHA-1"
|
||||
<< "user"
|
||||
<< "testuser"
|
||||
<< "userSource"
|
||||
<< "admin"
|
||||
<< "pwd"
|
||||
<< "secret"),
|
||||
.expected = {.mechanism = AuthMechanism::kScramSha1,
|
||||
.db = std::string{"admin"},
|
||||
.username = std::string{"testuser"},
|
||||
.password = std::string{"secret"}}},
|
||||
{.name = "ParseMechanismProperties",
|
||||
.params = BSON("mechanism" << "GSSAPI"
|
||||
<< "user"
|
||||
<< "testuser"
|
||||
<< "db"
|
||||
<< "$external"
|
||||
<< "serviceName"
|
||||
<< "mongodb"
|
||||
<< "serviceHostname"
|
||||
<< "localhost"),
|
||||
.expected = {.mechanism = AuthMechanism::kGSSAPI,
|
||||
.db = std::string{"$external"},
|
||||
.username = std::string{"testuser"},
|
||||
.mechanismProperties = BSON("serviceName" << "mongodb"
|
||||
<< "serviceHostname"
|
||||
<< "localhost")}},
|
||||
{.name = "ParseMinimalCredential",
|
||||
.params = BSON("mechanism" << "MONGODB-AWS"),
|
||||
.expected = {.mechanism = AuthMechanism::kMongoAWS}},
|
||||
{.name = "ParseEmptyStringFields",
|
||||
.params = BSON("mechanism" << "SCRAM-SHA-256"
|
||||
<< "user"
|
||||
<< ""
|
||||
<< "pwd"
|
||||
<< ""
|
||||
<< "db"
|
||||
<< ""),
|
||||
.expected = {.mechanism = AuthMechanism::kScramSha256}},
|
||||
{.name = "DatabaseFieldPrecedence_UseDbWhenPresent",
|
||||
.params = BSON("mechanism" << "SCRAM-SHA-256"
|
||||
<< "user"
|
||||
<< "testuser"
|
||||
<< "db"
|
||||
<< "mydb"
|
||||
<< "pwd"
|
||||
<< "secret"),
|
||||
.expected = {.mechanism = AuthMechanism::kScramSha256,
|
||||
.db = std::string{"mydb"},
|
||||
.username = std::string{"testuser"},
|
||||
.password = std::string{"secret"}}},
|
||||
{.name = "DatabaseFieldPrecedence_UseUserSourceWhenDbMissing",
|
||||
.params = BSON("mechanism" << "SCRAM-SHA-256"
|
||||
<< "user"
|
||||
<< "testuser"
|
||||
<< "userSource"
|
||||
<< "sourcedb"
|
||||
<< "pwd"
|
||||
<< "secret"),
|
||||
.expected = {.mechanism = AuthMechanism::kScramSha256,
|
||||
.db = std::string{"sourcedb"},
|
||||
.username = std::string{"testuser"},
|
||||
.password = std::string{"secret"}}},
|
||||
{.name = "DatabaseFieldPrecedence_NoDbOrUserSource",
|
||||
.params = BSON("mechanism" << "SCRAM-SHA-256"
|
||||
<< "user"
|
||||
<< "testuser"
|
||||
<< "pwd"
|
||||
<< "secret"),
|
||||
.expected = {.mechanism = AuthMechanism::kScramSha256,
|
||||
.username = std::string{"testuser"},
|
||||
.password = std::string{"secret"}}},
|
||||
{.name = "EmptyDatabaseField",
|
||||
.params = BSON("mechanism" << "SCRAM-SHA-256"
|
||||
<< "user"
|
||||
<< "testuser"
|
||||
<< "db"
|
||||
<< ""
|
||||
<< "pwd"
|
||||
<< "secret"),
|
||||
.expected = {.mechanism = AuthMechanism::kScramSha256,
|
||||
.username = std::string{"testuser"},
|
||||
.password = std::string{"secret"}}},
|
||||
{.name = "SpecialCharactersInDatabase",
|
||||
.params = BSON("mechanism" << "SCRAM-SHA-256"
|
||||
<< "user"
|
||||
<< "testuser"
|
||||
<< "db"
|
||||
<< "my@db"
|
||||
<< "pwd"
|
||||
<< "secret"),
|
||||
.expected = {.mechanism = AuthMechanism::kScramSha256,
|
||||
.db = std::string{"my@db"},
|
||||
.username = std::string{"testuser"},
|
||||
.password = std::string{"secret"}}},
|
||||
{.name = "ExternalDatabaseForX509",
|
||||
.params = BSON("mechanism" << "MONGODB-X509"
|
||||
<< "user"
|
||||
<< "CN=test"
|
||||
<< "db"
|
||||
<< "$external"),
|
||||
.expected = {.mechanism = AuthMechanism::kMongoX509,
|
||||
.db = std::string{"$external"},
|
||||
.username = std::string{"CN=test"}}},
|
||||
{.name = "ParsePlainCredential",
|
||||
.params = BSON("mechanism" << "PLAIN"
|
||||
<< "user"
|
||||
<< "testuser"
|
||||
<< "db"
|
||||
<< "$external"
|
||||
<< "pwd"
|
||||
<< "secret"),
|
||||
.expected = {.mechanism = AuthMechanism::kSaslPlain,
|
||||
.db = std::string{"$external"},
|
||||
.username = std::string{"testuser"},
|
||||
.password = std::string{"secret"}}},
|
||||
{.name = "ParseOidcCredential",
|
||||
.params = BSON("mechanism" << "MONGODB-OIDC"
|
||||
<< "db"
|
||||
<< "$external"
|
||||
<< "user"
|
||||
<< "testuser"),
|
||||
.expected = {.mechanism = AuthMechanism::kMongoOIDC,
|
||||
.db = std::string{"$external"},
|
||||
.username = std::string{"testuser"}}},
|
||||
{.name = "ParseMongoCrCredential",
|
||||
.params = BSON("mechanism" << "MONGODB-CR"
|
||||
<< "user"
|
||||
<< "testuser"
|
||||
<< "db"
|
||||
<< "admin"
|
||||
<< "pwd"
|
||||
<< "secret"),
|
||||
.expected = {.mechanism = AuthMechanism::kMongoDbCr,
|
||||
.db = std::string{"admin"},
|
||||
.username = std::string{"testuser"},
|
||||
.password = std::string{"secret"}}},
|
||||
};
|
||||
|
||||
for (const auto& tc : cases) {
|
||||
auto swCred = Credential::fromBSON(tc.params);
|
||||
ASSERT_OK(swCred.getStatus()) << tc.name;
|
||||
assertCredentialEq(tc.name, swCred.getValue(), tc.expected);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(CredentialTest, ParseFailureCases) {
|
||||
const std::vector<ParseFailureCase> cases = {
|
||||
{.name = "RejectBothDbAndUserSource",
|
||||
.params = BSON("mechanism" << "SCRAM-SHA-256"
|
||||
<< "user"
|
||||
<< "testuser"
|
||||
<< "db"
|
||||
<< "test"
|
||||
<< "userSource"
|
||||
<< "admin"
|
||||
<< "pwd"
|
||||
<< "secret"),
|
||||
.expectedCode = ErrorCodes::AuthenticationFailed},
|
||||
{.name = "RejectMissingMechanism",
|
||||
.params = BSON("user" << "testuser"
|
||||
<< "pwd"
|
||||
<< "secret")},
|
||||
{.name = "RejectInvalidMechanism",
|
||||
.params = BSON("mechanism" << "INVALID-MECHANISM"
|
||||
<< "user"
|
||||
<< "testuser"),
|
||||
.expectedCode = ErrorCodes::InvalidOptions},
|
||||
{.name = "RejectEmptyMechanismString",
|
||||
.params = BSON("mechanism" << ""
|
||||
<< "user"
|
||||
<< "testuser"),
|
||||
.expectedCode = ErrorCodes::InvalidOptions},
|
||||
{.name = "RejectNonStringMechanism",
|
||||
.params = BSON("mechanism" << 42 << "user"
|
||||
<< "testuser"),
|
||||
.expectedCode = ErrorCodes::TypeMismatch},
|
||||
{.name = "RejectNonStringDbField",
|
||||
.params = BSON("mechanism" << "SCRAM-SHA-256"
|
||||
<< "db" << 42),
|
||||
.expectedCode = ErrorCodes::TypeMismatch},
|
||||
{.name = "RejectNonStringUserSourceField",
|
||||
.params = BSON("mechanism" << "SCRAM-SHA-256"
|
||||
<< "userSource" << 42),
|
||||
.expectedCode = ErrorCodes::TypeMismatch},
|
||||
{.name = "RejectNonStringUserField",
|
||||
.params = BSON("mechanism" << "SCRAM-SHA-256"
|
||||
<< "user" << 42),
|
||||
.expectedCode = ErrorCodes::TypeMismatch},
|
||||
{.name = "RejectNonStringPasswordField",
|
||||
.params = BSON("mechanism" << "SCRAM-SHA-256"
|
||||
<< "pwd" << 42),
|
||||
.expectedCode = ErrorCodes::TypeMismatch},
|
||||
};
|
||||
|
||||
for (const auto& tc : cases) {
|
||||
auto swCred = Credential::fromBSON(tc.params);
|
||||
ASSERT_NOT_OK(swCred.getStatus()) << tc.name;
|
||||
if (tc.expectedCode.has_value()) {
|
||||
ASSERT_EQ(swCred.getStatus().code(), *tc.expectedCode) << tc.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(CredentialTest, ParseMechanismMappingCases) {
|
||||
struct TestCase {
|
||||
StringData name;
|
||||
StringData mechanism;
|
||||
AuthMechanism expected;
|
||||
};
|
||||
|
||||
const std::vector<TestCase> cases = {
|
||||
{"ScramSha1", "SCRAM-SHA-1", AuthMechanism::kScramSha1},
|
||||
{"ScramSha256", "SCRAM-SHA-256", AuthMechanism::kScramSha256},
|
||||
{"MongoX509", "MONGODB-X509", AuthMechanism::kMongoX509},
|
||||
{"Plain", "PLAIN", AuthMechanism::kSaslPlain},
|
||||
{"Gssapi", "GSSAPI", AuthMechanism::kGSSAPI},
|
||||
{"MongoAws", "MONGODB-AWS", AuthMechanism::kMongoAWS},
|
||||
{"MongoOidc", "MONGODB-OIDC", AuthMechanism::kMongoOIDC},
|
||||
{"MongoCr", "MONGODB-CR", AuthMechanism::kMongoDbCr},
|
||||
};
|
||||
|
||||
for (const auto& tc : cases) {
|
||||
auto swCred = Credential::fromBSON(BSON("mechanism" << tc.mechanism));
|
||||
ASSERT_OK(swCred.getStatus()) << tc.name;
|
||||
ASSERT_EQ(swCred.getValue().mechanism, tc.expected) << tc.name;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace auth
|
||||
} // namespace mongo
|
||||
|
||||
@ -408,7 +408,8 @@ void DBClientBase::_auth(const BSONObj& params) {
|
||||
#endif
|
||||
|
||||
HostAndPort remote(getServerAddress());
|
||||
auth::authenticateClient(params, remote, clientName, _makeAuthRunCommandHook()).get();
|
||||
auto credential = uassertStatusOK(auth::Credential::fromBSON(params));
|
||||
auth::authenticateClient(credential, remote, clientName, _makeAuthRunCommandHook()).get();
|
||||
_isClientAuthenticated.store(true);
|
||||
}
|
||||
|
||||
@ -468,7 +469,8 @@ void DBClientBase::auth(const DatabaseName& dbname, StringData username, StringD
|
||||
|
||||
// To prevent unexpected behavior for existing clients, default to SCRAM-SHA-1 if the SASL
|
||||
// negotiation does not succeeed for some reason.
|
||||
StringData mech = mechResult.isOK() ? mechResult.getValue() : "SCRAM-SHA-1"_sd;
|
||||
StringData mech =
|
||||
mechResult.isOK() ? mechResult.getValue() : auth::kInternalAuthFallbackMechanism;
|
||||
|
||||
const auto authParams = auth::buildAuthParams(dbname, username, password_text, mech);
|
||||
auth(authParams);
|
||||
|
||||
@ -188,8 +188,8 @@ executor::RemoteCommandResponse initWireVersion(
|
||||
}
|
||||
|
||||
*speculativeAuthType = auth::speculateAuth(&bob, uri, saslClientSession);
|
||||
if (!uri.getUser().empty()) {
|
||||
UserName user(uri.getUser(), uri.getAuthenticationDatabase());
|
||||
if (auto& cred = uri.getCredential(); cred && cred->username) {
|
||||
UserName user(*cred->username, uri.getAuthenticationDatabase());
|
||||
bob.append("saslSupportedMechs", user.getUnambiguousName());
|
||||
}
|
||||
|
||||
|
||||
@ -29,12 +29,8 @@
|
||||
|
||||
#include "mongo/client/internal_auth.h"
|
||||
|
||||
#include "mongo/base/error_codes.h"
|
||||
#include "mongo/bson/bsonelement.h"
|
||||
#include "mongo/bson/bsonmisc.h"
|
||||
#include "mongo/bson/bsonobjbuilder.h"
|
||||
#include "mongo/bson/bsontypes.h"
|
||||
#include "mongo/client/authenticate.h"
|
||||
#include "mongo/config.h" // IWYU pragma: keep
|
||||
#include "mongo/db/auth/authorization_manager.h"
|
||||
#include "mongo/db/auth/sasl_command_constants.h"
|
||||
@ -43,12 +39,9 @@
|
||||
#include "mongo/util/assert_util.h"
|
||||
#include "mongo/util/password_digest.h"
|
||||
#include "mongo/util/read_through_cache.h"
|
||||
#include "mongo/util/str.h"
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include <boost/move/utility_core.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
|
||||
namespace mongo {
|
||||
@ -57,19 +50,18 @@ namespace auth {
|
||||
static std::mutex internalAuthKeysMutex;
|
||||
static bool internalAuthSet = false;
|
||||
static std::vector<std::string> internalAuthKeys;
|
||||
static BSONObj internalAuthParams;
|
||||
static boost::optional<Credential> internalAuthCredential;
|
||||
|
||||
void setInternalAuthKeys(const std::vector<std::string>& keys) {
|
||||
std::lock_guard<std::mutex> lk(internalAuthKeysMutex);
|
||||
|
||||
internalAuthKeys = keys;
|
||||
fassert(50996, internalAuthKeys.size() > 0);
|
||||
internalAuthSet = true;
|
||||
}
|
||||
|
||||
void setInternalUserAuthParams(BSONObj obj) {
|
||||
void setInternalUserAuthParams(Credential credential) {
|
||||
std::lock_guard<std::mutex> lk(internalAuthKeysMutex);
|
||||
internalAuthParams = obj.getOwned();
|
||||
internalAuthCredential = std::move(credential);
|
||||
internalAuthKeys.clear();
|
||||
internalAuthSet = true;
|
||||
}
|
||||
@ -84,63 +76,52 @@ bool isInternalAuthSet() {
|
||||
return internalAuthSet;
|
||||
}
|
||||
|
||||
BSONObj createInternalX509AuthDocument(boost::optional<StringData> userName) {
|
||||
BSONObjBuilder builder;
|
||||
builder.append(saslCommandMechanismFieldName, "MONGODB-X509");
|
||||
builder.append(saslCommandUserDBFieldName, "$external");
|
||||
|
||||
if (userName) {
|
||||
builder.append(saslCommandUserFieldName, userName.value());
|
||||
}
|
||||
|
||||
return builder.obj();
|
||||
Credential createInternalX509AuthCredential(boost::optional<StringData> userName) {
|
||||
return Credential{
|
||||
.mechanism = AuthMechanism::kMongoX509,
|
||||
.db = std::string{"$external"},
|
||||
.username = userName ? boost::make_optional(std::string{*userName}) : boost::none,
|
||||
};
|
||||
}
|
||||
|
||||
BSONObj getInternalAuthParams(size_t idx, StringData mechanism) {
|
||||
boost::optional<Credential> getInternalAuthParams(size_t idx, StringData mechanism) {
|
||||
std::lock_guard<std::mutex> lk(internalAuthKeysMutex);
|
||||
if (!internalAuthSet) {
|
||||
return BSONObj();
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
// If we've set a specific BSONObj as the internal auth pararms, return it if the index
|
||||
// is zero (there are no alternate credentials if we've set a BSONObj explicitly).
|
||||
if (!internalAuthParams.isEmpty()) {
|
||||
return idx == 0 ? internalAuthParams : BSONObj();
|
||||
// Explicit credential (e.g. X.509): only one entry, no alternates.
|
||||
if (internalAuthCredential) {
|
||||
return idx == 0 ? internalAuthCredential : boost::none;
|
||||
}
|
||||
|
||||
// If the index is larger than the number of keys we know about then return an empty
|
||||
// BSONObj.
|
||||
if (idx + 1 > internalAuthKeys.size()) {
|
||||
return BSONObj();
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
auto swMech = authMechanismFromString(mechanism);
|
||||
if (!swMech.isOK())
|
||||
return boost::none;
|
||||
|
||||
auto password = internalAuthKeys.at(idx);
|
||||
auto systemUser = internalSecurity.getUser();
|
||||
if (mechanism == kMechanismScramSha1) {
|
||||
if (swMech.getValue() == AuthMechanism::kScramSha1) {
|
||||
password = mongo::createPasswordDigest((*systemUser)->getName().getUser(), password);
|
||||
}
|
||||
|
||||
return BSON(saslCommandMechanismFieldName
|
||||
<< mechanism << saslCommandUserDBFieldName << (*systemUser)->getName().getDB()
|
||||
<< saslCommandUserFieldName << (*systemUser)->getName().getUser()
|
||||
<< saslCommandPasswordFieldName << password << saslCommandDigestPasswordFieldName
|
||||
<< false);
|
||||
// digestPassword: false because the password is already in its final form above.
|
||||
return Credential{swMech.getValue(),
|
||||
std::string{(*systemUser)->getName().getDB()},
|
||||
std::string{(*systemUser)->getName().getUser()},
|
||||
std::move(password),
|
||||
BSON(saslCommandDigestPasswordFieldName << false)};
|
||||
}
|
||||
|
||||
std::string getBSONString(const BSONObj& container, StringData field) {
|
||||
auto elem = container[field];
|
||||
uassert(ErrorCodes::BadValue,
|
||||
str::stream() << "Field '" << field << "' must be of type string",
|
||||
elem.type() == BSONType::string);
|
||||
return elem.String();
|
||||
}
|
||||
|
||||
|
||||
std::string getInternalAuthDB() {
|
||||
std::lock_guard<std::mutex> lk(internalAuthKeysMutex);
|
||||
|
||||
if (!internalAuthParams.isEmpty()) {
|
||||
return getBSONString(internalAuthParams, saslCommandUserDBFieldName);
|
||||
if (internalAuthCredential && internalAuthCredential->db) {
|
||||
return *internalAuthCredential->db;
|
||||
}
|
||||
|
||||
auto systemUser = internalSecurity.getUser();
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "mongo/base/string_data.h"
|
||||
#include "mongo/bson/bsonobj.h"
|
||||
#include "mongo/client/credential.h"
|
||||
#include "mongo/util/modules.h"
|
||||
|
||||
#include <cstddef>
|
||||
@ -39,13 +39,9 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/none.hpp>
|
||||
#include <boost/optional/optional.hpp>
|
||||
|
||||
namespace mongo {
|
||||
|
||||
class BSONObj;
|
||||
|
||||
namespace MONGO_MOD_PUBLIC auth {
|
||||
|
||||
/**
|
||||
@ -58,7 +54,7 @@ void setInternalAuthKeys(const std::vector<std::string>& keys);
|
||||
/**
|
||||
* Sets the parameters for non-password based internal authentication.
|
||||
*/
|
||||
void setInternalUserAuthParams(BSONObj obj);
|
||||
void setInternalUserAuthParams(Credential credential);
|
||||
|
||||
/**
|
||||
* Returns whether there are multiple keys that will be tried while authenticating an internal
|
||||
@ -77,14 +73,15 @@ bool isInternalAuthSet();
|
||||
std::string getInternalAuthDB();
|
||||
|
||||
/**
|
||||
* Returns the internal auth sasl parameters.
|
||||
* Returns the internal auth credential for the given index and mechanism.
|
||||
* Returns boost::none if no internal auth has been configured or index is out of range.
|
||||
*/
|
||||
BSONObj getInternalAuthParams(size_t idx, StringData mechanism);
|
||||
boost::optional<Credential> getInternalAuthParams(size_t idx, StringData mechanism);
|
||||
|
||||
/**
|
||||
* Create a BSON document for internal authentication.
|
||||
* Create a Credential for internal X.509 authentication.
|
||||
*/
|
||||
BSONObj createInternalX509AuthDocument(boost::optional<StringData> userName = boost::none);
|
||||
Credential createInternalX509AuthCredential(boost::optional<StringData> userName = boost::none);
|
||||
|
||||
} // namespace MONGO_MOD_PUBLIC auth
|
||||
} // namespace mongo
|
||||
|
||||
@ -316,6 +316,53 @@ URIParts::URIParts(StringData uri) {
|
||||
database = databaseAndOptions.first;
|
||||
options = databaseAndOptions.second;
|
||||
}
|
||||
|
||||
// Resolves the auth mechanism with the following precedence:
|
||||
// 1. Explicit authMechanism URI option.
|
||||
// 2. SCRAM mechanism advertised by the server for the selected auth source (prefer SHA-256).
|
||||
// 3. Default to SCRAM-SHA-256 when server mechanisms are unavailable.
|
||||
StatusWith<auth::AuthMechanism> resolveMechanism(
|
||||
const MongoURI::OptionsMap& options,
|
||||
boost::optional<std::vector<std::string>> saslMechsForAuth = boost::none) {
|
||||
if (auto it = options.find("authMechanism"); it != options.end())
|
||||
return auth::authMechanismFromString(it->second);
|
||||
if (saslMechsForAuth) {
|
||||
return std::find(saslMechsForAuth->begin(),
|
||||
saslMechsForAuth->end(),
|
||||
auth::kMechanismScramSha256) != saslMechsForAuth->end()
|
||||
? auth::AuthMechanism::kScramSha256
|
||||
: auth::AuthMechanism::kScramSha1;
|
||||
}
|
||||
return auth::AuthMechanism::kScramSha256;
|
||||
}
|
||||
|
||||
bool mechanismRequiresUsername(auth::AuthMechanism mechanism) {
|
||||
return mechanism != auth::AuthMechanism::kMongoX509 &&
|
||||
mechanism != auth::AuthMechanism::kMongoAWS && mechanism != auth::AuthMechanism::kMongoOIDC;
|
||||
}
|
||||
|
||||
// Resolves the authentication database with the following precedence:
|
||||
// 1. Explicit non-empty authSource URI option.
|
||||
// 2. For X.509/AWS/OIDC/GSSAPI, default to $external.
|
||||
// 3. For PLAIN, use the URI database when present, otherwise $external.
|
||||
// 4. For all other mechanisms, use the URI database when present, otherwise admin.
|
||||
std::string resolveAuthSource(auth::AuthMechanism mechanism,
|
||||
const MongoURI::OptionsMap& options,
|
||||
StringData database) {
|
||||
const bool isExternalDefaultDbMech = mechanism == auth::AuthMechanism::kMongoX509 ||
|
||||
mechanism == auth::AuthMechanism::kMongoAWS ||
|
||||
mechanism == auth::AuthMechanism::kMongoOIDC || mechanism == auth::AuthMechanism::kGSSAPI;
|
||||
if (auto it = options.find("authSource"); it != options.end() && !it->second.empty())
|
||||
return it->second;
|
||||
if (isExternalDefaultDbMech)
|
||||
return std::string{DatabaseName::kExternal.db(omitTenant)};
|
||||
if (mechanism == auth::AuthMechanism::kSaslPlain)
|
||||
return !database.empty() ? std::string{database}
|
||||
: std::string{DatabaseName::kExternal.db(omitTenant)};
|
||||
return !database.empty() ? std::string{database}
|
||||
: std::string{DatabaseName::kAdmin.db(omitTenant)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MongoURI::CaseInsensitiveString::CaseInsensitiveString(std::string str)
|
||||
@ -523,12 +570,32 @@ MongoURI MongoURI::parseImpl(StringData url) {
|
||||
: transport::ConnectSSLMode::kDisableSSL;
|
||||
}
|
||||
|
||||
// Build a Credential if the URI carries authentication data.
|
||||
boost::optional<auth::Credential> credential;
|
||||
{
|
||||
auto swMech = resolveMechanism(options);
|
||||
uassertStatusOK(swMech);
|
||||
const auto mechanism = swMech.getValue();
|
||||
// Build credentials when either:
|
||||
// - a username is present, or
|
||||
// - the selected mechanism allows username omission (for example X509/AWS/OIDC).
|
||||
// In the latter case, a non-empty username is still valid and is forwarded when building
|
||||
// the auth command.
|
||||
if (!username.empty() || !mechanismRequiresUsername(mechanism)) {
|
||||
credential = auth::Credential{
|
||||
.mechanism = mechanism,
|
||||
.db = resolveAuthSource(mechanism, options, database),
|
||||
.username = username.empty() ? boost::none : boost::optional<std::string>{username},
|
||||
.password =
|
||||
password.empty() ? boost::none : boost::optional<std::string>{password}};
|
||||
}
|
||||
}
|
||||
|
||||
auto cs = replicaSetName.empty()
|
||||
? ConnectionString::forStandalones(std::move(servers))
|
||||
: ConnectionString::forReplicaSet(replicaSetName, std::move(servers));
|
||||
return MongoURI(std::move(cs),
|
||||
username,
|
||||
password,
|
||||
std::move(credential),
|
||||
database,
|
||||
retryWrites,
|
||||
tlsMode,
|
||||
@ -556,10 +623,10 @@ boost::optional<std::string> MongoURI::getAppName() const {
|
||||
std::string MongoURI::canonicalizeURIAsString() const {
|
||||
StringBuilder uri;
|
||||
uri << kURIPrefix;
|
||||
if (!_user.empty()) {
|
||||
uri << uriEncode(_user);
|
||||
if (!_password.empty()) {
|
||||
uri << ":" << uriEncode(_password);
|
||||
if (_credential && _credential->username && !_credential->username->empty()) {
|
||||
uri << uriEncode(*_credential->username);
|
||||
if (_credential->password && !_credential->password->empty()) {
|
||||
uri << ":" << uriEncode(*_credential->password);
|
||||
}
|
||||
uri << "@";
|
||||
}
|
||||
@ -640,42 +707,28 @@ boost::optional<BSONObj> MongoURI::makeAuthObjFromOptions(
|
||||
// and OIDC, which infers it from the access token.
|
||||
bool usernameRequired = true;
|
||||
|
||||
const auto credUser =
|
||||
(_credential && _credential->username) ? *_credential->username : std::string{};
|
||||
const auto credPassword =
|
||||
(_credential && _credential->password) ? *_credential->password : std::string{};
|
||||
|
||||
BSONObjBuilder bob;
|
||||
if (!_password.empty()) {
|
||||
bob.append(saslCommandPasswordFieldName, _password);
|
||||
if (!credPassword.empty()) {
|
||||
bob.append(saslCommandPasswordFieldName, credPassword);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
const auto mechanism = resolveMechanism(_options, saslMechsForAuth).getValue();
|
||||
usernameRequired = mechanismRequiresUsername(mechanism);
|
||||
bob.append(saslCommandUserDBFieldName, resolveAuthSource(mechanism, _options, _database));
|
||||
bob.append(saslCommandMechanismFieldName, auth::toString(mechanism));
|
||||
|
||||
it = _options.find("authMechanism");
|
||||
if (it != _options.end()) {
|
||||
bob.append(saslCommandMechanismFieldName, it->second);
|
||||
if (it->second == auth::kMechanismMongoX509 || it->second == auth::kMechanismMongoAWS ||
|
||||
it->second == auth::kMechanismMongoOIDC) {
|
||||
usernameRequired = false;
|
||||
}
|
||||
} else if (std::find(saslMechsForAuth.begin(),
|
||||
saslMechsForAuth.end(),
|
||||
auth::kMechanismScramSha256) != saslMechsForAuth.end()) {
|
||||
bob.append(saslCommandMechanismFieldName, auth::kMechanismScramSha256);
|
||||
} else {
|
||||
bob.append(saslCommandMechanismFieldName, auth::kMechanismScramSha1);
|
||||
}
|
||||
|
||||
if (usernameRequired && _user.empty()) {
|
||||
if (usernameRequired && credUser.empty()) {
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
std::string username(_user); // may have to tack on service realm before we append
|
||||
std::string username = credUser; // may have to tack on service realm before we append
|
||||
|
||||
it = _options.find("authMechanismProperties");
|
||||
auto it = _options.find("authMechanismProperties");
|
||||
if (it != _options.end()) {
|
||||
BSONObj parsed(parseAuthMechanismProperties(it->second));
|
||||
|
||||
|
||||
@ -35,6 +35,7 @@
|
||||
#include "mongo/bson/util/builder.h"
|
||||
#include "mongo/bson/util/builder_fwd.h"
|
||||
#include "mongo/client/connection_string.h"
|
||||
#include "mongo/client/credential.h"
|
||||
#include "mongo/transport/transport_layer.h"
|
||||
#include "mongo/util/assert_util.h"
|
||||
#include "mongo/util/modules.h"
|
||||
@ -167,20 +168,32 @@ public:
|
||||
const boost::optional<TransientSSLParams>& transientSSLParams = boost::none,
|
||||
ErrorCodes::Error* errcode = nullptr) const;
|
||||
|
||||
const std::string& getUser() const {
|
||||
return _user;
|
||||
}
|
||||
|
||||
void setUser(std::string newUsername) {
|
||||
_user = std::move(newUsername);
|
||||
}
|
||||
|
||||
const std::string& getPassword() const {
|
||||
return _password;
|
||||
if (_credential) {
|
||||
_credential->username = std::move(newUsername);
|
||||
} else {
|
||||
_credential = auth::Credential{auth::AuthMechanism::kScramSha256,
|
||||
/* db= */ boost::none,
|
||||
std::move(newUsername),
|
||||
/* password= */ boost::none,
|
||||
BSONObj{}};
|
||||
}
|
||||
}
|
||||
|
||||
void setPassword(std::string newPassword) {
|
||||
_password = std::move(newPassword);
|
||||
if (_credential) {
|
||||
_credential->password = std::move(newPassword);
|
||||
} else {
|
||||
_credential = auth::Credential{auth::AuthMechanism::kScramSha256,
|
||||
/* db= */ boost::none,
|
||||
/* username= */ boost::none,
|
||||
std::move(newPassword),
|
||||
BSONObj{}};
|
||||
}
|
||||
}
|
||||
|
||||
const boost::optional<auth::Credential>& getCredential() const {
|
||||
return _credential;
|
||||
}
|
||||
|
||||
const OptionsMap& getOptions() const {
|
||||
@ -310,16 +323,14 @@ public:
|
||||
|
||||
private:
|
||||
MongoURI(ConnectionString connectString,
|
||||
const std::string& user,
|
||||
const std::string& password,
|
||||
boost::optional<auth::Credential> credential,
|
||||
const std::string& database,
|
||||
boost::optional<bool> retryWrites,
|
||||
transport::ConnectSSLMode sslMode,
|
||||
boost::optional<bool> helloOk,
|
||||
OptionsMap options)
|
||||
: _connectString(std::move(connectString)),
|
||||
_user(user),
|
||||
_password(password),
|
||||
_credential(std::move(credential)),
|
||||
_database(database),
|
||||
_retryWrites(std::move(retryWrites)),
|
||||
_sslMode(sslMode),
|
||||
@ -328,8 +339,7 @@ private:
|
||||
|
||||
#ifdef MONGO_CONFIG_GRPC
|
||||
MongoURI(ConnectionString connectString,
|
||||
const std::string& user,
|
||||
const std::string& password,
|
||||
boost::optional<auth::Credential> credential,
|
||||
const std::string& database,
|
||||
boost::optional<bool> retryWrites,
|
||||
transport::ConnectSSLMode sslMode,
|
||||
@ -337,8 +347,7 @@ private:
|
||||
boost::optional<bool> grpc,
|
||||
OptionsMap options)
|
||||
: _connectString(std::move(connectString)),
|
||||
_user(user),
|
||||
_password(password),
|
||||
_credential(std::move(credential)),
|
||||
_database(database),
|
||||
_retryWrites(std::move(retryWrites)),
|
||||
_sslMode(sslMode),
|
||||
@ -350,8 +359,7 @@ private:
|
||||
static MongoURI parseImpl(StringData url);
|
||||
|
||||
ConnectionString _connectString;
|
||||
std::string _user;
|
||||
std::string _password;
|
||||
boost::optional<auth::Credential> _credential;
|
||||
std::string _database;
|
||||
boost::optional<bool> _retryWrites;
|
||||
transport::ConnectSSLMode _sslMode = transport::kGlobalSSLMode;
|
||||
|
||||
@ -38,6 +38,8 @@
|
||||
#include "mongo/bson/bsonobj.h"
|
||||
#include "mongo/bson/bsontypes.h"
|
||||
#include "mongo/bson/json.h"
|
||||
#include "mongo/db/auth/sasl_command_constants.h"
|
||||
#include "mongo/db/database_name.h"
|
||||
#include "mongo/db/service_context_test_fixture.h"
|
||||
#include "mongo/logv2/log.h"
|
||||
#include "mongo/unittest/unittest.h"
|
||||
@ -645,8 +647,9 @@ void testValidURIFormat(URITestCase testCase) {
|
||||
const auto cs_status = MongoURI::parse(testCase.URI);
|
||||
ASSERT_OK(cs_status);
|
||||
auto result = cs_status.getValue();
|
||||
ASSERT_EQ(testCase.uname, result.getUser());
|
||||
ASSERT_EQ(testCase.password, result.getPassword());
|
||||
auto& cred = result.getCredential();
|
||||
ASSERT_EQ(testCase.uname, cred ? cred->username.value_or("") : "");
|
||||
ASSERT_EQ(testCase.password, cred ? cred->password.value_or("") : "");
|
||||
ASSERT_EQ(testCase.type, result.type());
|
||||
ASSERT_EQ(testCase.setname, result.getReplicaSetName());
|
||||
ASSERT_EQ(testCase.numservers, result.getServers().size());
|
||||
@ -1044,9 +1047,10 @@ TEST(MongoURI, srvRecordTest) {
|
||||
ASSERT_OK(rs.getStatus()) << "Failed on URI: " << test.uri
|
||||
<< " data on line: " << test.lineNumber;
|
||||
auto rv = rs.getValue();
|
||||
ASSERT_EQ(rv.getUser(), test.user)
|
||||
auto& cred = rv.getCredential();
|
||||
ASSERT_EQ(cred ? cred->username.value_or("") : "", test.user)
|
||||
<< "Failed on URI: " << test.uri << " data on line: " << test.lineNumber;
|
||||
ASSERT_EQ(rv.getPassword(), test.password)
|
||||
ASSERT_EQ(cred ? cred->password.value_or("") : "", test.password)
|
||||
<< "Failed on URI: " << test.uri << " data on line : " << test.lineNumber;
|
||||
ASSERT_EQ(rv.getDatabase(), test.database)
|
||||
<< "Failed on URI: " << test.uri << " data on line : " << test.lineNumber;
|
||||
@ -1098,5 +1102,402 @@ TEST(MongoURI, Redact) {
|
||||
ASSERT_EQ(MongoURI::redact(toRedactSRV), redactedSRV);
|
||||
}
|
||||
|
||||
// MONGODB-CR is a deprecated mechanism. It must be recognized at URI parse time so that
|
||||
// username and password are preserved in the credential (they are needed to produce a useful
|
||||
// error at authentication time). Completely unknown mechanisms must fail at parse time.
|
||||
TEST(MongoURI, DeprecatedAndUnknownMechanisms) {
|
||||
// MONGODB-CR: parse succeeds; username/password are preserved.
|
||||
{
|
||||
auto rs = MongoURI::parse("mongodb://user:pwd@localhost/db?authMechanism=MONGODB-CR");
|
||||
ASSERT_OK(rs.getStatus());
|
||||
const auto& cred = rs.getValue().getCredential();
|
||||
ASSERT_TRUE(cred.has_value());
|
||||
ASSERT_EQ(cred->mechanism, auth::AuthMechanism::kMongoDbCr);
|
||||
ASSERT_EQ(cred->username.value_or(""), "user");
|
||||
ASSERT_EQ(cred->password.value_or(""), "pwd");
|
||||
ASSERT_EQ(cred->db.value_or(""), "db");
|
||||
}
|
||||
|
||||
// Truly unknown mechanism: parse must fail.
|
||||
{
|
||||
auto rs =
|
||||
MongoURI::parse("mongodb://user:pwd@localhost/db?authMechanism=COMPLETELY-UNKNOWN");
|
||||
ASSERT_NOT_OK(rs.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
// External auth mechanisms (X.509, AWS, OIDC) must use "$external" as the auth db even when
|
||||
// the URI carries a database path (e.g. /admin). Without an explicit authSource, picking up the
|
||||
// URI path database breaks speculative authentication because the saslStart lands on "admin"
|
||||
// instead of "$external".
|
||||
TEST(MongoURI, ExternalMechanismDefaultsToExternalDb) {
|
||||
struct TestCase {
|
||||
std::string uri;
|
||||
auth::AuthMechanism expectedMech;
|
||||
std::string expectedDb;
|
||||
};
|
||||
|
||||
const TestCase cases[] = {
|
||||
{.uri = "mongodb://localhost/admin?authMechanism=MONGODB-X509",
|
||||
.expectedMech = auth::AuthMechanism::kMongoX509,
|
||||
.expectedDb = "$external"},
|
||||
{.uri = "mongodb://localhost/?authMechanism=MONGODB-X509",
|
||||
.expectedMech = auth::AuthMechanism::kMongoX509,
|
||||
.expectedDb = "$external"},
|
||||
{.uri = "mongodb://localhost/admin?authMechanism=MONGODB-AWS",
|
||||
.expectedMech = auth::AuthMechanism::kMongoAWS,
|
||||
.expectedDb = "$external"},
|
||||
{.uri = "mongodb://localhost/admin?authMechanism=MONGODB-OIDC",
|
||||
.expectedMech = auth::AuthMechanism::kMongoOIDC,
|
||||
.expectedDb = "$external"},
|
||||
{.uri = "mongodb://user@localhost/admin?authMechanism=GSSAPI",
|
||||
.expectedMech = auth::AuthMechanism::kGSSAPI,
|
||||
.expectedDb = "$external"},
|
||||
// Explicit authSource overrides the external default.
|
||||
{.uri = "mongodb://localhost/admin?authMechanism=MONGODB-X509&authSource=$external",
|
||||
.expectedMech = auth::AuthMechanism::kMongoX509,
|
||||
.expectedDb = "$external"},
|
||||
{.uri = "mongodb://user:pwd@localhost/admin?authMechanism=SCRAM-SHA-256",
|
||||
.expectedMech = auth::AuthMechanism::kScramSha256,
|
||||
.expectedDb = "admin"},
|
||||
{.uri = "mongodb://user:pwd@localhost/admin?authMechanism=SCRAM-SHA-1",
|
||||
.expectedMech = auth::AuthMechanism::kScramSha1,
|
||||
.expectedDb = "admin"},
|
||||
{.uri = "mongodb://user:pwd@localhost/?authMechanism=PLAIN",
|
||||
.expectedMech = auth::AuthMechanism::kSaslPlain,
|
||||
.expectedDb = "$external"},
|
||||
{.uri = "mongodb://user:pwd@localhost/admin?authMechanism=MONGODB-CR",
|
||||
.expectedMech = auth::AuthMechanism::kMongoDbCr,
|
||||
.expectedDb = "admin"},
|
||||
// No authMechanism specified: URI parsing defaults to SCRAM-SHA-256.
|
||||
{.uri = "mongodb://user:pwd@localhost/admin",
|
||||
.expectedMech = auth::AuthMechanism::kScramSha256,
|
||||
.expectedDb = "admin"},
|
||||
};
|
||||
|
||||
for (const auto& tc : cases) {
|
||||
auto rs = MongoURI::parse(tc.uri);
|
||||
ASSERT_OK(rs.getStatus()) << "URI: " << tc.uri;
|
||||
const auto& cred = rs.getValue().getCredential();
|
||||
ASSERT_TRUE(cred.has_value()) << "URI: " << tc.uri;
|
||||
ASSERT_EQ(cred->mechanism, tc.expectedMech) << "URI: " << tc.uri;
|
||||
ASSERT_EQ(cred->db.value_or(""), tc.expectedDb) << "URI: " << tc.uri;
|
||||
}
|
||||
}
|
||||
|
||||
// Test authSource precedence according to the MongoDB driver specification:
|
||||
// 1. Explicit authSource option (highest priority)
|
||||
// 2. Database path in URI
|
||||
// 3. Mechanism-specific default is used (e.g. "admin" for SCRAM).
|
||||
TEST(MongoURI, AuthSourcePrecedence) {
|
||||
struct TestCase {
|
||||
std::string uri;
|
||||
std::string expectedDb;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
const TestCase cases[] = {
|
||||
// Explicit authSource takes precedence over database path
|
||||
{"mongodb://user:pwd@localhost/mydb?authSource=authdb",
|
||||
"authdb",
|
||||
"Explicit authSource overrides database path"},
|
||||
|
||||
// Database path is used when no authSource specified
|
||||
{"mongodb://user:pwd@localhost/mydb", "mydb", "Database path used as authSource"},
|
||||
|
||||
// Empty database path with explicit authSource
|
||||
{"mongodb://user:pwd@localhost/?authSource=customdb",
|
||||
"customdb",
|
||||
"Explicit authSource with no database path"},
|
||||
|
||||
// No database path and no authSource uses mechanism-specific default.
|
||||
{"mongodb://user:pwd@localhost/?authMechanism=SCRAM-SHA-256",
|
||||
"admin",
|
||||
"No authSource and no database path default to admin for SCRAM"},
|
||||
|
||||
// Explicit authSource can override to non-standard database
|
||||
{"mongodb://user:pwd@localhost/prod?authSource=test",
|
||||
"test",
|
||||
"authSource overrides to different database"},
|
||||
|
||||
// Database path with special characters (URL encoded)
|
||||
{"mongodb://user:pwd@localhost/my%40db?authMechanism=SCRAM-SHA-256",
|
||||
"my@db",
|
||||
"Database path with special characters"},
|
||||
|
||||
// Explicit authSource with special characters
|
||||
{"mongodb://user:pwd@localhost/?authSource=my%40db",
|
||||
"my@db",
|
||||
"authSource with special characters"},
|
||||
};
|
||||
|
||||
for (const auto& tc : cases) {
|
||||
auto rs = MongoURI::parse(tc.uri);
|
||||
ASSERT_OK(rs.getStatus()) << "Failed to parse URI: " << tc.uri
|
||||
<< " (test: " << tc.description << ")";
|
||||
const auto& cred = rs.getValue().getCredential();
|
||||
ASSERT_TRUE(cred.has_value())
|
||||
<< "No credential created for URI: " << tc.uri << " (test: " << tc.description << ")";
|
||||
ASSERT_TRUE(cred->db.has_value()) << "No authSource in credential for URI: " << tc.uri
|
||||
<< " (test: " << tc.description << ")";
|
||||
ASSERT_EQ(*cred->db, tc.expectedDb)
|
||||
<< "Wrong authSource for URI: " << tc.uri << " (test: " << tc.description << ")";
|
||||
}
|
||||
}
|
||||
|
||||
// For non-external mechanisms at URI parse time:
|
||||
// 1. Database path is used when present.
|
||||
// 2. Explicit authSource overrides the database path.
|
||||
// 3. If both are absent, mechanism-specific default is used.
|
||||
TEST(MongoURI, NonExternalMechanismAuthSourceDefaults) {
|
||||
struct TestCase {
|
||||
std::string uri;
|
||||
std::string expectedDb;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
const TestCase cases[] = {
|
||||
// SCRAM-SHA-256 with database path
|
||||
{"mongodb://user:pwd@localhost/mydb?authMechanism=SCRAM-SHA-256",
|
||||
"mydb",
|
||||
"SCRAM-SHA-256 uses database path"},
|
||||
|
||||
// SCRAM-SHA-1 with database path
|
||||
{"mongodb://user:pwd@localhost/testdb?authMechanism=SCRAM-SHA-1",
|
||||
"testdb",
|
||||
"SCRAM-SHA-1 uses database path"},
|
||||
|
||||
// PLAIN with explicit authSource (PLAIN typically requires $external)
|
||||
{"mongodb://user:pwd@localhost/?authMechanism=PLAIN&authSource=$external",
|
||||
"$external",
|
||||
"PLAIN with explicit $external authSource"},
|
||||
|
||||
// PLAIN with database path defaults to that database.
|
||||
{"mongodb://user:pwd@localhost/mydb?authMechanism=PLAIN",
|
||||
"mydb",
|
||||
"PLAIN with database path defaults to that database"},
|
||||
|
||||
// SCRAM with no database path and no authSource defaults to admin.
|
||||
{"mongodb://user:pwd@localhost/?authMechanism=SCRAM-SHA-256",
|
||||
"admin",
|
||||
"SCRAM-SHA-256 without authSource or database path defaults to admin"},
|
||||
|
||||
// PLAIN with no database path and no authSource defaults to $external.
|
||||
{"mongodb://user:pwd@localhost/?authMechanism=PLAIN",
|
||||
"$external",
|
||||
"PLAIN without authSource or database path defaults to $external"},
|
||||
};
|
||||
|
||||
for (const auto& tc : cases) {
|
||||
auto rs = MongoURI::parse(tc.uri);
|
||||
ASSERT_OK(rs.getStatus()) << "Failed to parse URI: " << tc.uri
|
||||
<< " (test: " << tc.description << ")";
|
||||
const auto& cred = rs.getValue().getCredential();
|
||||
ASSERT_TRUE(cred.has_value())
|
||||
<< "No credential created for URI: " << tc.uri << " (test: " << tc.description << ")";
|
||||
ASSERT_TRUE(cred->db.has_value()) << "No authSource in credential for URI: " << tc.uri
|
||||
<< " (test: " << tc.description << ")";
|
||||
ASSERT_EQ(*cred->db, tc.expectedDb)
|
||||
<< "Wrong authSource for URI: " << tc.uri << " (test: " << tc.description << ")";
|
||||
}
|
||||
}
|
||||
|
||||
// Validate authSource defaulting in MongoURI::makeAuthObjFromOptions so auth command generation
|
||||
// matches parse-time defaults.
|
||||
TEST(MongoURI, MakeAuthObjAuthSourceDefaults) {
|
||||
struct TestCase {
|
||||
std::string uri;
|
||||
std::vector<std::string> saslMechsForAuth;
|
||||
std::string expectedDb;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
const TestCase cases[] = {
|
||||
{"mongodb://user:pwd@localhost/mydb?authSource=authdb&authMechanism=SCRAM-SHA-256",
|
||||
{},
|
||||
"authdb",
|
||||
"Explicit authSource overrides database path"},
|
||||
{"mongodb://localhost/admin?authMechanism=MONGODB-AWS",
|
||||
{},
|
||||
"$external",
|
||||
"External mechanism defaults to $external"},
|
||||
{"mongodb://user:pwd@localhost/mydb?authMechanism=PLAIN",
|
||||
{},
|
||||
"mydb",
|
||||
"PLAIN defaults to database path when present"},
|
||||
{"mongodb://user:pwd@localhost/?authMechanism=PLAIN",
|
||||
{},
|
||||
"$external",
|
||||
"PLAIN defaults to $external when database path is absent"},
|
||||
{"mongodb://user:pwd@localhost/mydb?authMechanism=SCRAM-SHA-256",
|
||||
{},
|
||||
"mydb",
|
||||
"SCRAM defaults to database path when present"},
|
||||
{"mongodb://user:pwd@localhost/?authMechanism=SCRAM-SHA-256",
|
||||
{},
|
||||
"admin",
|
||||
"SCRAM defaults to admin when database path is absent"},
|
||||
{"mongodb://user:pwd@localhost/",
|
||||
{std::string{auth::kMechanismScramSha1}},
|
||||
"admin",
|
||||
"Negotiated SCRAM-SHA-1 defaults to admin when database path is absent"},
|
||||
};
|
||||
|
||||
for (const auto& tc : cases) {
|
||||
auto rs = MongoURI::parse(tc.uri);
|
||||
ASSERT_OK(rs.getStatus()) << "Failed to parse URI: " << tc.uri
|
||||
<< " (test: " << tc.description << ")";
|
||||
|
||||
auto authObj =
|
||||
rs.getValue().makeAuthObjFromOptions(/*maxWireVersion*/ 0, tc.saslMechsForAuth);
|
||||
ASSERT_TRUE(authObj.has_value())
|
||||
<< "No auth object created for URI: " << tc.uri << " (test: " << tc.description << ")";
|
||||
|
||||
const auto sourceField = authObj->getField(saslCommandUserDBFieldName);
|
||||
ASSERT_FALSE(sourceField.eoo()) << "No auth db in auth object for URI: " << tc.uri
|
||||
<< " (test: " << tc.description << ")";
|
||||
ASSERT_EQ(sourceField.type(), BSONType::string)
|
||||
<< "Auth db has wrong type for URI: " << tc.uri << " (test: " << tc.description << ")";
|
||||
ASSERT_EQ(sourceField.String(), tc.expectedDb)
|
||||
<< "Wrong auth db in auth object for URI: " << tc.uri << " (test: " << tc.description
|
||||
<< ")";
|
||||
}
|
||||
}
|
||||
|
||||
// Test that explicit authSource can override the external mechanism default
|
||||
TEST(MongoURI, ExplicitAuthSourceOverridesExternalDefault) {
|
||||
struct TestCase {
|
||||
std::string uri;
|
||||
std::string expectedDb;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
const TestCase cases[] = {
|
||||
// X.509 with explicit non-$external authSource (unusual but should be allowed)
|
||||
{"mongodb://CN=test@localhost/?authMechanism=MONGODB-X509&authSource=admin",
|
||||
"admin",
|
||||
"X.509 with explicit admin authSource"},
|
||||
|
||||
// AWS with explicit authSource
|
||||
{"mongodb://localhost/?authMechanism=MONGODB-AWS&authSource=custom",
|
||||
"custom",
|
||||
"MONGODB-AWS with explicit custom authSource"},
|
||||
|
||||
// OIDC with explicit authSource
|
||||
{"mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=mydb",
|
||||
"mydb",
|
||||
"MONGODB-OIDC with explicit custom authSource"},
|
||||
|
||||
// GSSAPI with explicit authSource
|
||||
{"mongodb://user@localhost/?authMechanism=GSSAPI&authSource=test",
|
||||
"test",
|
||||
"GSSAPI with explicit test authSource"},
|
||||
};
|
||||
|
||||
for (const auto& tc : cases) {
|
||||
auto rs = MongoURI::parse(tc.uri);
|
||||
ASSERT_OK(rs.getStatus()) << "Failed to parse URI: " << tc.uri
|
||||
<< " (test: " << tc.description << ")";
|
||||
const auto& cred = rs.getValue().getCredential();
|
||||
ASSERT_TRUE(cred.has_value())
|
||||
<< "No credential created for URI: " << tc.uri << " (test: " << tc.description << ")";
|
||||
ASSERT_TRUE(cred->db.has_value()) << "No authSource in credential for URI: " << tc.uri
|
||||
<< " (test: " << tc.description << ")";
|
||||
ASSERT_EQ(*cred->db, tc.expectedDb)
|
||||
<< "Wrong authSource for URI: " << tc.uri << " (test: " << tc.description << ")";
|
||||
}
|
||||
}
|
||||
|
||||
// Test URIs without credentials don't create credentials
|
||||
TEST(MongoURI, NoCredentialsWithoutAuthInfo) {
|
||||
struct TestCase {
|
||||
std::string uri;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
const TestCase cases[] = {
|
||||
{"mongodb://localhost/", "No username, no database"},
|
||||
{"mongodb://localhost/mydb", "Database path but no username"},
|
||||
{"mongodb://localhost/?authSource=test", "authSource but no username"},
|
||||
};
|
||||
|
||||
for (const auto& tc : cases) {
|
||||
auto rs = MongoURI::parse(tc.uri);
|
||||
ASSERT_OK(rs.getStatus()) << "Failed to parse URI: " << tc.uri
|
||||
<< " (test: " << tc.description << ")";
|
||||
const auto& cred = rs.getValue().getCredential();
|
||||
ASSERT_FALSE(cred.has_value()) << "Unexpected credential created for URI: " << tc.uri
|
||||
<< " (test: " << tc.description << ")";
|
||||
}
|
||||
}
|
||||
|
||||
TEST(MongoURI, MakeAuthObjMechanismNegotiation) {
|
||||
struct TestCase {
|
||||
std::string uri;
|
||||
std::vector<std::string> saslMechsForAuth;
|
||||
std::string expectedMechanism;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
const std::string base = "mongodb://user:pwd@localhost/";
|
||||
const TestCase cases[] = {
|
||||
{.uri = base,
|
||||
.saslMechsForAuth = {std::string{auth::kMechanismScramSha256},
|
||||
std::string{auth::kMechanismScramSha1}},
|
||||
.expectedMechanism = std::string{auth::kMechanismScramSha256},
|
||||
.description = "SHA-256 preferred when both advertised"},
|
||||
{.uri = base,
|
||||
.saslMechsForAuth = {std::string{auth::kMechanismScramSha1}},
|
||||
.expectedMechanism = std::string{auth::kMechanismScramSha1},
|
||||
.description = "SHA-1 chosen when only SHA-1 advertised"},
|
||||
{.uri = base,
|
||||
.saslMechsForAuth = {std::string{auth::kMechanismScramSha256}},
|
||||
.expectedMechanism = std::string{auth::kMechanismScramSha256},
|
||||
.description = "SHA-256 chosen when only SHA-256 advertised"},
|
||||
{.uri = base,
|
||||
.saslMechsForAuth = {},
|
||||
.expectedMechanism = std::string{auth::kMechanismScramSha1},
|
||||
.description = "SHA-1 chosen when server advertises no mechanisms"},
|
||||
{.uri = base + "?authMechanism=SCRAM-SHA-256",
|
||||
.saslMechsForAuth = {std::string{auth::kMechanismScramSha1}},
|
||||
.expectedMechanism = std::string{auth::kMechanismScramSha256},
|
||||
.description = "Explicit URI mechanism overrides saslMechsForAuth"},
|
||||
};
|
||||
|
||||
for (const auto& tc : cases) {
|
||||
auto rs = MongoURI::parse(tc.uri);
|
||||
ASSERT_OK(rs.getStatus()) << tc.description;
|
||||
|
||||
auto authObj = rs.getValue().makeAuthObjFromOptions(0, tc.saslMechsForAuth);
|
||||
ASSERT_TRUE(authObj.has_value()) << tc.description;
|
||||
|
||||
const auto mechField = authObj->getField(saslCommandMechanismFieldName);
|
||||
ASSERT_FALSE(mechField.eoo()) << tc.description;
|
||||
ASSERT_EQ(mechField.String(), tc.expectedMechanism) << tc.description;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(MongoURI, EmptyAuthSourceRejectedByParser) {
|
||||
// The URI parser rejects authSource= (empty value); it does not silently ignore it.
|
||||
const std::string uri = "mongodb://user:pwd@localhost/?authSource=&authMechanism=SCRAM-SHA-256";
|
||||
auto rs = MongoURI::parse(uri);
|
||||
ASSERT_NOT_OK(rs.getStatus());
|
||||
ASSERT_EQ(rs.getStatus().code(), ErrorCodes::FailedToParse);
|
||||
}
|
||||
|
||||
TEST(MongoURI, MissingAuthSourceDefaultsToAdmin) {
|
||||
// SCRAM-SHA-256 with no authSource specified should default the auth database to "admin".
|
||||
const std::string uri = "mongodb://user:pwd@localhost/?authMechanism=SCRAM-SHA-256";
|
||||
auto rs = MongoURI::parse(uri);
|
||||
ASSERT_OK(rs.getStatus());
|
||||
|
||||
auto authObj = rs.getValue().makeAuthObjFromOptions(0, {});
|
||||
ASSERT_TRUE(authObj.has_value());
|
||||
|
||||
const auto sourceField = authObj->getField(saslCommandUserDBFieldName);
|
||||
ASSERT_FALSE(sourceField.eoo());
|
||||
ASSERT_EQ(sourceField.String(), DatabaseName::kAdmin.db(omitTenant));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mongo
|
||||
|
||||
@ -97,8 +97,8 @@ public:
|
||||
BSONObj generateSection(OperationContext* opCtx,
|
||||
const BSONElement& configElement) const override {
|
||||
BSONObjBuilder builder;
|
||||
cacheToBSON(scramsha1ClientCache, "SCRAM-SHA-1", &builder);
|
||||
cacheToBSON(scramsha256ClientCache, "SCRAM-SHA-256", &builder);
|
||||
cacheToBSON(scramsha1ClientCache, auth::kMechanismScramSha1, &builder);
|
||||
cacheToBSON(scramsha256ClientCache, auth::kMechanismScramSha256, &builder);
|
||||
return builder.obj();
|
||||
}
|
||||
};
|
||||
|
||||
@ -45,7 +45,7 @@ namespace mongo {
|
||||
|
||||
Future<void> (*saslClientAuthenticate)(auth::RunCommandHook runCommand,
|
||||
const HostAndPort& hostname,
|
||||
const BSONObj& saslParameters) = nullptr;
|
||||
const auth::Credential& credential) = nullptr;
|
||||
|
||||
Status saslExtractPayload(const BSONObj& cmdObj, std::string* payload, BSONType* type) {
|
||||
BSONElement payloadElement;
|
||||
|
||||
@ -60,23 +60,12 @@ class SaslClientSession;
|
||||
* client application must have successfully executed mongo::runGlobalInitializersOrDie() or its
|
||||
* ilk to make this functionality available.
|
||||
*
|
||||
* The "saslParameters" BSONObj should be initialized with zero or more of the
|
||||
* fields below. Which fields are required depends on the mechanism. Consult the
|
||||
* relevant IETF standards.
|
||||
*
|
||||
* "mechanism": The std::string name of the sasl mechanism to use. Mandatory.
|
||||
* "autoAuthorize": Truthy values tell the server to automatically acquire privileges on
|
||||
* all resources after successful authentication, which is the default. Falsey values
|
||||
* instruct the server to await separate privilege-acquisition commands.
|
||||
* "user": The std::string name of the user to authenticate.
|
||||
* "db": The database target of the auth command, which identifies the location
|
||||
* of the credential information for the user. May be "$external" if credential
|
||||
* information is stored outside of the mongo cluster.
|
||||
* "pwd": The password.
|
||||
* "serviceName": The GSSAPI service name to use. Defaults to "mongodb".
|
||||
* "serviceHostname": The GSSAPI hostname to use. Defaults to the name of the remote host.
|
||||
*
|
||||
* Other fields in saslParameters are silently ignored.
|
||||
* The "credential" struct must have "mechanism" set. Other fields are mechanism-dependent:
|
||||
* - "username": required for SCRAM, PLAIN, GSSAPI; omitted for X.509, AWS, OIDC.
|
||||
* - "db": auth-source database; uses mechanism default ($external or admin) when absent.
|
||||
* - "password": required for SCRAM and PLAIN; absent for other mechanisms.
|
||||
* - "mechanismProperties": mechanism-specific options such as serviceName, serviceHostname,
|
||||
* awsIamSessionToken, oidcAccessToken, digestPassword.
|
||||
*
|
||||
* Returns an OK status on success, and ErrorCodes::AuthenticationFailed if authentication is
|
||||
* rejected. Other failures, all of which are tantamount to authentication failure, may also be
|
||||
@ -84,7 +73,7 @@ class SaslClientSession;
|
||||
*/
|
||||
extern Future<void> (*saslClientAuthenticate)(auth::RunCommandHook runCommand,
|
||||
const HostAndPort& hostname,
|
||||
const BSONObj& saslParameters);
|
||||
const auth::Credential& credential);
|
||||
|
||||
/**
|
||||
* Extracts the payload field from "cmdObj", and store it into "*payload".
|
||||
@ -101,17 +90,15 @@ Status saslExtractPayload(const BSONObj& cmdObj, std::string* payload, BSONType*
|
||||
constexpr int kSaslClientLogLevelDefault = 4;
|
||||
|
||||
/**
|
||||
* Configures and initializes "session" to perform the client side of a
|
||||
* SASL conversation over connection "client".
|
||||
* Configures and initializes "session" to perform the client side of a SASL conversation.
|
||||
*
|
||||
* "saslParameters" is a BSON document providing the necessary configuration information.
|
||||
* Reads mechanism, username, password, and mechanism-specific properties from "credential".
|
||||
*
|
||||
* Returns Status::OK() on success.
|
||||
*/
|
||||
Status saslConfigureSession(SaslClientSession* session,
|
||||
const HostAndPort& hostname,
|
||||
StringData targetDatabase,
|
||||
const BSONObj& saslParameters);
|
||||
const auth::Credential& credential);
|
||||
|
||||
/**
|
||||
* Continue a previously started sasl session and proceed until completion.
|
||||
|
||||
@ -41,7 +41,6 @@
|
||||
#include "mongo/base/status.h"
|
||||
#include "mongo/base/string_data.h"
|
||||
#include "mongo/bson/bsonelement.h"
|
||||
#include "mongo/bson/bsonmisc.h"
|
||||
#include "mongo/bson/bsonobj.h"
|
||||
#include "mongo/bson/bsonobjbuilder.h"
|
||||
#include "mongo/bson/bsontypes.h"
|
||||
@ -65,6 +64,7 @@
|
||||
#include "mongo/util/future_impl.h"
|
||||
#include "mongo/util/net/hostandport.h"
|
||||
#include "mongo/util/password_digest.h"
|
||||
#include "mongo/util/str.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@ -83,9 +83,9 @@ namespace {
|
||||
|
||||
constexpr auto saslClientLogFieldName = "clientLogLevel"_sd;
|
||||
|
||||
int getSaslClientLogLevel(const BSONObj& saslParameters) {
|
||||
int getSaslClientLogLevel(const auth::Credential& credential) {
|
||||
int saslLogLevel = kSaslClientLogLevelDefault;
|
||||
BSONElement saslLogElement = saslParameters[saslClientLogFieldName];
|
||||
BSONElement saslLogElement = credential.mechanismProperties[saslClientLogFieldName];
|
||||
|
||||
if (saslLogElement.trueValue()) {
|
||||
saslLogLevel = 1;
|
||||
@ -98,100 +98,68 @@ int getSaslClientLogLevel(const BSONObj& saslParameters) {
|
||||
return saslLogLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the password data from "saslParameters" and stores it to "outPassword".
|
||||
*
|
||||
* If "digestPassword" indicates that the password needs to be "digested" via
|
||||
* mongo::createPasswordDigest(), this method takes care of that.
|
||||
* On success, the value of "*outPassword" is always the correct value to set
|
||||
* as the password on the SaslClientSession.
|
||||
*
|
||||
* Returns Status::OK() on success, and ErrorCodes::NoSuchKey if the password data is not
|
||||
* present in "saslParameters". Other ErrorCodes returned indicate other errors.
|
||||
*/
|
||||
Status extractPassword(const BSONObj& saslParameters,
|
||||
bool digestPassword,
|
||||
std::string* outPassword) {
|
||||
std::string rawPassword;
|
||||
Status status =
|
||||
bsonExtractStringField(saslParameters, saslCommandPasswordFieldName, &rawPassword);
|
||||
if (!status.isOK())
|
||||
return status;
|
||||
|
||||
if (digestPassword) {
|
||||
std::string user;
|
||||
status = bsonExtractStringField(saslParameters, saslCommandUserFieldName, &user);
|
||||
if (!status.isOK())
|
||||
return status;
|
||||
|
||||
*outPassword = mongo::createPasswordDigest(user, rawPassword);
|
||||
} else {
|
||||
*outPassword = rawPassword;
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Status saslConfigureSession(SaslClientSession* session,
|
||||
const HostAndPort& hostname,
|
||||
StringData targetDatabase,
|
||||
const BSONObj& saslParameters) {
|
||||
const auth::Credential& credential) {
|
||||
// SERVER-59876 Ensure hostname is never empty. If it is empty, the client-side SCRAM cache will
|
||||
// not be used which creates performance problems.
|
||||
dassert(!hostname.empty());
|
||||
|
||||
std::string mechanism;
|
||||
Status status =
|
||||
bsonExtractStringField(saslParameters, saslCommandMechanismFieldName, &mechanism);
|
||||
if (!status.isOK())
|
||||
return status;
|
||||
session->setParameter(SaslClientSession::parameterMechanism, mechanism);
|
||||
const auto mechStr = toString(credential.mechanism);
|
||||
session->setParameter(SaslClientSession::parameterMechanism, mechStr);
|
||||
|
||||
const auto& props = credential.mechanismProperties;
|
||||
std::string value;
|
||||
status = bsonExtractStringFieldWithDefault(
|
||||
saslParameters, saslCommandServiceNameFieldName, saslDefaultServiceName, &value);
|
||||
Status status = bsonExtractStringFieldWithDefault(
|
||||
props, saslCommandServiceNameFieldName, saslDefaultServiceName, &value);
|
||||
if (!status.isOK())
|
||||
return status;
|
||||
session->setParameter(SaslClientSession::parameterServiceName, value);
|
||||
|
||||
status = bsonExtractStringFieldWithDefault(
|
||||
saslParameters, saslCommandServiceHostnameFieldName, hostname.host(), &value);
|
||||
props, saslCommandServiceHostnameFieldName, hostname.host(), &value);
|
||||
if (!status.isOK())
|
||||
return status;
|
||||
session->setParameter(SaslClientSession::parameterServiceHostname, value);
|
||||
session->setParameter(SaslClientSession::parameterServiceHostAndPort, hostname.toString());
|
||||
|
||||
status = bsonExtractStringField(saslParameters, saslCommandUserFieldName, &value);
|
||||
if (status.isOK()) {
|
||||
session->setParameter(SaslClientSession::parameterUser, value);
|
||||
const auto targetDatabase = credential.db.value_or(std::string{saslDefaultDBName});
|
||||
if (credential.username) {
|
||||
session->setParameter(SaslClientSession::parameterUser, *credential.username);
|
||||
} else if ((targetDatabase != DatabaseName::kExternal.db(omitTenant)) ||
|
||||
((mechanism != auth::kMechanismMongoAWS) &&
|
||||
(mechanism != auth::kMechanismMongoOIDC))) {
|
||||
return status;
|
||||
(credential.mechanism != auth::AuthMechanism::kMongoAWS &&
|
||||
credential.mechanism != auth::AuthMechanism::kMongoOIDC)) {
|
||||
return Status(ErrorCodes::AuthenticationFailed,
|
||||
str::stream() << "Username required for mechanism " << mechStr);
|
||||
}
|
||||
|
||||
const bool digestPasswordDefault = (mechanism == auth::kMechanismScramSha1);
|
||||
bool digestPassword;
|
||||
status = bsonExtractBooleanFieldWithDefault(
|
||||
saslParameters, saslCommandDigestPasswordFieldName, digestPasswordDefault, &digestPassword);
|
||||
if (!status.isOK())
|
||||
return status;
|
||||
if (credential.password) {
|
||||
const bool digestPasswordDefault =
|
||||
(credential.mechanism == auth::AuthMechanism::kScramSha1);
|
||||
bool digestPassword = digestPasswordDefault;
|
||||
status = bsonExtractBooleanFieldWithDefault(
|
||||
props, saslCommandDigestPasswordFieldName, digestPasswordDefault, &digestPassword);
|
||||
if (!status.isOK())
|
||||
return status;
|
||||
|
||||
status = extractPassword(saslParameters, digestPassword, &value);
|
||||
if (status.isOK()) {
|
||||
session->setParameter(SaslClientSession::parameterPassword, value);
|
||||
} else if (!(status == ErrorCodes::NoSuchKey &&
|
||||
targetDatabase == DatabaseName::kExternal.db(omitTenant))) {
|
||||
// $external users do not have passwords, hence NoSuchKey is expected
|
||||
return status;
|
||||
std::string processedPassword = *credential.password;
|
||||
if (digestPassword && credential.username) {
|
||||
processedPassword =
|
||||
mongo::createPasswordDigest(*credential.username, *credential.password);
|
||||
}
|
||||
session->setParameter(SaslClientSession::parameterPassword, processedPassword);
|
||||
} else if (targetDatabase != DatabaseName::kExternal.db(omitTenant)) {
|
||||
return Status(ErrorCodes::AuthenticationFailed, "Password required");
|
||||
}
|
||||
|
||||
status = bsonExtractStringField(saslParameters, saslCommandIamSessionToken, &value);
|
||||
status = bsonExtractStringField(props, saslCommandIamSessionToken, &value);
|
||||
if (status.isOK()) {
|
||||
session->setParameter(SaslClientSession::parameterAWSSessionToken, value);
|
||||
}
|
||||
|
||||
status = bsonExtractStringField(saslParameters, saslCommandOIDCAccessToken, &value);
|
||||
status = bsonExtractStringField(props, saslCommandOIDCAccessToken, &value);
|
||||
if (status.isOK()) {
|
||||
session->setParameter(SaslClientSession::parameterOIDCAccessToken, value);
|
||||
}
|
||||
@ -304,36 +272,21 @@ namespace {
|
||||
*/
|
||||
Future<void> saslClientAuthenticateImpl(auth::RunCommandHook runCommand,
|
||||
const HostAndPort& hostname,
|
||||
const BSONObj& saslParameters) {
|
||||
int saslLogLevel = getSaslClientLogLevel(saslParameters);
|
||||
std::string targetDatabase;
|
||||
try {
|
||||
Status status = bsonExtractStringFieldWithDefault(
|
||||
saslParameters, saslCommandUserDBFieldName, saslDefaultDBName, &targetDatabase);
|
||||
if (!status.isOK())
|
||||
return status;
|
||||
} catch (const DBException& ex) {
|
||||
return ex.toStatus();
|
||||
}
|
||||
const auth::Credential& credential) {
|
||||
if (credential.mechanism == auth::AuthMechanism::kMongoDbCr)
|
||||
return Status{ErrorCodes::AuthenticationFailed,
|
||||
"MONGODB-CR is deprecated and no longer supported. Use SCRAM for "
|
||||
"password-based authentication instead."};
|
||||
|
||||
std::string username;
|
||||
Status status = bsonExtractStringFieldWithDefault(
|
||||
saslParameters, saslCommandUserFieldName, ""_sd, &username);
|
||||
if (!status.isOK()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
std::string mechanism;
|
||||
status = bsonExtractStringField(saslParameters, saslCommandMechanismFieldName, &mechanism);
|
||||
if (!status.isOK()) {
|
||||
return status;
|
||||
}
|
||||
int saslLogLevel = getSaslClientLogLevel(credential);
|
||||
const auto targetDatabase = credential.db.value_or(std::string{saslDefaultDBName});
|
||||
const auto mechStr = toString(credential.mechanism);
|
||||
|
||||
// NOTE: this must be a shared_ptr so that we can capture it in a lambda later on.
|
||||
// Come C++14, we should be able to do this in a nicer way.
|
||||
std::shared_ptr<SaslClientSession> session(SaslClientSession::create(mechanism));
|
||||
std::shared_ptr<SaslClientSession> session(SaslClientSession::create(std::string{mechStr}));
|
||||
|
||||
status = saslConfigureSession(session.get(), hostname, targetDatabase, saslParameters);
|
||||
auto status = saslConfigureSession(session.get(), hostname, credential);
|
||||
if (!status.isOK())
|
||||
return status;
|
||||
|
||||
@ -344,11 +297,11 @@ Future<void> saslClientAuthenticateImpl(auth::RunCommandHook runCommand,
|
||||
|
||||
BSONObj inputObj = BSON(saslCommandPayloadFieldName << "");
|
||||
|
||||
auto mechCounter = authCounter.getEgressMechanismCounter(mechanism);
|
||||
auto mechCounter = authCounter.getEgressMechanismCounter(mechStr);
|
||||
mechCounter.incAuthenticateSent();
|
||||
|
||||
auto argsBlock =
|
||||
std::make_tuple(hostname, saslParameters, username, targetDatabase, mechanism, mechCounter);
|
||||
const auto username = credential.username.value_or("");
|
||||
auto argsBlock = std::make_tuple(hostname, username, targetDatabase, mechStr, mechCounter);
|
||||
auto sharedBlock = std::make_shared<decltype(argsBlock)>(std::move(argsBlock));
|
||||
|
||||
session->metrics()->restart();
|
||||
@ -357,13 +310,11 @@ Future<void> saslClientAuthenticateImpl(auth::RunCommandHook runCommand,
|
||||
runCommand, session, saslFirstCommandPrefix, inputObj, targetDatabase, saslLogLevel)
|
||||
.onError([session, sharedBlock](Status status) {
|
||||
BSONObj metrics = session->metrics()->captureEgress();
|
||||
auto [hostname, saslParameters, username, targetDatabase, mechanism, _] =
|
||||
*sharedBlock.get();
|
||||
auto [hostname, username, targetDatabase, mechanism, _] = *sharedBlock.get();
|
||||
if (gEnableDetailedConnectionHealthMetricLogLines.load()) {
|
||||
LOGV2(10748700,
|
||||
"Authentication to remote host failed using SASL",
|
||||
"hostname"_attr = hostname,
|
||||
"saslParameters"_attr = saslParameters,
|
||||
"username"_attr = username,
|
||||
"targetDatabase"_attr = targetDatabase,
|
||||
"mechanism"_attr = mechanism,
|
||||
@ -375,14 +326,12 @@ Future<void> saslClientAuthenticateImpl(auth::RunCommandHook runCommand,
|
||||
})
|
||||
.then([session, sharedBlock]() {
|
||||
BSONObj metrics = session->metrics()->captureEgress();
|
||||
auto [hostname, saslParameters, username, targetDatabase, mechanism, mechCounter] =
|
||||
*sharedBlock.get();
|
||||
auto [hostname, username, targetDatabase, mechanism, mechCounter] = *sharedBlock.get();
|
||||
mechCounter.incEgressAuthenticateSuccessful();
|
||||
if (gEnableDetailedConnectionHealthMetricLogLines.load()) {
|
||||
LOGV2(10748701,
|
||||
"Authentication to remote host succeeded using SASL",
|
||||
"hostname"_attr = hostname,
|
||||
"saslParameters"_attr = saslParameters,
|
||||
"username"_attr = username,
|
||||
"targetDatabase"_attr = targetDatabase,
|
||||
"mechanism"_attr = mechanism,
|
||||
@ -390,7 +339,6 @@ Future<void> saslClientAuthenticateImpl(auth::RunCommandHook runCommand,
|
||||
"metrics"_attr = metrics);
|
||||
}
|
||||
});
|
||||
;
|
||||
}
|
||||
|
||||
MONGO_INITIALIZER(SaslClientAuthenticateFunction)(InitializerContext* context) {
|
||||
|
||||
88
src/mongo/client/sasl_client_authenticate_impl_test.cpp
Normal file
88
src/mongo/client/sasl_client_authenticate_impl_test.cpp
Normal file
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (C) 2026-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/error_codes.h"
|
||||
#include "mongo/bson/bsonmisc.h"
|
||||
#include "mongo/bson/bsonobj.h"
|
||||
#include "mongo/client/credential.h"
|
||||
#include "mongo/client/sasl_client_authenticate.h"
|
||||
#include "mongo/client/sasl_client_session.h"
|
||||
#include "mongo/db/auth/auth_mechanism.h"
|
||||
#include "mongo/unittest/unittest.h"
|
||||
#include "mongo/util/net/hostandport.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mongo {
|
||||
namespace {
|
||||
|
||||
class StubSaslClientSession : public SaslClientSession {
|
||||
public:
|
||||
Status initialize() override {
|
||||
return Status::OK();
|
||||
}
|
||||
Status step(StringData, std::string*) override {
|
||||
return Status::OK();
|
||||
}
|
||||
bool isSuccess() const override {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const HostAndPort kHost{"localhost", 27017};
|
||||
|
||||
TEST(SaslConfigureSessionTest, DigestPasswordWrongTypeReturnsError) {
|
||||
StubSaslClientSession session;
|
||||
auth::Credential cred;
|
||||
cred.mechanism = auth::AuthMechanism::kScramSha1;
|
||||
cred.username = std::string{"alice"};
|
||||
cred.password = std::string{"secret"};
|
||||
cred.mechanismProperties = BSON("digestPassword" << "notabool");
|
||||
|
||||
auto status = saslConfigureSession(&session, kHost, cred);
|
||||
|
||||
ASSERT_NOT_OK(status);
|
||||
ASSERT_EQ(status.code(), ErrorCodes::TypeMismatch);
|
||||
}
|
||||
|
||||
TEST(SaslConfigureSessionTest, MissingPasswordForNonExternalUserReturnsAuthenticationFailed) {
|
||||
StubSaslClientSession session;
|
||||
auth::Credential cred;
|
||||
cred.mechanism = auth::AuthMechanism::kScramSha256;
|
||||
cred.username = std::string{"alice"};
|
||||
cred.db = std::string{"admin"};
|
||||
|
||||
auto status = saslConfigureSession(&session, kHost, cred);
|
||||
|
||||
ASSERT_NOT_OK(status);
|
||||
ASSERT_EQ(status.code(), ErrorCodes::AuthenticationFailed);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mongo
|
||||
@ -288,6 +288,19 @@ idl_generator(
|
||||
],
|
||||
)
|
||||
|
||||
mongo_cc_library(
|
||||
name = "auth_mechanism",
|
||||
srcs = [
|
||||
"auth_mechanism.cpp",
|
||||
],
|
||||
hdrs = [
|
||||
"auth_mechanism.h",
|
||||
],
|
||||
deps = [
|
||||
"//src/mongo:base",
|
||||
],
|
||||
)
|
||||
|
||||
mongo_cc_library(
|
||||
name = "auth_options",
|
||||
srcs = [
|
||||
@ -350,6 +363,7 @@ mongo_cc_library(
|
||||
"sasl_options.cpp",
|
||||
],
|
||||
deps = [
|
||||
":auth_mechanism",
|
||||
"//src/mongo/db:server_base",
|
||||
"//src/mongo/db/exec/document_value",
|
||||
"//src/mongo/db/stats:counters",
|
||||
@ -552,6 +566,7 @@ mongo_cc_library(
|
||||
deps = [
|
||||
"auth",
|
||||
"authprivilege",
|
||||
":auth_mechanism",
|
||||
"//src/mongo/crypto:sha_block",
|
||||
"//src/mongo/db:server_base",
|
||||
],
|
||||
|
||||
96
src/mongo/db/auth/auth_mechanism.cpp
Normal file
96
src/mongo/db/auth/auth_mechanism.cpp
Normal file
@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "mongo/db/auth/auth_mechanism.h"
|
||||
|
||||
#include "mongo/base/error_codes.h"
|
||||
#include "mongo/util/str.h"
|
||||
|
||||
namespace mongo {
|
||||
namespace auth {
|
||||
|
||||
StringData toString(AuthMechanism mechanism) {
|
||||
switch (mechanism) {
|
||||
case AuthMechanism::kMongoX509:
|
||||
return kMechanismMongoX509;
|
||||
case AuthMechanism::kSaslPlain:
|
||||
return kMechanismSaslPlain;
|
||||
case AuthMechanism::kGSSAPI:
|
||||
return kMechanismGSSAPI;
|
||||
case AuthMechanism::kScramSha1:
|
||||
return kMechanismScramSha1;
|
||||
case AuthMechanism::kScramSha256:
|
||||
return kMechanismScramSha256;
|
||||
case AuthMechanism::kMongoAWS:
|
||||
return kMechanismMongoAWS;
|
||||
case AuthMechanism::kMongoOIDC:
|
||||
return kMechanismMongoOIDC;
|
||||
case AuthMechanism::kMongoDbCr:
|
||||
return kMechanismMongoDbCr;
|
||||
}
|
||||
MONGO_UNREACHABLE;
|
||||
}
|
||||
|
||||
StatusWith<AuthMechanism> authMechanismFromString(StringData s) {
|
||||
if (s == kMechanismMongoX509)
|
||||
return AuthMechanism::kMongoX509;
|
||||
if (s == kMechanismSaslPlain)
|
||||
return AuthMechanism::kSaslPlain;
|
||||
if (s == kMechanismGSSAPI)
|
||||
return AuthMechanism::kGSSAPI;
|
||||
if (s == kMechanismScramSha1)
|
||||
return AuthMechanism::kScramSha1;
|
||||
if (s == kMechanismScramSha256)
|
||||
return AuthMechanism::kScramSha256;
|
||||
if (s == kMechanismMongoAWS)
|
||||
return AuthMechanism::kMongoAWS;
|
||||
if (s == kMechanismMongoOIDC)
|
||||
return AuthMechanism::kMongoOIDC;
|
||||
if (s == kMechanismMongoDbCr)
|
||||
return AuthMechanism::kMongoDbCr;
|
||||
return Status{ErrorCodes::InvalidOptions,
|
||||
str::stream() << "Unsupported authentication mechanism: " << s};
|
||||
}
|
||||
|
||||
Status validateAuthMechanism(StringData value) {
|
||||
auto sw = authMechanismFromString(value);
|
||||
if (!sw.isOK())
|
||||
return {ErrorCodes::BadValue,
|
||||
str::stream() << "Unknown authentication mechanism '" << value
|
||||
<< "'. Supported mechanisms: MONGODB-X509, PLAIN, GSSAPI, "
|
||||
"SCRAM-SHA-1, SCRAM-SHA-256, MONGODB-AWS, MONGODB-OIDC"};
|
||||
if (sw.getValue() == AuthMechanism::kMongoDbCr)
|
||||
return Status{ErrorCodes::BadValue,
|
||||
"MONGODB-CR is deprecated and no longer supported. Use SCRAM for "
|
||||
"password-based authentication instead."};
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
} // namespace auth
|
||||
} // namespace mongo
|
||||
72
src/mongo/db/auth/auth_mechanism.h
Normal file
72
src/mongo/db/auth/auth_mechanism.h
Normal file
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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/base/status_with.h"
|
||||
#include "mongo/base/string_data.h"
|
||||
#include "mongo/util/modules.h"
|
||||
|
||||
namespace mongo {
|
||||
namespace MONGO_MOD_PUBLIC auth {
|
||||
|
||||
/** Wire-protocol names for supported authentication mechanisms. */
|
||||
constexpr auto kMechanismMongoX509 = "MONGODB-X509"_sd;
|
||||
constexpr auto kMechanismSaslPlain = "PLAIN"_sd;
|
||||
constexpr auto kMechanismGSSAPI = "GSSAPI"_sd;
|
||||
constexpr auto kMechanismScramSha1 = "SCRAM-SHA-1"_sd;
|
||||
constexpr auto kMechanismScramSha256 = "SCRAM-SHA-256"_sd;
|
||||
constexpr auto kMechanismMongoAWS = "MONGODB-AWS"_sd;
|
||||
constexpr auto kMechanismMongoOIDC = "MONGODB-OIDC"_sd;
|
||||
constexpr auto kMechanismMongoDbCr = "MONGODB-CR"_sd;
|
||||
constexpr auto kInternalAuthFallbackMechanism = kMechanismScramSha1;
|
||||
|
||||
/** Typed enum of supported authentication mechanisms. */
|
||||
enum class AuthMechanism {
|
||||
kMongoX509,
|
||||
kSaslPlain,
|
||||
kGSSAPI,
|
||||
kScramSha1,
|
||||
kScramSha256,
|
||||
kMongoAWS,
|
||||
kMongoOIDC,
|
||||
kMongoDbCr, // deprecated; recognized at parse time, rejected at auth time
|
||||
};
|
||||
|
||||
/** Return the wire-protocol string for a mechanism (e.g. "SCRAM-SHA-256"). */
|
||||
StringData toString(AuthMechanism mechanism);
|
||||
|
||||
/** Parse a wire-protocol string into an AuthMechanism, or return InvalidOptions. */
|
||||
StatusWith<AuthMechanism> authMechanismFromString(StringData s);
|
||||
|
||||
/** Validate that a string names a supported mechanism; returns BadValue on failure. */
|
||||
Status validateAuthMechanism(StringData value);
|
||||
|
||||
} // namespace MONGO_MOD_PUBLIC auth
|
||||
} // namespace mongo
|
||||
@ -103,7 +103,7 @@ Service::ConstructorActionRegisterer createAuthorizationManager(
|
||||
if (clusterAuthMode.sendsX509()) {
|
||||
// Send x509 authentication if we can.
|
||||
#ifdef MONGO_CONFIG_SSL
|
||||
auth::setInternalUserAuthParams(auth::createInternalX509AuthDocument(
|
||||
auth::setInternalUserAuthParams(auth::createInternalX509AuthCredential(
|
||||
boost::optional<StringData>{SSLManagerCoordinator::get()
|
||||
->getSSLManager()
|
||||
->getSSLConfiguration()
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
|
||||
#include "mongo/base/string_data.h"
|
||||
#include "mongo/crypto/hash_block.h"
|
||||
#include "mongo/db/auth/auth_mechanism.h"
|
||||
#include "mongo/db/auth/sasl_mechanism_registry.h"
|
||||
#include "mongo/util/modules.h"
|
||||
|
||||
@ -38,7 +39,7 @@ namespace mongo {
|
||||
|
||||
struct AWSIAMPolicy {
|
||||
static constexpr StringData getName() {
|
||||
return "MONGODB-AWS"_sd;
|
||||
return auth::kMechanismMongoAWS;
|
||||
}
|
||||
static SecurityPropertySet getProperties() {
|
||||
return SecurityPropertySet{SecurityProperty::kNoPlainText};
|
||||
@ -53,7 +54,7 @@ struct AWSIAMPolicy {
|
||||
|
||||
struct PLAINPolicy {
|
||||
static constexpr StringData getName() {
|
||||
return "PLAIN"_sd;
|
||||
return auth::kMechanismSaslPlain;
|
||||
}
|
||||
static SecurityPropertySet getProperties() {
|
||||
return SecurityPropertySet{};
|
||||
@ -70,7 +71,7 @@ struct SCRAMSHA1Policy {
|
||||
using HashBlock = SHA1Block;
|
||||
|
||||
static constexpr StringData getName() {
|
||||
return "SCRAM-SHA-1"_sd;
|
||||
return auth::kMechanismScramSha1;
|
||||
}
|
||||
static SecurityPropertySet getProperties() {
|
||||
return SecurityPropertySet{SecurityProperty::kNoPlainText, SecurityProperty::kMutualAuth};
|
||||
@ -87,7 +88,7 @@ struct SCRAMSHA256Policy {
|
||||
using HashBlock = SHA256Block;
|
||||
|
||||
static constexpr StringData getName() {
|
||||
return "SCRAM-SHA-256"_sd;
|
||||
return auth::kMechanismScramSha256;
|
||||
}
|
||||
static SecurityPropertySet getProperties() {
|
||||
return SecurityPropertySet{SecurityProperty::kNoPlainText, SecurityProperty::kMutualAuth};
|
||||
@ -102,7 +103,7 @@ struct SCRAMSHA256Policy {
|
||||
|
||||
struct GSSAPIPolicy {
|
||||
static constexpr StringData getName() {
|
||||
return "GSSAPI"_sd;
|
||||
return auth::kMechanismGSSAPI;
|
||||
}
|
||||
static SecurityPropertySet getProperties() {
|
||||
return SecurityPropertySet{SecurityProperty::kNoPlainText, SecurityProperty::kMutualAuth};
|
||||
@ -117,7 +118,7 @@ struct GSSAPIPolicy {
|
||||
|
||||
struct X509Policy {
|
||||
static constexpr StringData getName() {
|
||||
return "MONGODB-X509"_sd;
|
||||
return auth::kMechanismMongoX509;
|
||||
}
|
||||
|
||||
static SecurityPropertySet getProperties() {
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
|
||||
#include "mongo/base/init.h" // IWYU pragma: keep
|
||||
#include "mongo/base/initializer.h"
|
||||
#include "mongo/db/auth/auth_mechanism.h"
|
||||
#include "mongo/db/auth/sasl_options_gen.h"
|
||||
#include "mongo/db/stats/counters.h"
|
||||
#include "mongo/util/text.h" // IWYU pragma: keep
|
||||
@ -38,7 +39,9 @@
|
||||
namespace mongo {
|
||||
|
||||
const std::vector<std::string> SASLGlobalParams::kDefaultAuthenticationMechanisms =
|
||||
std::vector<std::string>{"MONGODB-X509", "SCRAM-SHA-1", "SCRAM-SHA-256"};
|
||||
std::vector<std::string>{std::string{auth::kMechanismMongoX509},
|
||||
std::string{auth::kMechanismScramSha1},
|
||||
std::string{auth::kMechanismScramSha256}};
|
||||
SASLGlobalParams saslGlobalParams;
|
||||
|
||||
SASLGlobalParams::SASLGlobalParams() {
|
||||
|
||||
@ -35,6 +35,7 @@
|
||||
#include "mongo/crypto/sha1_block.h"
|
||||
#include "mongo/crypto/sha256_block.h"
|
||||
#include "mongo/db/auth/action_set.h"
|
||||
#include "mongo/db/auth/auth_mechanism.h"
|
||||
#include "mongo/db/auth/privilege.h"
|
||||
#include "mongo/db/auth/resource_pattern.h"
|
||||
#include "mongo/db/auth/restriction_set.h"
|
||||
@ -241,8 +242,8 @@ class User {
|
||||
|
||||
public:
|
||||
using UserId = std::vector<std::uint8_t>;
|
||||
constexpr static auto kSHA1FieldName = "SCRAM-SHA-1"_sd;
|
||||
constexpr static auto kSHA256FieldName = "SCRAM-SHA-256"_sd;
|
||||
constexpr static auto kSHA1FieldName = auth::kMechanismScramSha1;
|
||||
constexpr static auto kSHA256FieldName = auth::kMechanismScramSha256;
|
||||
constexpr static auto kExternalFieldName = "external"_sd;
|
||||
constexpr static auto kIterationCountFieldName = "iterationCount"_sd;
|
||||
constexpr static auto kSaltFieldName = "salt"_sd;
|
||||
|
||||
@ -74,8 +74,7 @@ constexpr StringData READONLY_FIELD_NAME = "readOnly"_sd;
|
||||
constexpr StringData CREDENTIALS_FIELD_NAME = "credentials"_sd;
|
||||
constexpr StringData ROLE_NAME_FIELD_NAME = "role"_sd;
|
||||
constexpr StringData ROLE_DB_FIELD_NAME = "db"_sd;
|
||||
constexpr StringData SCRAMSHA1_CREDENTIAL_FIELD_NAME = "SCRAM-SHA-1"_sd;
|
||||
constexpr StringData SCRAMSHA256_CREDENTIAL_FIELD_NAME = "SCRAM-SHA-256"_sd;
|
||||
|
||||
constexpr StringData MONGODB_EXTERNAL_CREDENTIAL_FIELD_NAME = "external"_sd;
|
||||
constexpr StringData AUTHENTICATION_RESTRICTIONS_FIELD_NAME = "authenticationRestrictions"_sd;
|
||||
constexpr StringData INHERITED_AUTHENTICATION_RESTRICTIONS_FIELD_NAME =
|
||||
@ -220,11 +219,11 @@ Status V2UserDocumentParser::checkValidUserDocument(const BSONObj& doc) const {
|
||||
return Status::OK();
|
||||
};
|
||||
|
||||
auto sha1status = validateScram(SCRAMSHA1_CREDENTIAL_FIELD_NAME);
|
||||
auto sha1status = validateScram(auth::kMechanismScramSha1);
|
||||
if (!sha1status.isOK() && (sha1status.code() != ErrorCodes::NoSuchKey)) {
|
||||
return sha1status;
|
||||
}
|
||||
auto sha256status = validateScram(SCRAMSHA256_CREDENTIAL_FIELD_NAME);
|
||||
auto sha256status = validateScram(auth::kMechanismScramSha256);
|
||||
if (!sha256status.isOK() && (sha256status.code() != ErrorCodes::NoSuchKey)) {
|
||||
return sha256status;
|
||||
}
|
||||
@ -278,9 +277,9 @@ Status V2UserDocumentParser::initializeUserCredentialsFromUserDocument(
|
||||
}
|
||||
} else {
|
||||
const bool haveSha1 = parseSCRAMCredentials(
|
||||
credentialsElement, credentials.scram_sha1, SCRAMSHA1_CREDENTIAL_FIELD_NAME);
|
||||
credentialsElement, credentials.scram_sha1, auth::kMechanismScramSha1);
|
||||
const bool haveSha256 = parseSCRAMCredentials(
|
||||
credentialsElement, credentials.scram_sha256, SCRAMSHA256_CREDENTIAL_FIELD_NAME);
|
||||
credentialsElement, credentials.scram_sha256, auth::kMechanismScramSha256);
|
||||
|
||||
if (!haveSha1 && !haveSha256) {
|
||||
return Status(
|
||||
|
||||
@ -432,6 +432,7 @@ mongo_cc_library(
|
||||
"//src/mongo/db:common",
|
||||
"//src/mongo/db:server_base",
|
||||
"//src/mongo/db/auth:address_restriction",
|
||||
"//src/mongo/db/auth:auth_mechanism",
|
||||
"//src/mongo/db/auth:authprivilege",
|
||||
"//src/mongo/db/auth:role_name_or_string",
|
||||
],
|
||||
|
||||
@ -87,7 +87,6 @@ namespace {
|
||||
|
||||
constexpr auto kDBFieldName = "db"_sd;
|
||||
constexpr auto kSASLPayloadUsernameField = "username"_sd;
|
||||
constexpr StringData kX509AuthMechanism = "MONGODB-X509"_sd;
|
||||
|
||||
class CmdLogout : public TypedCommand<CmdLogout> {
|
||||
public:
|
||||
@ -251,7 +250,7 @@ void _authenticateX509(OperationContext* opCtx, AuthenticationSession* session)
|
||||
const auto& v = saslGlobalParams.authenticationMechanisms;
|
||||
uassert(ErrorCodes::BadValue,
|
||||
kX509AuthenticationDisabledMessage,
|
||||
std::find(v.begin(), v.end(), kX509AuthMechanism) != v.end());
|
||||
std::find(v.begin(), v.end(), auth::kMechanismMongoX509) != v.end());
|
||||
|
||||
uassertStatusOK(
|
||||
authorizationSession->addAndAuthorizeUser(opCtx, std::move(request), boost::none));
|
||||
|
||||
@ -48,6 +48,7 @@
|
||||
#include "mongo/db/audit.h"
|
||||
#include "mongo/db/auth/action_set.h"
|
||||
#include "mongo/db/auth/address_restriction.h"
|
||||
#include "mongo/db/auth/auth_mechanism.h"
|
||||
#include "mongo/db/auth/auth_name.h"
|
||||
#include "mongo/db/auth/auth_options_gen.h"
|
||||
#include "mongo/db/auth/authorization_backend_interface.h"
|
||||
@ -589,9 +590,9 @@ void buildCredentials(BSONObjBuilder* builder, const UserName& userName, const T
|
||||
bool buildSCRAMSHA1 = false, buildSCRAMSHA256 = false;
|
||||
if (auto mechanisms = cmd.getMechanisms(); mechanisms && !mechanisms->empty()) {
|
||||
for (const auto& mech : mechanisms.get()) {
|
||||
if (mech == "SCRAM-SHA-1") {
|
||||
if (mech == auth::kMechanismScramSha1) {
|
||||
buildSCRAMSHA1 = true;
|
||||
} else if (mech == "SCRAM-SHA-256") {
|
||||
} else if (mech == auth::kMechanismScramSha256) {
|
||||
buildSCRAMSHA256 = true;
|
||||
} else {
|
||||
uasserted(ErrorCodes::BadValue,
|
||||
@ -604,8 +605,8 @@ void buildCredentials(BSONObjBuilder* builder, const UserName& userName, const T
|
||||
}
|
||||
|
||||
} else {
|
||||
buildSCRAMSHA1 = haveAuthMechanism("SCRAM-SHA-1");
|
||||
buildSCRAMSHA256 = haveAuthMechanism("SCRAM-SHA-256");
|
||||
buildSCRAMSHA1 = haveAuthMechanism(auth::kMechanismScramSha1);
|
||||
buildSCRAMSHA256 = haveAuthMechanism(auth::kMechanismScramSha256);
|
||||
}
|
||||
|
||||
auto password = cmd.getPwd().get();
|
||||
@ -621,7 +622,7 @@ void buildCredentials(BSONObjBuilder* builder, const UserName& userName, const T
|
||||
}
|
||||
auto sha1Cred = scram::Secrets<SHA1Block>::generateCredentials(
|
||||
hashedPwd, saslGlobalParams.scramSHA1IterationCount.load());
|
||||
builder->append("SCRAM-SHA-1", sha1Cred);
|
||||
builder->append(auth::kMechanismScramSha1, sha1Cred);
|
||||
}
|
||||
|
||||
if (buildSCRAMSHA256) {
|
||||
@ -632,7 +633,7 @@ void buildCredentials(BSONObjBuilder* builder, const UserName& userName, const T
|
||||
auto prepPwd = uassertStatusOK(icuSaslPrep(password));
|
||||
auto sha256Cred = scram::Secrets<SHA256Block>::generateCredentials(
|
||||
prepPwd, saslGlobalParams.scramSHA256IterationCount.load());
|
||||
builder->append("SCRAM-SHA-256", sha256Cred);
|
||||
builder->append(auth::kMechanismScramSha256, sha256Cred);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1121,9 +1122,9 @@ void trimCredentials(OperationContext* opCtx,
|
||||
"mechanisms field must be a subset of previously set mechanisms",
|
||||
creds.hasField(mech));
|
||||
|
||||
if (mech == "SCRAM-SHA-1") {
|
||||
if (mech == auth::kMechanismScramSha1) {
|
||||
keepSCRAMSHA1 = true;
|
||||
} else if (mech == "SCRAM-SHA-256") {
|
||||
} else if (mech == auth::kMechanismScramSha256) {
|
||||
keepSCRAMSHA256 = true;
|
||||
}
|
||||
}
|
||||
@ -2155,8 +2156,8 @@ void _auditCreateOrUpdateUser(const BSONObj& userObj, bool create) {
|
||||
authenticationRestrictions = r.getValue();
|
||||
}
|
||||
|
||||
const bool hasPwd = userObj["credentials"].Obj().hasField("SCRAM-SHA-1") ||
|
||||
userObj["credentials"].Obj().hasField("SCRAM-SHA-256");
|
||||
const bool hasPwd = userObj["credentials"].Obj().hasField(auth::kMechanismScramSha1) ||
|
||||
userObj["credentials"].Obj().hasField(auth::kMechanismScramSha256);
|
||||
if (create) {
|
||||
audit::logCreateUser(Client::getCurrent(),
|
||||
userName,
|
||||
|
||||
@ -29,7 +29,7 @@ global:
|
||||
cpp_namespace: "mongo"
|
||||
mod_visibility: public
|
||||
cpp_includes:
|
||||
- "mongo/db/commands/user_management_commands_validation.h"
|
||||
- "mongo/db/auth/auth_mechanism.h"
|
||||
|
||||
imports:
|
||||
- "mongo/db/basic_types.idl"
|
||||
@ -373,7 +373,7 @@ commands:
|
||||
type: string
|
||||
optional: true
|
||||
validator:
|
||||
callback: validateAuthMechanism
|
||||
callback: auth::validateAuthMechanism
|
||||
filter:
|
||||
description: >-
|
||||
A document that specifies $match stage conditions to return information
|
||||
|
||||
@ -201,6 +201,7 @@ mongo_cc_library(
|
||||
":egress_connection_closer_manager",
|
||||
":network_interface",
|
||||
":network_interface_tl",
|
||||
"//src/mongo/client:authentication",
|
||||
] + select({
|
||||
"//bazel/config:build_grpc_enabled": ["//src/mongo/transport/grpc:grpc_async_client_factory"],
|
||||
"//conditions:default": [],
|
||||
@ -463,15 +464,18 @@ mongo_cc_unit_test(
|
||||
"async_rpc",
|
||||
"async_rpc_util",
|
||||
"connection_pool_executor",
|
||||
"connection_pool_tl",
|
||||
"egress_connection_closer_manager",
|
||||
"network_interface_mock",
|
||||
"network_interface_tl",
|
||||
"network_test_env",
|
||||
"pinned_connection_task_executor",
|
||||
"thread_pool_task_executor_test_fixture",
|
||||
"//src/mongo/client:authentication",
|
||||
"//src/mongo/client:remote_command_targeter",
|
||||
"//src/mongo/client:remote_command_targeter_mock",
|
||||
"//src/mongo/db:service_context_test_fixture",
|
||||
"//src/mongo/db/auth:auth_mechanism",
|
||||
"//src/mongo/db/commands:standalone",
|
||||
"//src/mongo/db/repl:task_executor_mock",
|
||||
"//src/mongo/db/repl/hello:hello_command",
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
#include "mongo/base/status.h"
|
||||
#include "mongo/base/status_with.h"
|
||||
#include "mongo/base/string_data.h"
|
||||
#include "mongo/client/authenticate.h"
|
||||
#include "mongo/config.h" // IWYU pragma: keep
|
||||
#include "mongo/executor/connection_pool_state.h"
|
||||
#include "mongo/executor/connection_pool_stats.h"
|
||||
@ -178,6 +179,12 @@ public:
|
||||
*/
|
||||
bool skipAuthentication = false;
|
||||
|
||||
/**
|
||||
* If set, new connections authenticate as this client credential immediately after setup,
|
||||
* before being returned to callers.
|
||||
*/
|
||||
boost::optional<auth::Credential> credential;
|
||||
|
||||
#ifdef MONGO_CONFIG_SSL
|
||||
/**
|
||||
* Provides SSL params if the egress cluster connection requires custom SSL certificates
|
||||
|
||||
@ -283,7 +283,7 @@ public:
|
||||
// X.509 auth only means we only want to use a single mechanism regards of what hello says
|
||||
if (_x509AuthOnly) {
|
||||
_saslMechsForInternalAuth.clear();
|
||||
_saslMechsForInternalAuth.push_back("MONGODB-X509");
|
||||
_saslMechsForInternalAuth.push_back(std::string{auth::kMechanismMongoX509});
|
||||
} else {
|
||||
const auto saslMechsElem = reply.getField("saslSupportedMechs");
|
||||
if (saslMechsElem.type() == BSONType::array) {
|
||||
@ -359,14 +359,14 @@ public:
|
||||
|
||||
~TransientInternalAuthParametersProvider() override = default;
|
||||
|
||||
BSONObj get(size_t index, StringData mechanism) final {
|
||||
boost::optional<auth::Credential> get(size_t index, StringData mechanism) final {
|
||||
if (_transientSSLContext) {
|
||||
if (index == 0) {
|
||||
return auth::createInternalX509AuthDocument(
|
||||
return auth::createInternalX509AuthCredential(
|
||||
boost::optional<StringData>{_transientSSLContext->manager->getSSLConfiguration()
|
||||
.clientSubjectName.toString()});
|
||||
} else {
|
||||
return BSONObj();
|
||||
return boost::none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -467,6 +467,10 @@ void TLConnection::setup(Milliseconds timeout, SetupCallback cb, std::string ins
|
||||
return Future<void>::makeReady();
|
||||
}
|
||||
|
||||
if (_credential) {
|
||||
return _client->authenticate(*_credential);
|
||||
}
|
||||
|
||||
boost::optional<std::string> mechanism;
|
||||
if (!helloHook->saslMechsForInternalAuth().empty())
|
||||
mechanism = helloHook->saslMechsForInternalAuth().front();
|
||||
@ -615,7 +619,8 @@ std::shared_ptr<ConnectionPool::ConnectionInterface> TLTypeFactory::makeConnecti
|
||||
generation,
|
||||
_onConnectHook.get(),
|
||||
_connPoolOptions.skipAuthentication,
|
||||
_transientSSLContext);
|
||||
_transientSSLContext,
|
||||
_connPoolOptions.credential);
|
||||
fasten(conn.get());
|
||||
return conn;
|
||||
}
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
|
||||
#include "mongo/base/string_data.h"
|
||||
#include "mongo/client/async_client.h"
|
||||
#include "mongo/client/authenticate.h"
|
||||
#include "mongo/db/service_context.h"
|
||||
#include "mongo/executor/connection_metrics.h"
|
||||
#include "mongo/executor/connection_pool.h"
|
||||
@ -185,7 +186,8 @@ public:
|
||||
size_t generation,
|
||||
NetworkConnectionHook* onConnectHook,
|
||||
bool skipAuth,
|
||||
std::shared_ptr<const transport::SSLConnectionContext> transientSSLContext = nullptr)
|
||||
std::shared_ptr<const transport::SSLConnectionContext> transientSSLContext = nullptr,
|
||||
boost::optional<auth::Credential> credential = boost::none)
|
||||
: ConnectionInterface(id, generation),
|
||||
TLTypeFactory::Type(factory),
|
||||
_reactor(reactor),
|
||||
@ -197,6 +199,7 @@ public:
|
||||
_sslMode(sslMode),
|
||||
_onConnectHook(onConnectHook),
|
||||
_transientSSLContext(transientSSLContext),
|
||||
_credential(std::move(credential)),
|
||||
_connMetrics(serviceContext->getFastClockSource()) {}
|
||||
|
||||
~TLConnection() override {
|
||||
@ -241,6 +244,7 @@ private:
|
||||
NetworkConnectionHook* const _onConnectHook;
|
||||
// SSL context to use intead of the default one for this pool.
|
||||
const std::shared_ptr<const transport::SSLConnectionContext> _transientSSLContext;
|
||||
boost::optional<auth::Credential> _credential;
|
||||
|
||||
// Guards assignment of the _client pointer.
|
||||
// Do not need to acquire this in contexts where the pointer is known to be valid.
|
||||
|
||||
@ -802,7 +802,7 @@ void MongoExternalInfo::construct(JSContext* cx, JS::CallArgs args) {
|
||||
|
||||
uassert(ErrorCodes::InvalidOptions,
|
||||
"Authentication is not currently supported when gRPC mode is enabled",
|
||||
cs.getUser().empty());
|
||||
!cs.getCredential() || !cs.getCredential()->username);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@ -101,8 +101,13 @@ void URIInfo::construct(JSContext* cx, JS::CallArgs args) {
|
||||
ObjectWrapper o(cx, thisv);
|
||||
|
||||
o.setValue(InternedString::uri, uriArg);
|
||||
o.setString(InternedString::user, parsed.getUser());
|
||||
o.setString(InternedString::password, parsed.getPassword());
|
||||
auto& cred = parsed.getCredential();
|
||||
if (cred && cred->username) {
|
||||
o.setString(InternedString::user, *cred->username);
|
||||
}
|
||||
if (cred && cred->password) {
|
||||
o.setString(InternedString::password, *cred->password);
|
||||
}
|
||||
o.setBSON(InternedString::options, optsBuilder.obj(), true);
|
||||
o.setString(InternedString::database, parsed.getDatabase());
|
||||
o.setBoolean(InternedString::isValid, parsed.isValid());
|
||||
|
||||
@ -745,11 +745,13 @@ int mongo_main(int argc, char* argv[]) {
|
||||
bool usingPassword = !shellGlobalParams.password.empty();
|
||||
|
||||
if (mechanismRequiresPassword(parsedURI) &&
|
||||
(parsedURI.getUser().size() || shellGlobalParams.username.size())) {
|
||||
((parsedURI.getCredential() && parsedURI.getCredential()->username) ||
|
||||
shellGlobalParams.username.size())) {
|
||||
usingPassword = true;
|
||||
}
|
||||
|
||||
if (usingPassword && parsedURI.getPassword().empty()) {
|
||||
if (usingPassword &&
|
||||
(!parsedURI.getCredential() || !parsedURI.getCredential()->password)) {
|
||||
if (!shellGlobalParams.password.empty()) {
|
||||
parsedURI.setPassword(std::as_const(shellGlobalParams.password));
|
||||
} else {
|
||||
@ -757,7 +759,8 @@ int mongo_main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedURI.getUser().empty() && !shellGlobalParams.username.empty()) {
|
||||
if ((!parsedURI.getCredential() || !parsedURI.getCredential()->username) &&
|
||||
!shellGlobalParams.username.empty()) {
|
||||
parsedURI.setUser(std::as_const(shellGlobalParams.username));
|
||||
}
|
||||
|
||||
|
||||
@ -283,11 +283,13 @@ Status storeMongoShellOptions(const moe::Environment& params,
|
||||
StringBuilder sb;
|
||||
sb << "ERROR: Cannot specify ";
|
||||
|
||||
if (!shellGlobalParams.username.empty() && !cs.getUser().empty() &&
|
||||
shellGlobalParams.username != cs.getUser()) {
|
||||
if (!shellGlobalParams.username.empty() && cs.getCredential() &&
|
||||
cs.getCredential()->username &&
|
||||
shellGlobalParams.username != *cs.getCredential()->username) {
|
||||
sb << "different usernames";
|
||||
} else if (!shellGlobalParams.password.empty() && !cs.getPassword().empty() &&
|
||||
shellGlobalParams.password != cs.getPassword()) {
|
||||
} else if (!shellGlobalParams.password.empty() && cs.getCredential() &&
|
||||
cs.getCredential()->password &&
|
||||
shellGlobalParams.password != *cs.getCredential()->password) {
|
||||
sb << "different passwords";
|
||||
} else if (!shellGlobalParams.authenticationMechanism.empty() &&
|
||||
uriOptions.count("authMechanism") &&
|
||||
|
||||
@ -76,7 +76,7 @@ public:
|
||||
|
||||
// Set the internal user auth parameters so we auth with X.509 externally
|
||||
auth::setInternalUserAuthParams(
|
||||
auth::createInternalX509AuthDocument(boost::optional<StringData>("Ignored")));
|
||||
auth::createInternalX509AuthCredential(boost::optional<StringData>("Ignored")));
|
||||
|
||||
createNet();
|
||||
net().startup();
|
||||
|
||||
@ -377,7 +377,7 @@ void SSLManagerCoordinator::rotate() {
|
||||
auto svcCtx = getGlobalServiceContext();
|
||||
const auto clusterAuthMode = ClusterAuthMode::get(svcCtx);
|
||||
if (clusterAuthMode.sendsX509()) {
|
||||
auth::setInternalUserAuthParams(auth::createInternalX509AuthDocument(
|
||||
auth::setInternalUserAuthParams(auth::createInternalX509AuthCredential(
|
||||
StringData(manager->getSSLConfiguration().clientSubjectName.toString())));
|
||||
}
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
#include "mongo/db/auth/cluster_auth_mode.h"
|
||||
#include "mongo/db/auth/sasl_command_constants.h"
|
||||
#include "mongo/db/server_options.h"
|
||||
#include "mongo/db/service_context.h"
|
||||
#include "mongo/util/net/ssl_options.h"
|
||||
#include "mongo/util/net/ssl_parameters_auth_gen.h"
|
||||
|
||||
@ -61,7 +62,7 @@ Status ClusterAuthModeServerParameter::setFromString(StringData strMode,
|
||||
// Set our ingress mode, then our egress parameters.
|
||||
ClusterAuthMode::set(getGlobalServiceContext(), mode);
|
||||
if (mode.sendsX509()) {
|
||||
auth::setInternalUserAuthParams(auth::createInternalX509AuthDocument());
|
||||
auth::setInternalUserAuthParams(auth::createInternalX509AuthCredential());
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user