diff --git a/SConstruct b/SConstruct index 3f440157c10..112256413d5 100644 --- a/SConstruct +++ b/SConstruct @@ -200,6 +200,8 @@ add_option( "gcov" , "compile with flags for gcov" , 0 , True ) add_option("smokedbprefix", "prefix to dbpath et al. for smoke tests", 1 , False ) add_option("smokeauth", "run smoke tests with --auth", 0 , False ) +add_option("use-sasl-client", "Support SASL authentication in the client library", 0, False) + add_option( "use-system-tcmalloc", "use system version of tcmalloc library", 0, True ) add_option( "use-system-pcre", "use system version of pcre library", 0, True ) @@ -819,6 +821,12 @@ def doConfigure(myenv): if not conf.CheckLib( v8_lib_choices ): Exit(1) + env['MONGO_BUILD_SASL_CLIENT'] = bool(has_option("use-sasl-client")) + if env['MONGO_BUILD_SASL_CLIENT'] and not conf.CheckLibWithHeader( + "gsasl", "gsasl.h", "C", "gsasl_check_version(GSASL_VERSION);", autoadd=False): + + Exit(1) + # requires ports devel/libexecinfo to be installed if freebsd or openbsd: if not conf.CheckLib("execinfo"): diff --git a/src/mongo/SConscript b/src/mongo/SConscript index 829580a3c15..d60c3d017f4 100644 --- a/src/mongo/SConscript +++ b/src/mongo/SConscript @@ -126,6 +126,13 @@ commonFiles = [ "pch.cpp", "db/dbmessage.cpp" ] +commonSysLibdeps = [] + +if env['MONGO_BUILD_SASL_CLIENT']: + commonFiles.extend(['client/sasl_client_authenticate.cpp', + 'util/gsasl_session.cpp']) + commonSysLibdeps.append('gsasl') + # handle processinfo* processInfoFiles = [ "util/processinfo.cpp" ] @@ -161,7 +168,8 @@ env.StaticLibrary('mongocommon', commonFiles, 'fail_point', '$BUILD_DIR/third_party/pcrecpp', '$BUILD_DIR/third_party/murmurhash3/murmurhash3', - '$BUILD_DIR/third_party/shim_boost'],) + '$BUILD_DIR/third_party/shim_boost'], + SYSLIBDEPS=commonSysLibdeps) env.StaticLibrary("coredb", [ "client/parallel.cpp", diff --git a/src/mongo/base/error_codes.err b/src/mongo/base/error_codes.err index 46956999cc1..59927f7a94e 100644 --- a/src/mongo/base/error_codes.err +++ b/src/mongo/base/error_codes.err @@ -16,7 +16,11 @@ error_code("UnsupportedFormat", 12) error_code("Unauthorized", 13) error_code("TypeMismatch", 14) error_code("Overflow", 15) -error_code("IllegalOperation", 16) -error_code("EmptyArrayOperation", 17) +error_code("InvalidLength", 16) +error_code("ProtocolError", 17) +error_code("AuthenticationFailed", 18) +error_code("CannotReuseObject", 19) +error_code("IllegalOperation", 20) +error_code("EmptyArrayOperation", 21) error_class("NetworkError", ["HostUnreachable", "HostNotFound"]) diff --git a/src/mongo/client/sasl_client_authenticate.cpp b/src/mongo/client/sasl_client_authenticate.cpp new file mode 100644 index 00000000000..fe1e31add53 --- /dev/null +++ b/src/mongo/client/sasl_client_authenticate.cpp @@ -0,0 +1,229 @@ +/* Copyright 2012 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "mongo/client/sasl_client_authenticate.h" + +#include + +#include "mongo/base/string_data.h" +#include "mongo/bson/util/bson_extract.h" +#include "mongo/util/base64.h" +#include "mongo/util/gsasl_session.h" +#include "mongo/util/log.h" +#include "mongo/util/mongoutils/str.h" +#include "mongo/util/net/hostandport.h" + +namespace mongo { + + using namespace mongoutils; + + const char* const saslStartCommandName = "saslStart"; + const char* const saslContinueCommandName = "saslContinue"; + const char* const saslCommandAutoAuthorizeFieldName = "autoAuthorize"; + const char* const saslCommandCodeFieldName = "code"; + const char* const saslCommandConversationIdFieldName = "conversationId"; + const char* const saslCommandDoneFieldName = "done"; + const char* const saslCommandErrmsgFieldName = "errmsg"; + const char* const saslCommandMechanismFieldName = "mechanism"; + const char* const saslCommandMechanismListFieldName = "supportedMechanisms"; + const char* const saslCommandPasswordFieldName = "password"; + const char* const saslCommandPayloadFieldName = "payload"; + const char* const saslCommandPrincipalFieldName = "principal"; + const char* const saslCommandServiceHostnameFieldName = "serviceHostname"; + const char* const saslCommandServiceNameFieldName = "serviceName"; + const char* const saslDefaultDBName = "admin"; + const char* const saslDefaultServiceName = "mongodb"; + + const char* const saslClientLogFieldName = "clientLogLevel"; + +namespace { + // Default log level on the client for SASL log messages. + const int defaultSaslClientLogLevel = 4; +} // namespace + + Status saslExtractPayload(const BSONObj& cmdObj, std::string* payload, BSONType* type) { + BSONElement payloadElement; + Status status = bsonExtractField(cmdObj, saslCommandPayloadFieldName, &payloadElement); + if (!status.isOK()) + return status; + + *type = payloadElement.type(); + if (payloadElement.type() == BinData) { + const char* payloadData; + int payloadLen; + payloadData = payloadElement.binData(payloadLen); + if (payloadLen < 0) + return Status(ErrorCodes::InvalidLength, "Negative payload length"); + *payload = std::string(payloadData, payloadData + payloadLen); + } + else if (payloadElement.type() == String) { + try { + *payload = base64::decode(payloadElement.str()); + } catch (UserException& e) { + return Status(ErrorCodes::FailedToParse, e.what()); + } + } + else { + return Status(ErrorCodes::TypeMismatch, + (str::stream() << "Wrong type for field; expected BinData or String for " + << payloadElement)); + } + + return Status::OK(); + } + +namespace { + + /** + * Configure "*session" as a client gsasl session for authenticating on the connection + * "*client", with the given "saslParameters". "gsasl" and "sessionHook" are passed through + * to GsaslSession::initializeClientSession, where they are documented. + */ + Status configureSession(Gsasl* gsasl, + DBClientWithCommands* client, + const BSONObj& saslParameters, + void* sessionHook, + GsaslSession* session) { + + std::string mechanism; + Status status = bsonExtractStringField(saslParameters, + saslCommandMechanismFieldName, + &mechanism); + if (!status.isOK()) + return status; + + status = session->initializeClientSession(gsasl, mechanism, sessionHook); + if (!status.isOK()) + return status; + + std::string service; + status = bsonExtractStringFieldWithDefault(saslParameters, + saslCommandServiceNameFieldName, + saslDefaultServiceName, + &service); + if (!status.isOK()) + return status; + session->setProperty(GSASL_SERVICE, service); + + std::string hostname; + status = bsonExtractStringFieldWithDefault(saslParameters, + saslCommandServiceHostnameFieldName, + HostAndPort(client->getServerAddress()).host(), + &hostname); + if (!status.isOK()) + return status; + session->setProperty(GSASL_HOSTNAME, hostname); + + BSONElement element = saslParameters[saslCommandPrincipalFieldName]; + if (element.type() == String) { + session->setProperty(GSASL_AUTHID, element.str()); + } + else if (!element.eoo()) { + return Status(ErrorCodes::TypeMismatch, + str::stream() << "Expected string for " << element); + } + + element = saslParameters[saslCommandPasswordFieldName]; + if (element.type() == String) { + session->setProperty(GSASL_PASSWORD, element.str()); + } + else if (!element.eoo()) { + return Status(ErrorCodes::TypeMismatch, + str::stream() << "Expected string for " << element); + } + + return Status::OK(); + } + + 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; + } + +} // namespace + + Status saslClientAuthenticate(Gsasl *gsasl, + DBClientWithCommands* client, + const BSONObj& saslParameters, + void* sessionHook) { + + GsaslSession session; + + int saslLogLevel = getSaslClientLogLevel(saslParameters); + + Status status = configureSession(gsasl, client, saslParameters, sessionHook, &session); + if (!status.isOK()) + return status; + + BSONObj saslFirstCommandPrefix = BSON( + saslStartCommandName << 1 << + saslCommandMechanismFieldName << session.getMechanism()); + + BSONObj saslFollowupCommandPrefix = BSON(saslContinueCommandName << 1); + BSONObj saslCommandPrefix = saslFirstCommandPrefix; + BSONObj inputObj = BSON(saslCommandPayloadFieldName << ""); + bool isServerDone = false; + while (!session.isDone()) { + std::string payload; + BSONType type; + + status = saslExtractPayload(inputObj, &payload, &type); + if (!status.isOK()) + return status; + + LOG(saslLogLevel) << "sasl client input: " << base64::encode(payload) << endl; + + std::string responsePayload; + status = session.step(payload, &responsePayload); + if (!status.isOK()) + return status; + + LOG(saslLogLevel) << "sasl client output: " << base64::encode(responsePayload) << endl; + + 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); + + if (!client->runCommand(saslDefaultDBName, commandBuilder.obj(), inputObj)) { + return Status(ErrorCodes::UnknownError, + inputObj[saslCommandErrmsgFieldName].str()); + } + + int statusCodeInt = inputObj[saslCommandCodeFieldName].Int(); + if (0 != statusCodeInt) + return Status(ErrorCodes::fromInt(statusCodeInt), + inputObj[saslCommandErrmsgFieldName].str()); + + isServerDone = inputObj[saslCommandDoneFieldName].trueValue(); + saslCommandPrefix = saslFollowupCommandPrefix; + } + + if (!isServerDone) + return Status(ErrorCodes::ProtocolError, "Client finished before server."); + return Status::OK(); + } + +} // namespace mongo diff --git a/src/mongo/client/sasl_client_authenticate.h b/src/mongo/client/sasl_client_authenticate.h new file mode 100644 index 00000000000..19de215aa14 --- /dev/null +++ b/src/mongo/client/sasl_client_authenticate.h @@ -0,0 +1,129 @@ +/* Copyright 2012 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "mongo/base/status.h" +#include "mongo/bson/bsontypes.h" +#include "mongo/client/dbclientinterface.h" + +struct Gsasl; + +namespace mongo { + class BSONObj; + + /** + * Attempts to authenticate "client" using the SASL protocol. + * + * Requires an initialized instance of the "gsasl" library, as the first + * parameter. + * + * The "saslParameters" BSONObj should be initialized with zero or more of the + * fields below. Which fields are required depends on the mechanism. Consult the + * libgsasl documentation. + * + * "mechanism": The 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. + * "database": The database target of the auth command. Optional for GSSAPI/Kerberos. + * "principal": The string name of the principal to authenticate, GSASL_AUTHID. + * "password": The password data, GSASL_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. + * + * "sessionHook" is a pointer to optional data, which may be used by the gsasl_callback + * previously set on "gsasl". The session hook is set on an underlying Gsasl_session using + * gsasl_session_hook_set, and may be accessed by callbacks using gsasl_session_hook_get. + * See the gsasl documentation. + * + * 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 + * returned. + */ + Status saslClientAuthenticate(Gsasl *gsasl, + DBClientWithCommands* client, + const BSONObj& saslParameters, + void* sessionHook); + + /** + * Extracts the payload field from "cmdObj", and store it into "*payload". + * + * Sets "*type" to the BSONType of the payload field in cmdObj. + * + * If the type of the payload field is String, the contents base64 decodes and + * stores into "*payload". If the type is BinData, the contents are stored directly + * into "*payload". In all other cases, returns + */ + Status saslExtractPayload(const BSONObj& cmdObj, std::string* payload, BSONType* type); + + // Constants + + /// String name of the saslStart command. + extern const char* const saslStartCommandName; + + /// String name of the saslContinue command. + extern const char* const saslContinueCommandName; + + /// Name of the saslStart parameter indicating that the server should automatically grant the + /// connection all privileges associated with the principal after successful authentication. + extern const char* const saslCommandAutoAuthorizeFieldName; + + /// Name of the field contain the status code in responses from the server. + extern const char* const saslCommandCodeFieldName; + + /// Name of the field containing the conversation identifier in server respones and saslContinue + /// commands. + extern const char* const saslCommandConversationIdFieldName; + + /// Name of the field that indicates whether or not the server believes authentication has + /// completed successfully. + extern const char* const saslCommandDoneFieldName; + + /// Field in which to store error messages associated with non-success return codes. + extern const char* const saslCommandErrmsgFieldName; + + /// Name of parameter to saslStart command indiciating the client's desired sasl mechanism. + extern const char* const saslCommandMechanismFieldName; + + /// In the event that saslStart supplies an unsupported mechanism, the server responds with a + /// field by this name, with a list of supported mechanisms. + extern const char* const saslCommandMechanismListFieldName; + + /// Field containing password information for saslClientAuthenticate(). + extern const char* const saslCommandPasswordFieldName; + + /// Field containing sasl payloads passed to and from the server. + extern const char* const saslCommandPayloadFieldName; + + /// Field containing the string identifier of the principal to authenticate in + /// saslClientAuthenticate(). + extern const char* const saslCommandPrincipalFieldName; + + /// Field overriding the FQDN of the hostname hosting the mongodb srevice in + /// saslClientAuthenticate(). + extern const char* const saslCommandServiceHostnameFieldName; + + /// Field overriding the name of the mongodb service saslClientAuthenticate(). + extern const char* const saslCommandServiceNameFieldName; + + /// Default database against which sasl authentication commands should run. + extern const char* const saslDefaultDBName; + + /// Default sasl service name, "mongodb". + extern const char* const saslDefaultServiceName; +} diff --git a/src/mongo/util/gsasl_session.cpp b/src/mongo/util/gsasl_session.cpp new file mode 100644 index 00000000000..c7ed0a4a8ec --- /dev/null +++ b/src/mongo/util/gsasl_session.cpp @@ -0,0 +1,94 @@ +/* Copyright 2012 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "mongo/util/gsasl_session.h" + +#include +#include + +#include "mongo/util/assert_util.h" + +namespace mongo { + + GsaslSession::GsaslSession() : _gsaslSession(NULL), _done(false) {} + + GsaslSession::~GsaslSession() { + if (_gsaslSession) + gsasl_finish(_gsaslSession); + } + + std::string GsaslSession::getMechanism() const { + return gsasl_mechanism_name(_gsaslSession); + } + + void GsaslSession::setProperty(Gsasl_property property, const StringData& value) { + gsasl_property_set_raw(_gsaslSession, property, value.data(), value.size()); + } + + Status GsaslSession::initializeClientSession(Gsasl* gsasl, + const StringData& mechanism, + void* sessionHook) { + return _initializeSession(&gsasl_client_start, gsasl, mechanism, sessionHook); + } + + Status GsaslSession::initializeServerSession(Gsasl* gsasl, + const StringData& mechanism, + void* sessionHook) { + return _initializeSession(&gsasl_server_start, gsasl, mechanism, sessionHook); + } + + Status GsaslSession::_initializeSession( + GsaslSessionStartFn sessionStartFn, + Gsasl* gsasl, const StringData& mechanism, void* sessionHook) { + + if (_done || _gsaslSession) + return Status(ErrorCodes::CannotReuseObject, "Cannot reuse GsaslSession."); + + int rc = sessionStartFn(gsasl, mechanism.data(), &_gsaslSession); + switch (rc) { + case GSASL_OK: + gsasl_session_hook_set(_gsaslSession, sessionHook); + return Status::OK(); + case GSASL_UNKNOWN_MECHANISM: + return Status(ErrorCodes::BadValue, gsasl_strerror(rc)); + default: + return Status(ErrorCodes::ProtocolError, gsasl_strerror(rc)); + } + } + + Status GsaslSession::step(const StringData& inputData, std::string* outputData) { + char* output; + size_t outputSize; + int rc = gsasl_step(_gsaslSession, + inputData.data(), inputData.size(), + &output, &outputSize); + + if (GSASL_OK == rc) + _done = true; + + switch (rc) { + case GSASL_OK: + case GSASL_NEEDS_MORE: + *outputData = std::string(output, output + outputSize); + free(output); + return Status::OK(); + case GSASL_AUTHENTICATION_ERROR: + return Status(ErrorCodes::AuthenticationFailed, gsasl_strerror(rc)); + default: + return Status(ErrorCodes::ProtocolError, gsasl_strerror(rc)); + } + } + +} // namespace mongo diff --git a/src/mongo/util/gsasl_session.h b/src/mongo/util/gsasl_session.h new file mode 100644 index 00000000000..f78f723f460 --- /dev/null +++ b/src/mongo/util/gsasl_session.h @@ -0,0 +1,138 @@ +/* Copyright 2012 10gen Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "mongo/base/disallow_copying.h" +#include "mongo/base/status.h" +#include "mongo/base/string_data.h" + +namespace mongo { + + /** + * C++ wrapper around Gsasl_session. + */ + class GsaslSession { + MONGO_DISALLOW_COPYING(GsaslSession); + public: + GsaslSession(); + ~GsaslSession(); + + /** + * Initializes "this" as a client sasl session. + * + * May only be called once on an instance of GsaslSession, and may not be called on an + * instance on which initializeServerSession has been called. + * + * "gsasl" is a pointer to a Gsasl library context that will exist for the rest of + * the lifetime of "this". + * + * "mechanism" is a SASL mechanism name. + * + * "sessionHook" is user-supplied data associated with this session. If is accessible in + * the gsasl callback set on "gsasl" using gsasl_session_hook_get(). May be NULL. Owned + * by caller, and must stay in scope as long as this object. + * + * Returns Status::OK() on success, some other status on errors. + */ + Status initializeClientSession(Gsasl* gsasl, + const StringData& mechanism, + void* sessionHook); + + /** + * Initializes "this" as a server sasl session. + * + * May only be called once on an instance of GsaslSession, and may not be called on an + * instance on which initializeClientSession has been called. + * + * "gsasl" is a pointer to a Gsasl library context that will exist for the rest of + * the lifetime of "this". + * + * "mechanism" is a SASL mechanism name. + * + * "sessionHook" is user-supplied data associated with this session. If is accessible in + * the gsasl callback set on "gsasl" using gsasl_session_hook_get(). May be NULL. Owned + * by caller, and must stay in scope as long as this object. + * + * Returns Status::OK() on success, some other status on errors. + */ + Status initializeServerSession(Gsasl* gsasl, + const StringData& mechanism, + void* sessionHook); + + /** + * Returns the string name of the SASL mechanism in use in this session. + * + * Not valid before initializeServerSession() or initializeClientSession(). + */ + std::string getMechanism() const; + + /** + * Sets a property on this session. + * + * Not valid before initializeServerSession() or initializeClientSession(). + */ + void setProperty(Gsasl_property property, const StringData& value); + + /** + * Performs one more step on this session. + * + * Receives "inputData" from the other side and produces "*outputData" to send. + * + * Both "inputData" and "*outputData" are logically strings of bytes, not characters. + * + * For the first step by the authentication initiator, "inputData" should have 0 length. + * + * Returns Status::OK() on success. In that case, isDone() can be queried to see if the + * session expects another call to step(). If isDone() is true, the authentication has + * completed successfully. + * + * Any return other than Status::OK() means that authentication has failed, but the specific + * code or reason message may provide insight as to why. + */ + Status step(const StringData& inputData, std::string* outputData); + + /** + * Returns true if this session has completed successfully. + * + * That is, returns true if the session expects no more calls to step(), and all previous + * calls to step() and initializeClientSession()/initializeServerSession() have returned + * Status::OK(). + */ + bool isDone() const { return _done; } + + private: + // Signature of gsas session start functions. + typedef int (*GsaslSessionStartFn)(Gsasl*, const char*, Gsasl_session**); + + /** + * Common helper code for initializing a session. + * + * Uses "sessionStartFn" to initialize the underlying Gsasl_session. + */ + Status _initializeSession(GsaslSessionStartFn sessionStartFn, + Gsasl* gsasl, const StringData& mechanism, void* sessionHook); + + /// Underlying C-library gsasl session object. + Gsasl_session* _gsaslSession; + + /// See isDone(), above. + bool _done; + }; + +} // namespace mongo