/* Copyright 2012 10gen Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*
* 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 GNU Affero General 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.
*/
/**
* This module implements the client side of SASL authentication in MongoDB, in terms of the Cyrus
* SASL library. See and http://cyrusimap.web.cmu.edu/ for relevant documentation.
*
* The primary entry point at runtime is saslClientAuthenticateImpl().
*/
#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kNetwork
#include "mongo/platform/basic.h"
#include
#include
#include "mongo/base/init.h"
#include "mongo/base/status.h"
#include "mongo/base/string_data.h"
#include "mongo/bson/util/bson_extract.h"
#include "mongo/client/sasl_client_authenticate.h"
#include "mongo/client/sasl_client_session.h"
#include "mongo/rpc/get_status_from_command_result.h"
#include "mongo/util/base64.h"
#include "mongo/util/log.h"
#include "mongo/util/net/hostandport.h"
#include "mongo/util/password_digest.h"
namespace mongo {
using std::endl;
using executor::RemoteCommandRequest;
using executor::RemoteCommandResponse;
namespace {
// Default log level on the client for SASL log messages.
const int defaultSaslClientLogLevel = 4;
const char* const saslClientLogFieldName = "clientLogLevel";
int getSaslClientLogLevel(const BSONObj& saslParameters) {
int saslLogLevel = defaultSaslClientLogLevel;
BSONElement saslLogElement = saslParameters[saslClientLogFieldName];
if (saslLogElement.trueValue())
saslLogLevel = 1;
if (saslLogElement.isNumber())
saslLogLevel = saslLogElement.numberInt();
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();
}
/**
* Configures "session" to perform the client side of a SASL conversation over connection
* "client".
*
* "saslParameters" is a BSON document providing the necessary configuration information.
*
* Returns Status::OK() on success.
*/
Status configureSession(SaslClientSession* session,
const HostAndPort& hostname,
StringData targetDatabase,
const BSONObj& saslParameters) {
std::string mechanism;
Status status =
bsonExtractStringField(saslParameters, saslCommandMechanismFieldName, &mechanism);
if (!status.isOK())
return status;
session->setParameter(SaslClientSession::parameterMechanism, mechanism);
std::string value;
status = bsonExtractStringFieldWithDefault(
saslParameters, saslCommandServiceNameFieldName, saslDefaultServiceName, &value);
if (!status.isOK())
return status;
session->setParameter(SaslClientSession::parameterServiceName, value);
status = bsonExtractStringFieldWithDefault(
saslParameters, 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())
return status;
session->setParameter(SaslClientSession::parameterUser, value);
bool digestPasswordDefault = !(targetDatabase == "$external" && mechanism == "PLAIN") &&
!(targetDatabase == "$external" && mechanism == "GSSAPI");
bool digestPassword;
status = bsonExtractBooleanFieldWithDefault(
saslParameters, 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 == "$external")) {
// $external users do not have passwords, hence NoSuchKey is expected
return status;
}
return session->initialize();
}
void asyncSaslConversation(auth::RunCommandHook runCommand,
const std::shared_ptr& session,
const BSONObj& saslCommandPrefix,
const BSONObj& inputObj,
std::string targetDatabase,
int saslLogLevel,
auth::AuthCompletionHandler handler) {
// Extract payload from previous step
std::string payload;
BSONType type;
auto status = saslExtractPayload(inputObj, &payload, &type);
if (!status.isOK())
return handler(std::move(status));
LOG(saslLogLevel) << "sasl client input: " << base64::encode(payload) << endl;
// Create new payload for our response
std::string responsePayload;
status = session->step(payload, &responsePayload);
if (!status.isOK())
return handler(std::move(status));
LOG(saslLogLevel) << "sasl client output: " << base64::encode(responsePayload) << endl;
// Build command using our new payload and conversationId
BSONObjBuilder commandBuilder;
commandBuilder.appendElements(saslCommandPrefix);
commandBuilder.appendBinData(saslCommandPayloadFieldName,
int(responsePayload.size()),
BinDataGeneral,
responsePayload.c_str());
BSONElement conversationId = inputObj[saslCommandConversationIdFieldName];
if (!conversationId.eoo())
commandBuilder.append(conversationId);
auto request = RemoteCommandRequest();
request.dbname = targetDatabase;
request.cmdObj = commandBuilder.obj();
// Asynchronously continue the conversation
runCommand(
request,
[runCommand, session, targetDatabase, saslLogLevel, handler](auth::AuthResponse response) {
if (!response.isOK()) {
return handler(std::move(response));
}
auto serverResponse = response.data.getOwned();
auto code = getStatusFromCommandResult(serverResponse).code();
// Server versions 2.3.2 and earlier may return "ok: 1" with a non-zero
// "code" field, indicating a failure. Subsequent versions should
// return "ok: 0" on failure with a non-zero "code" field to indicate specific
// failure. In all versions, either (ok: 1, code: > 0) or (ok: 0, code optional)
// indicate failure.
if (code != ErrorCodes::OK) {
return handler({code, serverResponse[saslCommandErrmsgFieldName].str()});
}
// Exit if we have finished
if (session->isDone()) {
bool isServerDone = serverResponse[saslCommandDoneFieldName].trueValue();
if (!isServerDone) {
return handler({ErrorCodes::ProtocolError, "Client finished before server."});
}
return handler(std::move(response));
}
BSONObj saslFollowupCommandPrefix = BSON(saslContinueCommandName << 1);
asyncSaslConversation(runCommand,
session,
std::move(saslFollowupCommandPrefix),
std::move(serverResponse),
std::move(targetDatabase),
saslLogLevel,
handler);
});
}
/**
* Driver for the client side of a sasl authentication session, conducted synchronously over
* "client".
*/
void saslClientAuthenticateImpl(auth::RunCommandHook runCommand,
const HostAndPort& hostname,
const BSONObj& saslParameters,
auth::AuthCompletionHandler handler) {
int saslLogLevel = getSaslClientLogLevel(saslParameters);
std::string targetDatabase;
try {
Status status = bsonExtractStringFieldWithDefault(
saslParameters, saslCommandUserDBFieldName, saslDefaultDBName, &targetDatabase);
if (!status.isOK())
return handler(std::move(status));
} catch (const DBException& ex) {
return handler(ex.toStatus());
}
std::string mechanism;
Status status =
bsonExtractStringField(saslParameters, saslCommandMechanismFieldName, &mechanism);
if (!status.isOK()) {
return handler(std::move(status));
}
// 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 session(SaslClientSession::create(mechanism));
status = configureSession(session.get(), hostname, targetDatabase, saslParameters);
if (!status.isOK())
return handler(std::move(status));
BSONObj saslFirstCommandPrefix =
BSON(saslStartCommandName << 1 << saslCommandMechanismFieldName
<< session->getParameter(SaslClientSession::parameterMechanism));
BSONObj inputObj = BSON(saslCommandPayloadFieldName << "");
asyncSaslConversation(runCommand,
session,
std::move(saslFirstCommandPrefix),
std::move(inputObj),
targetDatabase,
saslLogLevel,
handler);
}
MONGO_INITIALIZER(SaslClientAuthenticateFunction)(InitializerContext* context) {
saslClientAuthenticate = saslClientAuthenticateImpl;
return Status::OK();
}
} // namespace
} // namespace mongo