TLS-negotiation [5/n]: Rename sasl_[client|server] to [client|server]_negotiation
This rename is in anticipation of a major refactor to both of these classes which will make them less specific to SASL. The file rename is kept in a separate commit in order to preserve git history. Change-Id: Ib4fb4bcdb7c7a43eea88b2911d2b2e75eafa62b0 Reviewed-on: http://gerrit.cloudera.org:8080/5759 Tested-by: Kudu Jenkins Reviewed-by: Todd Lipcon <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/kudu/repo Commit: http://git-wip-us.apache.org/repos/asf/kudu/commit/41c45523 Tree: http://git-wip-us.apache.org/repos/asf/kudu/tree/41c45523 Diff: http://git-wip-us.apache.org/repos/asf/kudu/diff/41c45523 Branch: refs/heads/master Commit: 41c45523f7d34d7800fd29d4cbe40330cac13e6a Parents: 9d0421e Author: Dan Burkert <[email protected]> Authored: Fri Jan 20 15:34:43 2017 -0800 Committer: Todd Lipcon <[email protected]> Committed: Mon Jan 23 23:37:18 2017 +0000 ---------------------------------------------------------------------- src/kudu/rpc/CMakeLists.txt | 8 +- src/kudu/rpc/client_negotiation.cc | 522 +++++++++++++++++++++++++++++ src/kudu/rpc/client_negotiation.h | 180 ++++++++++ src/kudu/rpc/connection.cc | 3 +- src/kudu/rpc/connection.h | 6 +- src/kudu/rpc/negotiation-test.cc | 567 ++++++++++++++++++++++++++++++++ src/kudu/rpc/negotiation.cc | 4 +- src/kudu/rpc/reactor.cc | 4 +- src/kudu/rpc/sasl_client.cc | 521 ----------------------------- src/kudu/rpc/sasl_client.h | 180 ---------- src/kudu/rpc/sasl_rpc-test.cc | 567 -------------------------------- src/kudu/rpc/sasl_server.cc | 498 ---------------------------- src/kudu/rpc/sasl_server.h | 180 ---------- src/kudu/rpc/server_negotiation.cc | 499 ++++++++++++++++++++++++++++ src/kudu/rpc/server_negotiation.h | 180 ++++++++++ 15 files changed, 1960 insertions(+), 1959 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/kudu/blob/41c45523/src/kudu/rpc/CMakeLists.txt ---------------------------------------------------------------------- diff --git a/src/kudu/rpc/CMakeLists.txt b/src/kudu/rpc/CMakeLists.txt index 6ba028b..fa21551 100644 --- a/src/kudu/rpc/CMakeLists.txt +++ b/src/kudu/rpc/CMakeLists.txt @@ -43,12 +43,13 @@ ADD_EXPORTABLE_LIBRARY(rpc_introspection_proto set(KRPC_SRCS acceptor_pool.cc blocking_ops.cc - outbound_call.cc + client_negotiation.cc connection.cc constants.cc inbound_call.cc messenger.cc negotiation.cc + outbound_call.cc proxy.cc reactor.cc remote_method.cc @@ -59,10 +60,9 @@ set(KRPC_SRCS rpc_controller.cc rpcz_store.cc sasl_common.cc - sasl_client.cc sasl_helper.cc - sasl_server.cc serialization.cc + server_negotiation.cc service_if.cc service_pool.cc service_queue.cc @@ -116,11 +116,11 @@ target_link_libraries(rtest_krpc set(KUDU_TEST_LINK_LIBS rtest_krpc krpc rpc_header_proto security-test ${KUDU_MIN_TEST_LIBS}) ADD_KUDU_TEST(exactly_once_rpc-test) ADD_KUDU_TEST(mt-rpc-test RUN_SERIAL true) +ADD_KUDU_TEST(negotiation-test) ADD_KUDU_TEST(reactor-test) ADD_KUDU_TEST(request_tracker-test) ADD_KUDU_TEST(rpc-bench RUN_SERIAL true) ADD_KUDU_TEST(rpc-test) ADD_KUDU_TEST(rpc_stub-test) -ADD_KUDU_TEST(sasl_rpc-test) ADD_KUDU_TEST(service_queue-test) http://git-wip-us.apache.org/repos/asf/kudu/blob/41c45523/src/kudu/rpc/client_negotiation.cc ---------------------------------------------------------------------- diff --git a/src/kudu/rpc/client_negotiation.cc b/src/kudu/rpc/client_negotiation.cc new file mode 100644 index 0000000..0eb96b8 --- /dev/null +++ b/src/kudu/rpc/client_negotiation.cc @@ -0,0 +1,522 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 "kudu/rpc/client_negotiation.h" + +#include <string.h> + +#include <map> +#include <set> +#include <string> + +#include <glog/logging.h> +#include <sasl/sasl.h> + +#include "kudu/gutil/endian.h" +#include "kudu/gutil/map-util.h" +#include "kudu/gutil/stl_util.h" +#include "kudu/gutil/stringprintf.h" +#include "kudu/gutil/strings/join.h" +#include "kudu/gutil/strings/substitute.h" +#include "kudu/gutil/strings/util.h" +#include "kudu/rpc/blocking_ops.h" +#include "kudu/rpc/constants.h" +#include "kudu/rpc/rpc_header.pb.h" +#include "kudu/rpc/sasl_common.h" +#include "kudu/rpc/sasl_helper.h" +#include "kudu/rpc/serialization.h" +#include "kudu/util/faststring.h" +#include "kudu/util/net/sockaddr.h" +#include "kudu/util/net/socket.h" +#include "kudu/util/scoped_cleanup.h" +#include "kudu/util/trace.h" + +namespace kudu { +namespace rpc { + +using std::map; +using std::set; +using std::string; + +static int SaslClientGetoptCb(void* sasl_client, const char* plugin_name, const char* option, + const char** result, unsigned* len) { + return static_cast<SaslClient*>(sasl_client) + ->GetOptionCb(plugin_name, option, result, len); +} + +static int SaslClientSimpleCb(void *sasl_client, int id, + const char **result, unsigned *len) { + return static_cast<SaslClient*>(sasl_client)->SimpleCb(id, result, len); +} + +static int SaslClientSecretCb(sasl_conn_t* conn, void *sasl_client, int id, + sasl_secret_t** psecret) { + return static_cast<SaslClient*>(sasl_client)->SecretCb(conn, id, psecret); +} + +// Return an appropriately-typed Status object based on an ErrorStatusPB returned +// from an Error RPC. +// In case there is no relevant Status type, return a RuntimeError. +static Status StatusFromRpcError(const ErrorStatusPB& error) { + DCHECK(error.IsInitialized()) << "Error status PB must be initialized"; + if (PREDICT_FALSE(!error.has_code())) { + return Status::RuntimeError(error.message()); + } + string code_name = ErrorStatusPB::RpcErrorCodePB_Name(error.code()); + switch (error.code()) { + case ErrorStatusPB_RpcErrorCodePB_FATAL_UNAUTHORIZED: + return Status::NotAuthorized(code_name, error.message()); + default: + return Status::RuntimeError(code_name, error.message()); + } +} + +SaslClient::SaslClient(string app_name, Socket* socket) + : app_name_(std::move(app_name)), + sock_(socket), + helper_(SaslHelper::CLIENT), + client_state_(SaslNegotiationState::NEW), + negotiated_mech_(SaslMechanism::INVALID), + deadline_(MonoTime::Max()) { + callbacks_.push_back(SaslBuildCallback(SASL_CB_GETOPT, + reinterpret_cast<int (*)()>(&SaslClientGetoptCb), this)); + callbacks_.push_back(SaslBuildCallback(SASL_CB_AUTHNAME, + reinterpret_cast<int (*)()>(&SaslClientSimpleCb), this)); + callbacks_.push_back(SaslBuildCallback(SASL_CB_PASS, + reinterpret_cast<int (*)()>(&SaslClientSecretCb), this)); + callbacks_.push_back(SaslBuildCallback(SASL_CB_LIST_END, nullptr, nullptr)); +} + +Status SaslClient::EnablePlain(const string& user, const string& pass) { + DCHECK_EQ(client_state_, SaslNegotiationState::NEW); + RETURN_NOT_OK(helper_.EnablePlain()); + plain_auth_user_ = user; + plain_pass_ = pass; + return Status::OK(); +} + +Status SaslClient::EnableGSSAPI() { + DCHECK_EQ(client_state_, SaslNegotiationState::NEW); + return helper_.EnableGSSAPI(); +} + +SaslMechanism::Type SaslClient::negotiated_mechanism() const { + DCHECK_EQ(client_state_, SaslNegotiationState::NEGOTIATED); + return negotiated_mech_; +} + +void SaslClient::set_local_addr(const Sockaddr& addr) { + DCHECK_EQ(client_state_, SaslNegotiationState::NEW); + helper_.set_local_addr(addr); +} + +void SaslClient::set_remote_addr(const Sockaddr& addr) { + DCHECK_EQ(client_state_, SaslNegotiationState::NEW); + helper_.set_remote_addr(addr); +} + +void SaslClient::set_server_fqdn(const string& domain_name) { + DCHECK_EQ(client_state_, SaslNegotiationState::NEW); + helper_.set_server_fqdn(domain_name); +} + +void SaslClient::set_deadline(const MonoTime& deadline) { + DCHECK_NE(client_state_, SaslNegotiationState::NEGOTIATED); + deadline_ = deadline; +} + +// calls sasl_client_init() and sasl_client_new() +Status SaslClient::Init(const string& service_type) { + RETURN_NOT_OK(SaslInit(app_name_.c_str())); + + // Ensure we are not called more than once. + if (client_state_ != SaslNegotiationState::NEW) { + return Status::IllegalState("Init() may only be called once per SaslClient object."); + } + + // TODO(unknown): Support security flags. + unsigned secflags = 0; + + sasl_conn_t* sasl_conn = nullptr; + Status s = WrapSaslCall(nullptr /* no conn */, [&]() { + return sasl_client_new( + service_type.c_str(), // Registered name of the service using SASL. Required. + helper_.server_fqdn(), // The fully qualified domain name of the remote server. + helper_.local_addr_string(), // Local and remote IP address strings. (NULL disables + helper_.remote_addr_string(), // mechanisms which require this info.) + &callbacks_[0], // Connection-specific callbacks. + secflags, // Security flags. + &sasl_conn); + }); + if (!s.ok()) { + return Status::RuntimeError("Unable to create new SASL client", + s.message()); + } + sasl_conn_.reset(sasl_conn); + + client_state_ = SaslNegotiationState::INITIALIZED; + return Status::OK(); +} + +Status SaslClient::Negotiate() { + // After negotiation, we no longer need the SASL library object, so + // may as well free its memory since the connection may be long-lived. + // Additionally, this works around a SEGV seen at process shutdown time: + // if we still have SASL objects retained by Reactor when the process + // is exiting, the SASL libraries may start destructing global state + // and cause a crash when we sasl_dispose the connection. + auto cleanup = MakeScopedCleanup([&]() { + sasl_conn_.reset(); + }); + TRACE("Called SaslClient::Negotiate()"); + + // Ensure we called exactly once, and in the right order. + if (client_state_ == SaslNegotiationState::NEW) { + return Status::IllegalState("SaslClient: Init() must be called before calling Negotiate()"); + } + if (client_state_ == SaslNegotiationState::NEGOTIATED) { + return Status::IllegalState("SaslClient: Negotiate() may only be called once per object."); + } + + // Ensure we can use blocking calls on the socket during negotiation. + RETURN_NOT_OK(EnsureBlockingMode(sock_)); + + // Start by asking the server for a list of available auth mechanisms. + RETURN_NOT_OK(SendNegotiateMessage()); + + faststring recv_buf; + nego_ok_ = false; + + // We set nego_ok_ = true when the SASL library returns SASL_OK to us. + // We set nego_response_expected_ = true each time we send a request to the server. + while (!nego_ok_ || nego_response_expected_) { + ResponseHeader header; + Slice param_buf; + RETURN_NOT_OK(ReceiveFramedMessageBlocking(sock_, &recv_buf, &header, ¶m_buf, deadline_)); + nego_response_expected_ = false; + + NegotiatePB response; + RETURN_NOT_OK(ParseNegotiatePB(header, param_buf, &response)); + + switch (response.step()) { + // NEGOTIATE: Server has sent us its list of supported SASL mechanisms. + case NegotiatePB::NEGOTIATE: + RETURN_NOT_OK(HandleNegotiateResponse(response)); + break; + + // SASL_CHALLENGE: Server sent us a follow-up to an SASL_INITIATE or SASL_RESPONSE request. + case NegotiatePB::SASL_CHALLENGE: + RETURN_NOT_OK(HandleChallengeResponse(response)); + break; + + // SASL_SUCCESS: Server has accepted our authentication request. Negotiation successful. + case NegotiatePB::SASL_SUCCESS: + RETURN_NOT_OK(HandleSuccessResponse(response)); + break; + + // Client sent us some unsupported SASL response. + default: + LOG(ERROR) << "SASL Client: Received unsupported response from server"; + return Status::InvalidArgument("RPC client doesn't support Negotiate step", + NegotiatePB::NegotiateStep_Name(response.step())); + } + } + + TRACE("SASL Client: Successful negotiation"); + client_state_ = SaslNegotiationState::NEGOTIATED; + return Status::OK(); +} + +Status SaslClient::SendNegotiatePB(const NegotiatePB& msg) { + DCHECK_NE(client_state_, SaslNegotiationState::NEW) + << "Must not send Negotiate messages before calling Init()"; + DCHECK_NE(client_state_, SaslNegotiationState::NEGOTIATED) + << "Must not send Negotiate messages after negotiation succeeds"; + + // Create header with SASL-specific callId + RequestHeader header; + header.set_call_id(kNegotiateCallId); + return helper_.SendNegotiatePB(sock_, header, msg, deadline_); +} + +Status SaslClient::ParseNegotiatePB(const ResponseHeader& header, + const Slice& param_buf, + NegotiatePB* response) { + RETURN_NOT_OK(helper_.SanityCheckNegotiateCallId(header.call_id())); + + if (header.is_error()) { + return ParseError(param_buf); + } + + return helper_.ParseNegotiatePB(param_buf, response); +} + +Status SaslClient::SendNegotiateMessage() { + NegotiatePB msg; + msg.set_step(NegotiatePB::NEGOTIATE); + + // Advertise our supported features. + for (RpcFeatureFlag feature : kSupportedClientRpcFeatureFlags) { + msg.add_supported_features(feature); + } + + TRACE("SASL Client: Sending NEGOTIATE request to server."); + RETURN_NOT_OK(SendNegotiatePB(msg)); + nego_response_expected_ = true; + return Status::OK(); +} + +Status SaslClient::SendInitiateMessage(const NegotiatePB_SaslAuth& auth, + const char* init_msg, unsigned init_msg_len) { + NegotiatePB msg; + msg.set_step(NegotiatePB::SASL_INITIATE); + msg.mutable_token()->assign(init_msg, init_msg_len); + msg.add_auths()->CopyFrom(auth); + TRACE("SASL Client: Sending SASL_INITIATE request to server."); + RETURN_NOT_OK(SendNegotiatePB(msg)); + nego_response_expected_ = true; + return Status::OK(); +} + +Status SaslClient::SendResponseMessage(const char* resp_msg, unsigned resp_msg_len) { + NegotiatePB reply; + reply.set_step(NegotiatePB::SASL_RESPONSE); + reply.mutable_token()->assign(resp_msg, resp_msg_len); + TRACE("SASL Client: Sending SASL_RESPONSE request to server."); + RETURN_NOT_OK(SendNegotiatePB(reply)); + nego_response_expected_ = true; + return Status::OK(); +} + +Status SaslClient::DoSaslStep(const string& in, const char** out, unsigned* out_len) { + TRACE("SASL Client: Calling sasl_client_step()"); + Status s = WrapSaslCall(sasl_conn_.get(), [&]() { + return sasl_client_step(sasl_conn_.get(), in.c_str(), in.length(), nullptr, out, out_len); + }); + if (s.ok()) { + nego_ok_ = true; + } + return s; +} + +Status SaslClient::HandleNegotiateResponse(const NegotiatePB& response) { + TRACE("SASL Client: Received NEGOTIATE response from server"); + // Fill in the set of features supported by the server. + for (int flag : response.supported_features()) { + // We only add the features that our local build knows about. + RpcFeatureFlag feature_flag = RpcFeatureFlag_IsValid(flag) ? + static_cast<RpcFeatureFlag>(flag) : UNKNOWN; + if (ContainsKey(kSupportedClientRpcFeatureFlags, feature_flag)) { + server_features_.insert(feature_flag); + } + } + + // Build a map of the mechanisms offered by the server. + const set<string>& local_mechs = helper_.LocalMechs(); + set<string> server_mechs; + map<string, NegotiatePB::SaslAuth> server_mech_map; + for (const NegotiatePB::SaslAuth& auth : response.auths()) { + const auto& mech = auth.mechanism(); + server_mech_map[mech] = auth; + server_mechs.insert(mech); + } + // Determine which server mechs are also enabled by the client. + // Cyrus SASL 2.1.25 and later supports doing this set intersection via + // the 'client_mech_list' option, but that version is not available on + // RHEL 6, so we have to do it manually. + set<string> matching_mechs = STLSetIntersection(local_mechs, server_mechs); + + if (matching_mechs.empty() && + ContainsKey(server_mechs, kSaslMechGSSAPI) && + !ContainsKey(local_mechs, kSaslMechGSSAPI)) { + return Status::NotAuthorized("server requires GSSAPI (Kerberos) authentication and " + "client was missing the required SASL module"); + } + + string matching_mechs_str = JoinElements(matching_mechs, " "); + TRACE("SASL Client: Matching mech list: $0", matching_mechs_str); + + const char* init_msg = nullptr; + unsigned init_msg_len = 0; + const char* negotiated_mech = nullptr; + + /* select a mechanism for a connection + * mechlist -- mechanisms server has available (punctuation ignored) + * output: + * prompt_need -- on SASL_INTERACT, list of prompts needed to continue + * clientout -- the initial client response to send to the server + * mech -- set to mechanism name + * + * Returns: + * SASL_OK -- success + * SASL_CONTINUE -- negotiation required + * SASL_NOMEM -- not enough memory + * SASL_NOMECH -- no mechanism meets requested properties + * SASL_INTERACT -- user interaction needed to fill in prompt_need list + */ + TRACE("SASL Client: Calling sasl_client_start()"); + Status s = WrapSaslCall(sasl_conn_.get(), [&]() { + return sasl_client_start( + sasl_conn_.get(), // The SASL connection context created by init() + matching_mechs_str.c_str(), // The list of mechanisms to negotiate. + nullptr, // Disables INTERACT return if NULL. + &init_msg, // Filled in on success. + &init_msg_len, // Filled in on success. + &negotiated_mech); // Filled in on success. + }); + + if (s.ok()) { + nego_ok_ = true; + } else if (!s.IsIncomplete()) { + return s; + } + + // The server matched one of our mechanisms. + NegotiatePB::SaslAuth* auth = FindOrNull(server_mech_map, negotiated_mech); + if (PREDICT_FALSE(auth == nullptr)) { + return Status::IllegalState("Unable to find auth in map, unexpected error", negotiated_mech); + } + negotiated_mech_ = SaslMechanism::value_of(negotiated_mech); + + RETURN_NOT_OK(SendInitiateMessage(*auth, init_msg, init_msg_len)); + return Status::OK(); +} + +Status SaslClient::HandleChallengeResponse(const NegotiatePB& response) { + TRACE("SASL Client: Received SASL_CHALLENGE response from server"); + if (PREDICT_FALSE(nego_ok_)) { + LOG(DFATAL) << "Server sent SASL_CHALLENGE response after client library returned SASL_OK"; + } + + if (PREDICT_FALSE(!response.has_token())) { + return Status::InvalidArgument("No token in SASL_CHALLENGE response from server"); + } + + const char* out = nullptr; + unsigned out_len = 0; + Status s = DoSaslStep(response.token(), &out, &out_len); + if (!s.ok() && !s.IsIncomplete()) { + return s; + } + RETURN_NOT_OK(SendResponseMessage(out, out_len)); + return Status::OK(); +} + +Status SaslClient::HandleSuccessResponse(const NegotiatePB& response) { + TRACE("SASL Client: Received SASL_SUCCESS response from server"); + if (!nego_ok_) { + const char* out = nullptr; + unsigned out_len = 0; + Status s = DoSaslStep(response.token(), &out, &out_len); + if (s.IsIncomplete()) { + return Status::IllegalState("Server indicated successful authentication, but client " + "was not complete"); + } + RETURN_NOT_OK(s); + if (out_len > 0) { + return Status::IllegalState("SASL client library generated spurious token after SASL_SUCCESS", + string(out, out_len)); + } + CHECK(nego_ok_); + } + return Status::OK(); +} + +// Parse error status message from raw bytes of an ErrorStatusPB. +Status SaslClient::ParseError(const Slice& err_data) { + ErrorStatusPB error; + if (!error.ParseFromArray(err_data.data(), err_data.size())) { + return Status::IOError("Invalid error response, missing fields", + error.InitializationErrorString()); + } + Status s = StatusFromRpcError(error); + TRACE("SASL Client: Received error response from server: $0", s.ToString()); + return s; +} + +int SaslClient::GetOptionCb(const char* plugin_name, const char* option, + const char** result, unsigned* len) { + return helper_.GetOptionCb(plugin_name, option, result, len); +} + +// Used for PLAIN. +// SASL callback for SASL_CB_USER, SASL_CB_AUTHNAME, SASL_CB_LANGUAGE +int SaslClient::SimpleCb(int id, const char** result, unsigned* len) { + if (PREDICT_FALSE(!helper_.IsPlainEnabled())) { + LOG(DFATAL) << "SASL Client: Simple callback called, but PLAIN auth is not enabled"; + return SASL_FAIL; + } + if (PREDICT_FALSE(result == nullptr)) { + LOG(DFATAL) << "SASL Client: result outparam is NULL"; + return SASL_BADPARAM; + } + switch (id) { + // TODO(unknown): Support impersonation? + // For impersonation, USER is the impersonated user, AUTHNAME is the "sudoer". + case SASL_CB_USER: + TRACE("SASL Client: callback for SASL_CB_USER"); + *result = plain_auth_user_.c_str(); + if (len != nullptr) *len = plain_auth_user_.length(); + break; + case SASL_CB_AUTHNAME: + TRACE("SASL Client: callback for SASL_CB_AUTHNAME"); + *result = plain_auth_user_.c_str(); + if (len != nullptr) *len = plain_auth_user_.length(); + break; + case SASL_CB_LANGUAGE: + LOG(DFATAL) << "SASL Client: Unable to handle SASL callback type SASL_CB_LANGUAGE" + << "(" << id << ")"; + return SASL_BADPARAM; + default: + LOG(DFATAL) << "SASL Client: Unexpected SASL callback type: " << id; + return SASL_BADPARAM; + } + + return SASL_OK; +} + +// Used for PLAIN. +// SASL callback for SASL_CB_PASS: User password. +int SaslClient::SecretCb(sasl_conn_t* conn, int id, sasl_secret_t** psecret) { + if (PREDICT_FALSE(!helper_.IsPlainEnabled())) { + LOG(DFATAL) << "SASL Client: Plain secret callback called, but PLAIN auth is not enabled"; + return SASL_FAIL; + } + switch (id) { + case SASL_CB_PASS: { + if (!conn || !psecret) return SASL_BADPARAM; + + size_t len = plain_pass_.length(); + *psecret = reinterpret_cast<sasl_secret_t*>(malloc(sizeof(sasl_secret_t) + len)); + if (!*psecret) { + return SASL_NOMEM; + } + psecret_.reset(*psecret); // Ensure that we free() this structure later. + (*psecret)->len = len; + memcpy((*psecret)->data, plain_pass_.c_str(), len + 1); + break; + } + default: + LOG(DFATAL) << "SASL Client: Unexpected SASL callback type: " << id; + return SASL_BADPARAM; + } + + return SASL_OK; +} + +} // namespace rpc +} // namespace kudu http://git-wip-us.apache.org/repos/asf/kudu/blob/41c45523/src/kudu/rpc/client_negotiation.h ---------------------------------------------------------------------- diff --git a/src/kudu/rpc/client_negotiation.h b/src/kudu/rpc/client_negotiation.h new file mode 100644 index 0000000..2f01e56 --- /dev/null +++ b/src/kudu/rpc/client_negotiation.h @@ -0,0 +1,180 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +#ifndef KUDU_RPC_SASL_CLIENT_H +#define KUDU_RPC_SASL_CLIENT_H + +#include <set> +#include <string> +#include <vector> + +#include <sasl/sasl.h> + +#include "kudu/gutil/gscoped_ptr.h" +#include "kudu/rpc/rpc_header.pb.h" +#include "kudu/rpc/sasl_common.h" +#include "kudu/rpc/sasl_helper.h" +#include "kudu/util/monotime.h" +#include "kudu/util/net/socket.h" +#include "kudu/util/status.h" + +namespace kudu { +namespace rpc { + +using std::string; + +class NegotiatePB; +class NegotiatePB_SaslAuth; +class ResponseHeader; + +// Class for doing SASL negotiation with a SaslServer over a bidirectional socket. +// Operations on this class are NOT thread-safe. +class SaslClient { + public: + // Does not take ownership of the socket indicated by the fd. + SaslClient(string app_name, Socket* socket); + + // Enable PLAIN authentication. + // Must be called after Init(). + Status EnablePlain(const string& user, const string& pass); + + // Enable GSSAPI authentication. + // Call after Init(). + Status EnableGSSAPI(); + + // Returns mechanism negotiated by this connection. + // Must be called after Negotiate(). + SaslMechanism::Type negotiated_mechanism() const; + + // Returns the set of RPC system features supported by the remote server. + // Must be called after Negotiate(). + const std::set<RpcFeatureFlag>& server_features() const { + return server_features_; + } + + // Specify IP:port of local side of connection. + // Must be called before Init(). Required for some mechanisms. + void set_local_addr(const Sockaddr& addr); + + // Specify IP:port of remote side of connection. + // Must be called before Init(). Required for some mechanisms. + void set_remote_addr(const Sockaddr& addr); + + // Specify the fully-qualified domain name of the remote server. + // Must be called before Init(). Required for some mechanisms. + void set_server_fqdn(const string& domain_name); + + // Set deadline for connection negotiation. + void set_deadline(const MonoTime& deadline); + + // Get deadline for connection negotiation. + const MonoTime& deadline() const { return deadline_; } + + // Initialize a new SASL client. Must be called before Negotiate(). + // Returns OK on success, otherwise RuntimeError. + Status Init(const string& service_type); + + // Begin negotiation with the SASL server on the other side of the fd socket + // that this client was constructed with. + // Returns OK on success. + // Otherwise, it may return NotAuthorized, NotSupported, or another non-OK status. + Status Negotiate(); + + // SASL callback for plugin options, supported mechanisms, etc. + // Returns SASL_FAIL if the option is not handled, which does not fail the handshake. + int GetOptionCb(const char* plugin_name, const char* option, + const char** result, unsigned* len); + + // SASL callback for SASL_CB_USER, SASL_CB_AUTHNAME, SASL_CB_LANGUAGE + int SimpleCb(int id, const char** result, unsigned* len); + + // SASL callback for SASL_CB_PASS + int SecretCb(sasl_conn_t* conn, int id, sasl_secret_t** psecret); + + private: + // Encode and send the specified negotiate message to the server. + Status SendNegotiatePB(const NegotiatePB& msg); + + // Validate that header does not indicate an error, parse param_buf into response. + Status ParseNegotiatePB(const ResponseHeader& header, + const Slice& param_buf, + NegotiatePB* response); + + // Send an NEGOTIATE message to the server. + Status SendNegotiateMessage(); + + // Send an SASL_INITIATE message to the server. + Status SendInitiateMessage(const NegotiatePB_SaslAuth& auth, + const char* init_msg, unsigned init_msg_len); + + // Send a RESPONSE message to the server. + Status SendResponseMessage(const char* resp_msg, unsigned resp_msg_len); + + // Perform a client-side step of the SASL negotiation. + // Input is what came from the server. Output is what we will send back to the server. + // Returns: + // Status::OK if sasl_client_step returns SASL_OK. + // Status::Incomplete if sasl_client_step returns SASL_CONTINUE + // otherwise returns an appropriate error status. + Status DoSaslStep(const string& in, const char** out, unsigned* out_len); + + // Handle case when server sends NEGOTIATE response. + Status HandleNegotiateResponse(const NegotiatePB& response); + + // Handle case when server sends CHALLENGE response. + Status HandleChallengeResponse(const NegotiatePB& response); + + // Handle case when server sends SUCCESS response. + Status HandleSuccessResponse(const NegotiatePB& response); + + // Parse error status message from raw bytes of an ErrorStatusPB. + Status ParseError(const Slice& err_data); + + string app_name_; + Socket* sock_; + std::vector<sasl_callback_t> callbacks_; + // The SASL connection object. This is initialized in Init() and + // freed after Negotiate() completes (regardless whether it was successful). + gscoped_ptr<sasl_conn_t, SaslDeleter> sasl_conn_; + SaslHelper helper_; + + string plain_auth_user_; + string plain_pass_; + gscoped_ptr<sasl_secret_t, FreeDeleter> psecret_; + + // The set of features supported by the server. + std::set<RpcFeatureFlag> server_features_; + + SaslNegotiationState::Type client_state_; + + // The mechanism we negotiated with the server. + SaslMechanism::Type negotiated_mech_; + + // Intra-negotiation state. + bool nego_ok_; // During negotiation: did we get a SASL_OK response from the SASL library? + bool nego_response_expected_; // During negotiation: Are we waiting for a server response? + + // Negotiation timeout deadline. + MonoTime deadline_; + + DISALLOW_COPY_AND_ASSIGN(SaslClient); +}; + +} // namespace rpc +} // namespace kudu + +#endif // KUDU_RPC_SASL_CLIENT_H http://git-wip-us.apache.org/repos/asf/kudu/blob/41c45523/src/kudu/rpc/connection.cc ---------------------------------------------------------------------- diff --git a/src/kudu/rpc/connection.cc b/src/kudu/rpc/connection.cc index c7a87e9..2abf6ca 100644 --- a/src/kudu/rpc/connection.cc +++ b/src/kudu/rpc/connection.cc @@ -30,14 +30,13 @@ #include "kudu/gutil/map-util.h" #include "kudu/gutil/strings/human_readable.h" #include "kudu/gutil/strings/substitute.h" +#include "kudu/rpc/client_negotiation.h" #include "kudu/rpc/constants.h" #include "kudu/rpc/messenger.h" #include "kudu/rpc/reactor.h" #include "kudu/rpc/rpc_controller.h" #include "kudu/rpc/rpc_header.pb.h" #include "kudu/rpc/rpc_introspection.pb.h" -#include "kudu/rpc/sasl_client.h" -#include "kudu/rpc/sasl_server.h" #include "kudu/rpc/transfer.h" #include "kudu/security/ssl_factory.h" #include "kudu/security/ssl_socket.h" http://git-wip-us.apache.org/repos/asf/kudu/blob/41c45523/src/kudu/rpc/connection.h ---------------------------------------------------------------------- diff --git a/src/kudu/rpc/connection.h b/src/kudu/rpc/connection.h index bb2aa94..ef70483 100644 --- a/src/kudu/rpc/connection.h +++ b/src/kudu/rpc/connection.h @@ -30,10 +30,10 @@ #include "kudu/gutil/gscoped_ptr.h" #include "kudu/gutil/ref_counted.h" -#include "kudu/rpc/outbound_call.h" -#include "kudu/rpc/sasl_client.h" -#include "kudu/rpc/sasl_server.h" +#include "kudu/rpc/client_negotiation.h" #include "kudu/rpc/inbound_call.h" +#include "kudu/rpc/outbound_call.h" +#include "kudu/rpc/server_negotiation.h" #include "kudu/rpc/transfer.h" #include "kudu/util/monotime.h" #include "kudu/util/net/sockaddr.h" http://git-wip-us.apache.org/repos/asf/kudu/blob/41c45523/src/kudu/rpc/negotiation-test.cc ---------------------------------------------------------------------- diff --git a/src/kudu/rpc/negotiation-test.cc b/src/kudu/rpc/negotiation-test.cc new file mode 100644 index 0000000..2bb73e4 --- /dev/null +++ b/src/kudu/rpc/negotiation-test.cc @@ -0,0 +1,567 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 "kudu/rpc/rpc-test-base.h" + +#include <stdlib.h> +#include <sys/stat.h> + +#include <functional> +#include <memory> +#include <string> +#include <thread> + +#include <gtest/gtest.h> +#include <sasl/sasl.h> + +#include "kudu/gutil/gscoped_ptr.h" +#include "kudu/gutil/map-util.h" +#include "kudu/gutil/strings/substitute.h" +#include "kudu/rpc/client_negotiation.h" +#include "kudu/rpc/constants.h" +#include "kudu/rpc/sasl_common.h" +#include "kudu/rpc/server_negotiation.h" +#include "kudu/security/test/mini_kdc.h" +#include "kudu/util/monotime.h" +#include "kudu/util/net/sockaddr.h" +#include "kudu/util/net/socket.h" +#include "kudu/util/subprocess.h" + +using std::string; +using std::thread; + +// HACK: MIT Kerberos doesn't have any way of determining its version number, +// but the error messages in krb5-1.10 and earlier are broken due to +// a bug: http://krbdev.mit.edu/rt/Ticket/Display.html?id=6973 +// +// Since we don't have any way to explicitly figure out the version, we just +// look for this random macro which was added in 1.11 (the same version in which +// the above bug was fixed). +#ifndef KRB5_RESPONDER_QUESTION_PASSWORD +#define KRB5_VERSION_LE_1_10 +#endif + +DEFINE_bool(is_test_child, false, + "Used by tests which require clean processes. " + "See TestDisableInit."); + +namespace kudu { +namespace rpc { + +class TestSaslRpc : public RpcTestBase { + public: + virtual void SetUp() OVERRIDE { + RpcTestBase::SetUp(); + ASSERT_OK(SaslInit(kSaslAppName)); + } +}; + +// Test basic initialization of the objects. +TEST_F(TestSaslRpc, TestBasicInit) { + SaslServer server(kSaslAppName, nullptr); + server.EnablePlain(); + ASSERT_OK(server.Init(kSaslAppName)); + SaslClient client(kSaslAppName, nullptr); + client.EnablePlain("test", "test"); + ASSERT_OK(client.Init(kSaslAppName)); +} + +// A "Callable" that takes a Socket* param, for use with starting a thread. +// Can be used for SaslServer or SaslClient threads. +typedef std::function<void(Socket*)> SocketCallable; + +// Call Accept() on the socket, then pass the connection to the server runner +static void RunAcceptingDelegator(Socket* acceptor, + const SocketCallable& server_runner) { + Socket conn; + Sockaddr remote; + CHECK_OK(acceptor->Accept(&conn, &remote, 0)); + server_runner(&conn); +} + +// Set up a socket and run a SASL negotiation. +static void RunNegotiationTest(const SocketCallable& server_runner, + const SocketCallable& client_runner) { + Socket server_sock; + CHECK_OK(server_sock.Init(0)); + ASSERT_OK(server_sock.BindAndListen(Sockaddr(), 1)); + Sockaddr server_bind_addr; + ASSERT_OK(server_sock.GetSocketAddress(&server_bind_addr)); + thread server(RunAcceptingDelegator, &server_sock, server_runner); + + Socket client_sock; + CHECK_OK(client_sock.Init(0)); + ASSERT_OK(client_sock.Connect(server_bind_addr)); + thread client(client_runner, &client_sock); + + LOG(INFO) << "Waiting for test threads to terminate..."; + client.join(); + LOG(INFO) << "Client thread terminated."; + + // TODO(todd): if the client fails to negotiate, it doesn't + // always result in sending a nice error message to the + // other side. + client_sock.Close(); + + server.join(); + LOG(INFO) << "Server thread terminated."; +} + +//////////////////////////////////////////////////////////////////////////////// + +static void RunPlainNegotiationServer(Socket* conn) { + SaslServer sasl_server(kSaslAppName, conn); + CHECK_OK(sasl_server.EnablePlain()); + CHECK_OK(sasl_server.Init(kSaslAppName)); + CHECK_OK(sasl_server.Negotiate()); + CHECK(ContainsKey(sasl_server.client_features(), APPLICATION_FEATURE_FLAGS)); + CHECK_EQ("my-username", sasl_server.authenticated_user()); +} + +static void RunPlainNegotiationClient(Socket* conn) { + SaslClient sasl_client(kSaslAppName, conn); + CHECK_OK(sasl_client.EnablePlain("my-username", "ignored password")); + CHECK_OK(sasl_client.Init(kSaslAppName)); + CHECK_OK(sasl_client.Negotiate()); + CHECK(ContainsKey(sasl_client.server_features(), APPLICATION_FEATURE_FLAGS)); +} + +// Test SASL negotiation using the PLAIN mechanism over a socket. +TEST_F(TestSaslRpc, TestPlainNegotiation) { + RunNegotiationTest(RunPlainNegotiationServer, RunPlainNegotiationClient); +} + +//////////////////////////////////////////////////////////////////////////////// + + +template<class T> +using CheckerFunction = std::function<void(const Status&, T&)>; + +// Run GSSAPI negotiation from the server side. Runs +// 'post_check' after negotiation to verify the result. +static void RunGSSAPINegotiationServer( + Socket* conn, + const CheckerFunction<SaslServer>& post_check) { + SaslServer sasl_server(kSaslAppName, conn); + sasl_server.set_server_fqdn("127.0.0.1"); + CHECK_OK(sasl_server.EnableGSSAPI()); + CHECK_OK(sasl_server.Init(kSaslAppName)); + post_check(sasl_server.Negotiate(), sasl_server); +} + +// Run GSSAPI negotiation from the client side. Runs +// 'post_check' after negotiation to verify the result. +static void RunGSSAPINegotiationClient( + Socket* conn, + const CheckerFunction<SaslClient>& post_check) { + SaslClient sasl_client(kSaslAppName, conn); + sasl_client.set_server_fqdn("127.0.0.1"); + CHECK_OK(sasl_client.EnableGSSAPI()); + CHECK_OK(sasl_client.Init(kSaslAppName)); + post_check(sasl_client.Negotiate(), sasl_client); +} + +// Test configuring a client to allow but not require Kerberos/GSSAPI, +// and connect to a server which requires Kerberos/GSSAPI. +// +// They should negotiate to use Kerberos/GSSAPI. +TEST_F(TestSaslRpc, TestRestrictiveServer_NonRestrictiveClient) { + MiniKdc kdc; + ASSERT_OK(kdc.Start()); + + // Create the server principal and keytab. + string kt_path; + ASSERT_OK(kdc.CreateServiceKeytab("kudu/127.0.0.1", &kt_path)); + CHECK_ERR(setenv("KRB5_KTNAME", kt_path.c_str(), 1 /*replace*/)); + + // Create and kinit as a client user. + ASSERT_OK(kdc.CreateUserPrincipal("testuser")); + ASSERT_OK(kdc.Kinit("testuser")); + ASSERT_OK(kdc.SetKrb5Environment()); + + // Authentication should now succeed on both sides. + RunNegotiationTest( + std::bind(RunGSSAPINegotiationServer, std::placeholders::_1, + [](const Status& s, SaslServer& server) { + CHECK_OK(s); + CHECK_EQ(SaslMechanism::GSSAPI, server.negotiated_mechanism()); + CHECK_EQ("testuser", server.authenticated_user()); + }), + [](Socket* conn) { + SaslClient sasl_client(kSaslAppName, conn); + sasl_client.set_server_fqdn("127.0.0.1"); + // The client enables both PLAIN and GSSAPI. + CHECK_OK(sasl_client.EnablePlain("foo", "bar")); + CHECK_OK(sasl_client.EnableGSSAPI()); + CHECK_OK(sasl_client.Init(kSaslAppName)); + CHECK_OK(sasl_client.Negotiate()); + CHECK_EQ(SaslMechanism::GSSAPI, sasl_client.negotiated_mechanism()); + }); +} + +// Test configuring a client to only support PLAIN, and a server which +// only supports GSSAPI. This would happen, for example, if an old Kudu +// client tries to talk to a secure-only cluster. +TEST_F(TestSaslRpc, TestNoMatchingMechanisms) { + MiniKdc kdc; + ASSERT_OK(kdc.Start()); + + // Create the server principal and keytab. + string kt_path; + ASSERT_OK(kdc.CreateServiceKeytab("kudu/localhost", &kt_path)); + CHECK_ERR(setenv("KRB5_KTNAME", kt_path.c_str(), 1 /*replace*/)); + + RunNegotiationTest( + std::bind(RunGSSAPINegotiationServer, std::placeholders::_1, + [](const Status& s, SaslServer& server) { + // The client fails to find a matching mechanism and + // doesn't send any failure message to the server. + // Instead, it just disconnects. + // + // TODO(todd): this could produce a better message! + ASSERT_STR_CONTAINS(s.ToString(), "got EOF from remote"); + }), + [](Socket* conn) { + SaslClient sasl_client(kSaslAppName, conn); + sasl_client.set_server_fqdn("127.0.0.1"); + // The client enables both PLAIN and GSSAPI. + CHECK_OK(sasl_client.EnablePlain("foo", "bar")); + CHECK_OK(sasl_client.Init(kSaslAppName)); + Status s = sasl_client.Negotiate(); + ASSERT_STR_CONTAINS(s.ToString(), "client was missing the required SASL module"); + }); +} + +// Test SASL negotiation using the GSSAPI (kerberos) mechanism over a socket. +TEST_F(TestSaslRpc, TestGSSAPINegotiation) { + MiniKdc kdc; + ASSERT_OK(kdc.Start()); + + // Create the server principal and keytab. + string kt_path; + ASSERT_OK(kdc.CreateServiceKeytab("kudu/127.0.0.1", &kt_path)); + CHECK_ERR(setenv("KRB5_KTNAME", kt_path.c_str(), 1 /*replace*/)); + + // Create and kinit as a client user. + ASSERT_OK(kdc.CreateUserPrincipal("testuser")); + ASSERT_OK(kdc.Kinit("testuser")); + ASSERT_OK(kdc.SetKrb5Environment()); + + // Authentication should succeed on both sides. + RunNegotiationTest( + std::bind(RunGSSAPINegotiationServer, std::placeholders::_1, + [](const Status& s, SaslServer& server) { + CHECK_OK(s); + CHECK_EQ(SaslMechanism::GSSAPI, server.negotiated_mechanism()); + CHECK_EQ("testuser", server.authenticated_user()); + }), + std::bind(RunGSSAPINegotiationClient, std::placeholders::_1, + [](const Status& s, SaslClient& client) { + CHECK_OK(s); + CHECK_EQ(SaslMechanism::GSSAPI, client.negotiated_mechanism()); + })); +} + +#ifndef __APPLE__ +// Test invalid SASL negotiations using the GSSAPI (kerberos) mechanism over a socket. +// This test is ignored on macOS because the system Kerberos implementation +// (Heimdal) caches the non-existence of client credentials, which causes futher +// tests to fail. +TEST_F(TestSaslRpc, TestGSSAPIInvalidNegotiation) { + MiniKdc kdc; + ASSERT_OK(kdc.Start()); + + // Try to negotiate with no krb5 credentials on either side. It should fail on both + // sides. + RunNegotiationTest( + std::bind(RunGSSAPINegotiationServer, std::placeholders::_1, + [](const Status& s, SaslServer& server) { + // The client notices there are no credentials and + // doesn't send any failure message to the server. + // Instead, it just disconnects. + // + // TODO(todd): it might be preferable to have the server + // fail to start if it has no valid keytab. + CHECK(s.IsNetworkError()); + }), + std::bind(RunGSSAPINegotiationClient, std::placeholders::_1, + [](const Status& s, SaslClient& client) { + CHECK(s.IsNotAuthorized()); +#ifndef KRB5_VERSION_LE_1_10 + CHECK_GT(s.ToString().find("No Kerberos credentials available"), 0); +#endif + })); + + + // Create the server principal and keytab. + string kt_path; + ASSERT_OK(kdc.CreateServiceKeytab("kudu/127.0.0.1", &kt_path)); + CHECK_ERR(setenv("KRB5_KTNAME", kt_path.c_str(), 1 /*replace*/)); + + // Try to negotiate with no krb5 credentials on the client. It should fail on both + // sides. + RunNegotiationTest( + std::bind(RunGSSAPINegotiationServer, std::placeholders::_1, + [](const Status& s, SaslServer& server) { + // The client notices there are no credentials and + // doesn't send any failure message to the server. + // Instead, it just disconnects. + CHECK(s.IsNetworkError()); + }), + std::bind(RunGSSAPINegotiationClient, std::placeholders::_1, + [](const Status& s, SaslClient& client) { + CHECK(s.IsNotAuthorized()); +#ifndef KRB5_VERSION_LE_1_10 + CHECK_EQ(s.message().ToString(), "No Kerberos credentials available"); +#endif + })); + + // Create and kinit as a client user. + ASSERT_OK(kdc.CreateUserPrincipal("testuser")); + ASSERT_OK(kdc.Kinit("testuser")); + ASSERT_OK(kdc.SetKrb5Environment()); + + // Change the server's keytab file so that it has inappropriate + // credentials. + // Authentication should now fail. + ASSERT_OK(kdc.CreateServiceKeytab("otherservice/127.0.0.1", &kt_path)); + CHECK_ERR(setenv("KRB5_KTNAME", kt_path.c_str(), 1 /*replace*/)); + + RunNegotiationTest( + std::bind(RunGSSAPINegotiationServer, std::placeholders::_1, + [](const Status& s, SaslServer& server) { + CHECK(s.IsNotAuthorized()); +#ifndef KRB5_VERSION_LE_1_10 + ASSERT_STR_CONTAINS(s.ToString(), + "No key table entry found matching kudu/127.0.0.1"); +#endif + }), + std::bind(RunGSSAPINegotiationClient, std::placeholders::_1, + [](const Status& s, SaslClient& client) { + CHECK(s.IsNotAuthorized()); +#ifndef KRB5_VERSION_LE_1_10 + ASSERT_STR_CONTAINS(s.ToString(), + "No key table entry found matching kudu/127.0.0.1"); +#endif + })); +} +#endif + +#ifndef __APPLE__ +// Test that the pre-flight check for servers requiring Kerberos provides +// nice error messages for missing or bad keytabs. +// +// This is ignored on macOS because the system Kerberos implementation does not +// fail the preflight check when the keytab is inaccessible, probably because +// the preflight check passes a 0-length token. +TEST_F(TestSaslRpc, TestPreflight) { + // Try pre-flight with no keytab. + Status s = SaslServer::PreflightCheckGSSAPI(kSaslAppName); + ASSERT_FALSE(s.ok()); +#ifndef KRB5_VERSION_LE_1_10 + ASSERT_STR_MATCHES(s.ToString(), "Key table file.*not found"); +#endif + // Try with a valid krb5 environment and keytab. + MiniKdc kdc; + ASSERT_OK(kdc.Start()); + ASSERT_OK(kdc.SetKrb5Environment()); + string kt_path; + ASSERT_OK(kdc.CreateServiceKeytab("kudu/127.0.0.1", &kt_path)); + CHECK_ERR(setenv("KRB5_KTNAME", kt_path.c_str(), 1 /*replace*/)); + + ASSERT_OK(SaslServer::PreflightCheckGSSAPI(kSaslAppName)); + + // Try with an inaccessible keytab. + CHECK_ERR(chmod(kt_path.c_str(), 0000)); + s = SaslServer::PreflightCheckGSSAPI(kSaslAppName); + ASSERT_FALSE(s.ok()); +#ifndef KRB5_VERSION_LE_1_10 + ASSERT_STR_MATCHES(s.ToString(), "error accessing keytab: Permission denied"); +#endif + CHECK_ERR(unlink(kt_path.c_str())); + + // Try with a keytab that has the wrong credentials. + ASSERT_OK(kdc.CreateServiceKeytab("wrong-service/127.0.0.1", &kt_path)); + CHECK_ERR(setenv("KRB5_KTNAME", kt_path.c_str(), 1 /*replace*/)); + s = SaslServer::PreflightCheckGSSAPI(kSaslAppName); + ASSERT_FALSE(s.ok()); +#ifndef KRB5_VERSION_LE_1_10 + ASSERT_STR_MATCHES(s.ToString(), "No key table entry found matching kudu/.*"); +#endif +} +#endif + +//////////////////////////////////////////////////////////////////////////////// + +static void RunTimeoutExpectingServer(Socket* conn) { + SaslServer sasl_server(kSaslAppName, conn); + CHECK_OK(sasl_server.EnablePlain()); + CHECK_OK(sasl_server.Init(kSaslAppName)); + Status s = sasl_server.Negotiate(); + ASSERT_TRUE(s.IsNetworkError()) << "Expected client to time out and close the connection. Got: " + << s.ToString(); +} + +static void RunTimeoutNegotiationClient(Socket* sock) { + SaslClient sasl_client(kSaslAppName, sock); + CHECK_OK(sasl_client.EnablePlain("test", "test")); + CHECK_OK(sasl_client.Init(kSaslAppName)); + MonoTime deadline = MonoTime::Now() - MonoDelta::FromMilliseconds(100L); + sasl_client.set_deadline(deadline); + Status s = sasl_client.Negotiate(); + ASSERT_TRUE(s.IsTimedOut()) << "Expected timeout! Got: " << s.ToString(); + CHECK_OK(sock->Shutdown(true, true)); +} + +// Ensure that the client times out. +TEST_F(TestSaslRpc, TestClientTimeout) { + RunNegotiationTest(RunTimeoutExpectingServer, RunTimeoutNegotiationClient); +} + +//////////////////////////////////////////////////////////////////////////////// + +static void RunTimeoutNegotiationServer(Socket* sock) { + SaslServer sasl_server(kSaslAppName, sock); + CHECK_OK(sasl_server.EnablePlain()); + CHECK_OK(sasl_server.Init(kSaslAppName)); + MonoTime deadline = MonoTime::Now() - MonoDelta::FromMilliseconds(100L); + sasl_server.set_deadline(deadline); + Status s = sasl_server.Negotiate(); + ASSERT_TRUE(s.IsTimedOut()) << "Expected timeout! Got: " << s.ToString(); + CHECK_OK(sock->Close()); +} + +static void RunTimeoutExpectingClient(Socket* conn) { + SaslClient sasl_client(kSaslAppName, conn); + CHECK_OK(sasl_client.EnablePlain("test", "test")); + CHECK_OK(sasl_client.Init(kSaslAppName)); + Status s = sasl_client.Negotiate(); + ASSERT_TRUE(s.IsNetworkError()) << "Expected server to time out and close the connection. Got: " + << s.ToString(); +} + +// Ensure that the server times out. +TEST_F(TestSaslRpc, TestServerTimeout) { + RunNegotiationTest(RunTimeoutNegotiationServer, RunTimeoutExpectingClient); +} + +//////////////////////////////////////////////////////////////////////////////// + +// This suite of tests ensure that applications that embed the Kudu client are +// able to externally handle the initialization of SASL. See KUDU-1749 and +// IMPALA-4497 for context. +// +// The tests are a bit tricky because the initialization of SASL is static state +// that we can't easily clear/reset between test cases. So, each test invokes +// itself as a subprocess with the appropriate --gtest_filter line as well as a +// special flag to indicate that it is the test child running. +class TestDisableInit : public KuduTest { + protected: + // Run the lambda 'f' in a newly-started process, capturing its stderr + // into 'stderr'. + template<class TestFunc> + void DoTest(const TestFunc& f, string* stderr = nullptr) { + if (FLAGS_is_test_child) { + f(); + return; + } + + // Invoke the currently-running test case in a new subprocess. + string filter_flag = strings::Substitute("--gtest_filter=$0.$1", + CURRENT_TEST_CASE_NAME(), CURRENT_TEST_NAME()); + string executable_path; + CHECK_OK(env_->GetExecutablePath(&executable_path)); + string stdout; + Status s = Subprocess::Call({ executable_path, "test", filter_flag, "--is_test_child" }, + "" /* stdin */, + &stdout, + stderr); + ASSERT_TRUE(s.ok()) << "Test failed: " << stdout; + } +}; + +// Test disabling SASL but not actually properly initializing it before usage. +TEST_F(TestDisableInit, TestDisableSasl_NotInitialized) { + DoTest([]() { + CHECK_OK(DisableSaslInitialization()); + Status s = SaslInit("kudu"); + ASSERT_STR_CONTAINS(s.ToString(), "was disabled, but SASL was not externally initialized"); + }); +} + +// Test disabling SASL with proper initialization by some other app. +TEST_F(TestDisableInit, TestDisableSasl_Good) { + DoTest([]() { + rpc::internal::SaslSetMutex(); + sasl_client_init(NULL); + CHECK_OK(DisableSaslInitialization()); + ASSERT_OK(SaslInit("kudu")); + }); +} + +// Test a client which inits SASL itself but doesn't remember to disable Kudu's +// SASL initialization. +TEST_F(TestDisableInit, TestMultipleSaslInit) { + string stderr; + DoTest([]() { + rpc::internal::SaslSetMutex(); + sasl_client_init(NULL); + ASSERT_OK(SaslInit("kudu")); + }, &stderr); + // If we are the parent, we should see the warning from the child that it automatically + // skipped initialization because it detected that it was already initialized. + if (!FLAGS_is_test_child) { + ASSERT_STR_CONTAINS(stderr, "Skipping initialization"); + } +} + +// We are not able to detect mutexes not being set with the macOS version of libsasl. +#ifndef __APPLE__ +// Test disabling SASL but not remembering to initialize the SASL mutex support. This +// should succeed but generate a warning. +TEST_F(TestDisableInit, TestDisableSasl_NoMutexImpl) { + string stderr; + DoTest([]() { + sasl_client_init(NULL); + CHECK_OK(DisableSaslInitialization()); + ASSERT_OK(SaslInit("kudu")); + }, &stderr); + // If we are the parent, we should see the warning from the child. + if (!FLAGS_is_test_child) { + ASSERT_STR_CONTAINS(stderr, "not provided with a mutex implementation"); + } +} + +// Test a client which inits SASL itself but doesn't remember to disable Kudu's +// SASL initialization. +TEST_F(TestDisableInit, TestMultipleSaslInit_NoMutexImpl) { + string stderr; + DoTest([]() { + sasl_client_init(NULL); + ASSERT_OK(SaslInit("kudu")); + }, &stderr); + // If we are the parent, we should see the warning from the child that it automatically + // skipped initialization because it detected that it was already initialized. + if (!FLAGS_is_test_child) { + ASSERT_STR_CONTAINS(stderr, "Skipping initialization"); + ASSERT_STR_CONTAINS(stderr, "not provided with a mutex implementation"); + } +} +#endif + +} // namespace rpc +} // namespace kudu http://git-wip-us.apache.org/repos/asf/kudu/blob/41c45523/src/kudu/rpc/negotiation.cc ---------------------------------------------------------------------- diff --git a/src/kudu/rpc/negotiation.cc b/src/kudu/rpc/negotiation.cc index e42a884..859016c 100644 --- a/src/kudu/rpc/negotiation.cc +++ b/src/kudu/rpc/negotiation.cc @@ -28,12 +28,12 @@ #include "kudu/gutil/stringprintf.h" #include "kudu/gutil/strings/substitute.h" #include "kudu/rpc/blocking_ops.h" +#include "kudu/rpc/client_negotiation.h" #include "kudu/rpc/connection.h" #include "kudu/rpc/reactor.h" #include "kudu/rpc/rpc_header.pb.h" -#include "kudu/rpc/sasl_client.h" #include "kudu/rpc/sasl_common.h" -#include "kudu/rpc/sasl_server.h" +#include "kudu/rpc/server_negotiation.h" #include "kudu/util/errno.h" #include "kudu/util/flag_tags.h" #include "kudu/util/status.h" http://git-wip-us.apache.org/repos/asf/kudu/blob/41c45523/src/kudu/rpc/reactor.cc ---------------------------------------------------------------------- diff --git a/src/kudu/rpc/reactor.cc b/src/kudu/rpc/reactor.cc index 0b2b19b..f178a2d 100644 --- a/src/kudu/rpc/reactor.cc +++ b/src/kudu/rpc/reactor.cc @@ -31,13 +31,13 @@ #include "kudu/gutil/ref_counted.h" #include "kudu/gutil/stringprintf.h" +#include "kudu/rpc/client_negotiation.h" #include "kudu/rpc/connection.h" #include "kudu/rpc/messenger.h" #include "kudu/rpc/negotiation.h" #include "kudu/rpc/rpc_controller.h" #include "kudu/rpc/rpc_introspection.pb.h" -#include "kudu/rpc/sasl_client.h" -#include "kudu/rpc/sasl_server.h" +#include "kudu/rpc/server_negotiation.h" #include "kudu/rpc/transfer.h" #include "kudu/security/ssl_factory.h" #include "kudu/security/ssl_socket.h" http://git-wip-us.apache.org/repos/asf/kudu/blob/41c45523/src/kudu/rpc/sasl_client.cc ---------------------------------------------------------------------- diff --git a/src/kudu/rpc/sasl_client.cc b/src/kudu/rpc/sasl_client.cc deleted file mode 100644 index c88e3cc..0000000 --- a/src/kudu/rpc/sasl_client.cc +++ /dev/null @@ -1,521 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you 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 "kudu/rpc/sasl_client.h" - -#include <string.h> - -#include <map> -#include <set> -#include <string> - -#include <glog/logging.h> -#include <sasl/sasl.h> - -#include "kudu/gutil/endian.h" -#include "kudu/gutil/map-util.h" -#include "kudu/gutil/stl_util.h" -#include "kudu/gutil/stringprintf.h" -#include "kudu/gutil/strings/join.h" -#include "kudu/gutil/strings/substitute.h" -#include "kudu/gutil/strings/util.h" -#include "kudu/rpc/blocking_ops.h" -#include "kudu/rpc/constants.h" -#include "kudu/rpc/rpc_header.pb.h" -#include "kudu/rpc/sasl_common.h" -#include "kudu/rpc/sasl_helper.h" -#include "kudu/rpc/serialization.h" -#include "kudu/util/faststring.h" -#include "kudu/util/net/sockaddr.h" -#include "kudu/util/net/socket.h" -#include "kudu/util/scoped_cleanup.h" -#include "kudu/util/trace.h" - -namespace kudu { -namespace rpc { - -using std::map; -using std::set; -using std::string; - -static int SaslClientGetoptCb(void* sasl_client, const char* plugin_name, const char* option, - const char** result, unsigned* len) { - return static_cast<SaslClient*>(sasl_client) - ->GetOptionCb(plugin_name, option, result, len); -} - -static int SaslClientSimpleCb(void *sasl_client, int id, - const char **result, unsigned *len) { - return static_cast<SaslClient*>(sasl_client)->SimpleCb(id, result, len); -} - -static int SaslClientSecretCb(sasl_conn_t* conn, void *sasl_client, int id, - sasl_secret_t** psecret) { - return static_cast<SaslClient*>(sasl_client)->SecretCb(conn, id, psecret); -} - -// Return an appropriately-typed Status object based on an ErrorStatusPB returned -// from an Error RPC. -// In case there is no relevant Status type, return a RuntimeError. -static Status StatusFromRpcError(const ErrorStatusPB& error) { - DCHECK(error.IsInitialized()) << "Error status PB must be initialized"; - if (PREDICT_FALSE(!error.has_code())) { - return Status::RuntimeError(error.message()); - } - string code_name = ErrorStatusPB::RpcErrorCodePB_Name(error.code()); - switch (error.code()) { - case ErrorStatusPB_RpcErrorCodePB_FATAL_UNAUTHORIZED: - return Status::NotAuthorized(code_name, error.message()); - default: - return Status::RuntimeError(code_name, error.message()); - } -} - -SaslClient::SaslClient(string app_name, Socket* socket) - : app_name_(std::move(app_name)), - sock_(socket), - helper_(SaslHelper::CLIENT), - client_state_(SaslNegotiationState::NEW), - negotiated_mech_(SaslMechanism::INVALID), - deadline_(MonoTime::Max()) { - callbacks_.push_back(SaslBuildCallback(SASL_CB_GETOPT, - reinterpret_cast<int (*)()>(&SaslClientGetoptCb), this)); - callbacks_.push_back(SaslBuildCallback(SASL_CB_AUTHNAME, - reinterpret_cast<int (*)()>(&SaslClientSimpleCb), this)); - callbacks_.push_back(SaslBuildCallback(SASL_CB_PASS, - reinterpret_cast<int (*)()>(&SaslClientSecretCb), this)); - callbacks_.push_back(SaslBuildCallback(SASL_CB_LIST_END, nullptr, nullptr)); -} - -Status SaslClient::EnablePlain(const string& user, const string& pass) { - DCHECK_EQ(client_state_, SaslNegotiationState::NEW); - RETURN_NOT_OK(helper_.EnablePlain()); - plain_auth_user_ = user; - plain_pass_ = pass; - return Status::OK(); -} - -Status SaslClient::EnableGSSAPI() { - DCHECK_EQ(client_state_, SaslNegotiationState::NEW); - return helper_.EnableGSSAPI(); -} - -SaslMechanism::Type SaslClient::negotiated_mechanism() const { - DCHECK_EQ(client_state_, SaslNegotiationState::NEGOTIATED); - return negotiated_mech_; -} - -void SaslClient::set_local_addr(const Sockaddr& addr) { - DCHECK_EQ(client_state_, SaslNegotiationState::NEW); - helper_.set_local_addr(addr); -} - -void SaslClient::set_remote_addr(const Sockaddr& addr) { - DCHECK_EQ(client_state_, SaslNegotiationState::NEW); - helper_.set_remote_addr(addr); -} - -void SaslClient::set_server_fqdn(const string& domain_name) { - DCHECK_EQ(client_state_, SaslNegotiationState::NEW); - helper_.set_server_fqdn(domain_name); -} - -void SaslClient::set_deadline(const MonoTime& deadline) { - DCHECK_NE(client_state_, SaslNegotiationState::NEGOTIATED); - deadline_ = deadline; -} - -// calls sasl_client_init() and sasl_client_new() -Status SaslClient::Init(const string& service_type) { - RETURN_NOT_OK(SaslInit(app_name_.c_str())); - - // Ensure we are not called more than once. - if (client_state_ != SaslNegotiationState::NEW) { - return Status::IllegalState("Init() may only be called once per SaslClient object."); - } - - // TODO: Support security flags. - unsigned secflags = 0; - - sasl_conn_t* sasl_conn = nullptr; - Status s = WrapSaslCall(nullptr /* no conn */, [&]() { - return sasl_client_new( - service_type.c_str(), // Registered name of the service using SASL. Required. - helper_.server_fqdn(), // The fully qualified domain name of the remote server. - helper_.local_addr_string(), // Local and remote IP address strings. (NULL disables - helper_.remote_addr_string(), // mechanisms which require this info.) - &callbacks_[0], // Connection-specific callbacks. - secflags, // Security flags. - &sasl_conn); - }); - if (!s.ok()) { - return Status::RuntimeError("Unable to create new SASL client", - s.message()); - } - sasl_conn_.reset(sasl_conn); - - client_state_ = SaslNegotiationState::INITIALIZED; - return Status::OK(); -} - -Status SaslClient::Negotiate() { - // After negotiation, we no longer need the SASL library object, so - // may as well free its memory since the connection may be long-lived. - // Additionally, this works around a SEGV seen at process shutdown time: - // if we still have SASL objects retained by Reactor when the process - // is exiting, the SASL libraries may start destructing global state - // and cause a crash when we sasl_dispose the connection. - auto cleanup = MakeScopedCleanup([&]() { - sasl_conn_.reset(); - }); - TRACE("Called SaslClient::Negotiate()"); - - // Ensure we called exactly once, and in the right order. - if (client_state_ == SaslNegotiationState::NEW) { - return Status::IllegalState("SaslClient: Init() must be called before calling Negotiate()"); - } else if (client_state_ == SaslNegotiationState::NEGOTIATED) { - return Status::IllegalState("SaslClient: Negotiate() may only be called once per object."); - } - - // Ensure we can use blocking calls on the socket during negotiation. - RETURN_NOT_OK(EnsureBlockingMode(sock_)); - - // Start by asking the server for a list of available auth mechanisms. - RETURN_NOT_OK(SendNegotiateMessage()); - - faststring recv_buf; - nego_ok_ = false; - - // We set nego_ok_ = true when the SASL library returns SASL_OK to us. - // We set nego_response_expected_ = true each time we send a request to the server. - while (!nego_ok_ || nego_response_expected_) { - ResponseHeader header; - Slice param_buf; - RETURN_NOT_OK(ReceiveFramedMessageBlocking(sock_, &recv_buf, &header, ¶m_buf, deadline_)); - nego_response_expected_ = false; - - NegotiatePB response; - RETURN_NOT_OK(ParseNegotiatePB(header, param_buf, &response)); - - switch (response.step()) { - // NEGOTIATE: Server has sent us its list of supported SASL mechanisms. - case NegotiatePB::NEGOTIATE: - RETURN_NOT_OK(HandleNegotiateResponse(response)); - break; - - // SASL_CHALLENGE: Server sent us a follow-up to an SASL_INITIATE or SASL_RESPONSE request. - case NegotiatePB::SASL_CHALLENGE: - RETURN_NOT_OK(HandleChallengeResponse(response)); - break; - - // SASL_SUCCESS: Server has accepted our authentication request. Negotiation successful. - case NegotiatePB::SASL_SUCCESS: - RETURN_NOT_OK(HandleSuccessResponse(response)); - break; - - // Client sent us some unsupported SASL response. - default: - LOG(ERROR) << "SASL Client: Received unsupported response from server"; - return Status::InvalidArgument("RPC client doesn't support Negotiate step", - NegotiatePB::NegotiateStep_Name(response.step())); - } - } - - TRACE("SASL Client: Successful negotiation"); - client_state_ = SaslNegotiationState::NEGOTIATED; - return Status::OK(); -} - -Status SaslClient::SendNegotiatePB(const NegotiatePB& msg) { - DCHECK_NE(client_state_, SaslNegotiationState::NEW) - << "Must not send Negotiate messages before calling Init()"; - DCHECK_NE(client_state_, SaslNegotiationState::NEGOTIATED) - << "Must not send Negotiate messages after negotiation succeeds"; - - // Create header with SASL-specific callId - RequestHeader header; - header.set_call_id(kNegotiateCallId); - return helper_.SendNegotiatePB(sock_, header, msg, deadline_); -} - -Status SaslClient::ParseNegotiatePB(const ResponseHeader& header, - const Slice& param_buf, - NegotiatePB* response) { - RETURN_NOT_OK(helper_.SanityCheckNegotiateCallId(header.call_id())); - - if (header.is_error()) { - return ParseError(param_buf); - } - - return helper_.ParseNegotiatePB(param_buf, response); -} - -Status SaslClient::SendNegotiateMessage() { - NegotiatePB msg; - msg.set_step(NegotiatePB::NEGOTIATE); - - // Advertise our supported features. - for (RpcFeatureFlag feature : kSupportedClientRpcFeatureFlags) { - msg.add_supported_features(feature); - } - - TRACE("SASL Client: Sending NEGOTIATE request to server."); - RETURN_NOT_OK(SendNegotiatePB(msg)); - nego_response_expected_ = true; - return Status::OK(); -} - -Status SaslClient::SendInitiateMessage(const NegotiatePB_SaslAuth& auth, - const char* init_msg, unsigned init_msg_len) { - NegotiatePB msg; - msg.set_step(NegotiatePB::SASL_INITIATE); - msg.mutable_token()->assign(init_msg, init_msg_len); - msg.add_auths()->CopyFrom(auth); - TRACE("SASL Client: Sending SASL_INITIATE request to server."); - RETURN_NOT_OK(SendNegotiatePB(msg)); - nego_response_expected_ = true; - return Status::OK(); -} - -Status SaslClient::SendResponseMessage(const char* resp_msg, unsigned resp_msg_len) { - NegotiatePB reply; - reply.set_step(NegotiatePB::SASL_RESPONSE); - reply.mutable_token()->assign(resp_msg, resp_msg_len); - TRACE("SASL Client: Sending SASL_RESPONSE request to server."); - RETURN_NOT_OK(SendNegotiatePB(reply)); - nego_response_expected_ = true; - return Status::OK(); -} - -Status SaslClient::DoSaslStep(const string& in, const char** out, unsigned* out_len) { - TRACE("SASL Client: Calling sasl_client_step()"); - Status s = WrapSaslCall(sasl_conn_.get(), [&]() { - return sasl_client_step(sasl_conn_.get(), in.c_str(), in.length(), nullptr, out, out_len); - }); - if (s.ok()) { - nego_ok_ = true; - } - return s; -} - -Status SaslClient::HandleNegotiateResponse(const NegotiatePB& response) { - TRACE("SASL Client: Received NEGOTIATE response from server"); - // Fill in the set of features supported by the server. - for (int flag : response.supported_features()) { - // We only add the features that our local build knows about. - RpcFeatureFlag feature_flag = RpcFeatureFlag_IsValid(flag) ? - static_cast<RpcFeatureFlag>(flag) : UNKNOWN; - if (ContainsKey(kSupportedClientRpcFeatureFlags, feature_flag)) { - server_features_.insert(feature_flag); - } - } - - // Build a map of the mechanisms offered by the server. - const set<string>& local_mechs = helper_.LocalMechs(); - set<string> server_mechs; - map<string, NegotiatePB::SaslAuth> server_mech_map; - for (const NegotiatePB::SaslAuth& auth : response.auths()) { - const auto& mech = auth.mechanism(); - server_mech_map[mech] = auth; - server_mechs.insert(mech); - } - // Determine which server mechs are also enabled by the client. - // Cyrus SASL 2.1.25 and later supports doing this set intersection via - // the 'client_mech_list' option, but that version is not available on - // RHEL 6, so we have to do it manually. - set<string> matching_mechs = STLSetIntersection(local_mechs, server_mechs); - - if (matching_mechs.empty() && - ContainsKey(server_mechs, kSaslMechGSSAPI) && - !ContainsKey(local_mechs, kSaslMechGSSAPI)) { - return Status::NotAuthorized("server requires GSSAPI (Kerberos) authentication and " - "client was missing the required SASL module"); - } - - string matching_mechs_str = JoinElements(matching_mechs, " "); - TRACE("SASL Client: Matching mech list: $0", matching_mechs_str); - - const char* init_msg = nullptr; - unsigned init_msg_len = 0; - const char* negotiated_mech = nullptr; - - /* select a mechanism for a connection - * mechlist -- mechanisms server has available (punctuation ignored) - * output: - * prompt_need -- on SASL_INTERACT, list of prompts needed to continue - * clientout -- the initial client response to send to the server - * mech -- set to mechanism name - * - * Returns: - * SASL_OK -- success - * SASL_CONTINUE -- negotiation required - * SASL_NOMEM -- not enough memory - * SASL_NOMECH -- no mechanism meets requested properties - * SASL_INTERACT -- user interaction needed to fill in prompt_need list - */ - TRACE("SASL Client: Calling sasl_client_start()"); - Status s = WrapSaslCall(sasl_conn_.get(), [&]() { - return sasl_client_start( - sasl_conn_.get(), // The SASL connection context created by init() - matching_mechs_str.c_str(), // The list of mechanisms to negotiate. - nullptr, // Disables INTERACT return if NULL. - &init_msg, // Filled in on success. - &init_msg_len, // Filled in on success. - &negotiated_mech); // Filled in on success. - }); - - if (s.ok()) { - nego_ok_ = true; - } else if (!s.IsIncomplete()) { - return s; - } - - // The server matched one of our mechanisms. - NegotiatePB::SaslAuth* auth = FindOrNull(server_mech_map, negotiated_mech); - if (PREDICT_FALSE(auth == nullptr)) { - return Status::IllegalState("Unable to find auth in map, unexpected error", negotiated_mech); - } - negotiated_mech_ = SaslMechanism::value_of(negotiated_mech); - - RETURN_NOT_OK(SendInitiateMessage(*auth, init_msg, init_msg_len)); - return Status::OK(); -} - -Status SaslClient::HandleChallengeResponse(const NegotiatePB& response) { - TRACE("SASL Client: Received SASL_CHALLENGE response from server"); - if (PREDICT_FALSE(nego_ok_)) { - LOG(DFATAL) << "Server sent SASL_CHALLENGE response after client library returned SASL_OK"; - } - - if (PREDICT_FALSE(!response.has_token())) { - return Status::InvalidArgument("No token in SASL_CHALLENGE response from server"); - } - - const char* out = nullptr; - unsigned out_len = 0; - Status s = DoSaslStep(response.token(), &out, &out_len); - if (!s.ok() && !s.IsIncomplete()) { - return s; - } - RETURN_NOT_OK(SendResponseMessage(out, out_len)); - return Status::OK(); -} - -Status SaslClient::HandleSuccessResponse(const NegotiatePB& response) { - TRACE("SASL Client: Received SASL_SUCCESS response from server"); - if (!nego_ok_) { - const char* out = nullptr; - unsigned out_len = 0; - Status s = DoSaslStep(response.token(), &out, &out_len); - if (s.IsIncomplete()) { - return Status::IllegalState("Server indicated successful authentication, but client " - "was not complete"); - } - RETURN_NOT_OK(s); - if (out_len > 0) { - return Status::IllegalState("SASL client library generated spurious token after SASL_SUCCESS", - string(out, out_len)); - } - CHECK(nego_ok_); - } - return Status::OK(); -} - -// Parse error status message from raw bytes of an ErrorStatusPB. -Status SaslClient::ParseError(const Slice& err_data) { - ErrorStatusPB error; - if (!error.ParseFromArray(err_data.data(), err_data.size())) { - return Status::IOError("Invalid error response, missing fields", - error.InitializationErrorString()); - } - Status s = StatusFromRpcError(error); - TRACE("SASL Client: Received error response from server: $0", s.ToString()); - return s; -} - -int SaslClient::GetOptionCb(const char* plugin_name, const char* option, - const char** result, unsigned* len) { - return helper_.GetOptionCb(plugin_name, option, result, len); -} - -// Used for PLAIN. -// SASL callback for SASL_CB_USER, SASL_CB_AUTHNAME, SASL_CB_LANGUAGE -int SaslClient::SimpleCb(int id, const char** result, unsigned* len) { - if (PREDICT_FALSE(!helper_.IsPlainEnabled())) { - LOG(DFATAL) << "SASL Client: Simple callback called, but PLAIN auth is not enabled"; - return SASL_FAIL; - } - if (PREDICT_FALSE(result == nullptr)) { - LOG(DFATAL) << "SASL Client: result outparam is NULL"; - return SASL_BADPARAM; - } - switch (id) { - // TODO: Support impersonation? - // For impersonation, USER is the impersonated user, AUTHNAME is the "sudoer". - case SASL_CB_USER: - TRACE("SASL Client: callback for SASL_CB_USER"); - *result = plain_auth_user_.c_str(); - if (len != nullptr) *len = plain_auth_user_.length(); - break; - case SASL_CB_AUTHNAME: - TRACE("SASL Client: callback for SASL_CB_AUTHNAME"); - *result = plain_auth_user_.c_str(); - if (len != nullptr) *len = plain_auth_user_.length(); - break; - case SASL_CB_LANGUAGE: - LOG(DFATAL) << "SASL Client: Unable to handle SASL callback type SASL_CB_LANGUAGE" - << "(" << id << ")"; - return SASL_BADPARAM; - default: - LOG(DFATAL) << "SASL Client: Unexpected SASL callback type: " << id; - return SASL_BADPARAM; - } - - return SASL_OK; -} - -// Used for PLAIN. -// SASL callback for SASL_CB_PASS: User password. -int SaslClient::SecretCb(sasl_conn_t* conn, int id, sasl_secret_t** psecret) { - if (PREDICT_FALSE(!helper_.IsPlainEnabled())) { - LOG(DFATAL) << "SASL Client: Plain secret callback called, but PLAIN auth is not enabled"; - return SASL_FAIL; - } - switch (id) { - case SASL_CB_PASS: { - if (!conn || !psecret) return SASL_BADPARAM; - - int len = plain_pass_.length(); - *psecret = reinterpret_cast<sasl_secret_t*>(malloc(sizeof(sasl_secret_t) + len)); - if (!*psecret) { - return SASL_NOMEM; - } - psecret_.reset(*psecret); // Ensure that we free() this structure later. - (*psecret)->len = len; - memcpy(reinterpret_cast<char *>((*psecret)->data), plain_pass_.c_str(), len + 1); - break; - } - default: - LOG(DFATAL) << "SASL Client: Unexpected SASL callback type: " << id; - return SASL_BADPARAM; - } - - return SASL_OK; -} - -} // namespace rpc -} // namespace kudu http://git-wip-us.apache.org/repos/asf/kudu/blob/41c45523/src/kudu/rpc/sasl_client.h ---------------------------------------------------------------------- diff --git a/src/kudu/rpc/sasl_client.h b/src/kudu/rpc/sasl_client.h deleted file mode 100644 index 746a6f8..0000000 --- a/src/kudu/rpc/sasl_client.h +++ /dev/null @@ -1,180 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you 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. - -#ifndef KUDU_RPC_SASL_CLIENT_H -#define KUDU_RPC_SASL_CLIENT_H - -#include <set> -#include <string> -#include <vector> - -#include <sasl/sasl.h> - -#include "kudu/gutil/gscoped_ptr.h" -#include "kudu/rpc/rpc_header.pb.h" -#include "kudu/rpc/sasl_common.h" -#include "kudu/rpc/sasl_helper.h" -#include "kudu/util/monotime.h" -#include "kudu/util/status.h" -#include "kudu/util/net/socket.h" - -namespace kudu { -namespace rpc { - -using std::string; - -class NegotiatePB; -class NegotiatePB_SaslAuth; -class ResponseHeader; - -// Class for doing SASL negotiation with a SaslServer over a bidirectional socket. -// Operations on this class are NOT thread-safe. -class SaslClient { - public: - // Does not take ownership of the socket indicated by the fd. - SaslClient(string app_name, Socket* socket); - - // Enable PLAIN authentication. - // Must be called after Init(). - Status EnablePlain(const string& user, const string& pass); - - // Enable GSSAPI authentication. - // Call after Init(). - Status EnableGSSAPI(); - - // Returns mechanism negotiated by this connection. - // Must be called after Negotiate(). - SaslMechanism::Type negotiated_mechanism() const; - - // Returns the set of RPC system features supported by the remote server. - // Must be called after Negotiate(). - const std::set<RpcFeatureFlag>& server_features() const { - return server_features_; - } - - // Specify IP:port of local side of connection. - // Must be called before Init(). Required for some mechanisms. - void set_local_addr(const Sockaddr& addr); - - // Specify IP:port of remote side of connection. - // Must be called before Init(). Required for some mechanisms. - void set_remote_addr(const Sockaddr& addr); - - // Specify the fully-qualified domain name of the remote server. - // Must be called before Init(). Required for some mechanisms. - void set_server_fqdn(const string& domain_name); - - // Set deadline for connection negotiation. - void set_deadline(const MonoTime& deadline); - - // Get deadline for connection negotiation. - const MonoTime& deadline() const { return deadline_; } - - // Initialize a new SASL client. Must be called before Negotiate(). - // Returns OK on success, otherwise RuntimeError. - Status Init(const string& service_type); - - // Begin negotiation with the SASL server on the other side of the fd socket - // that this client was constructed with. - // Returns OK on success. - // Otherwise, it may return NotAuthorized, NotSupported, or another non-OK status. - Status Negotiate(); - - // SASL callback for plugin options, supported mechanisms, etc. - // Returns SASL_FAIL if the option is not handled, which does not fail the handshake. - int GetOptionCb(const char* plugin_name, const char* option, - const char** result, unsigned* len); - - // SASL callback for SASL_CB_USER, SASL_CB_AUTHNAME, SASL_CB_LANGUAGE - int SimpleCb(int id, const char** result, unsigned* len); - - // SASL callback for SASL_CB_PASS - int SecretCb(sasl_conn_t* conn, int id, sasl_secret_t** psecret); - - private: - // Encode and send the specified negotiate message to the server. - Status SendNegotiatePB(const NegotiatePB& msg); - - // Validate that header does not indicate an error, parse param_buf into response. - Status ParseNegotiatePB(const ResponseHeader& header, - const Slice& param_buf, - NegotiatePB* response); - - // Send an NEGOTIATE message to the server. - Status SendNegotiateMessage(); - - // Send an SASL_INITIATE message to the server. - Status SendInitiateMessage(const NegotiatePB_SaslAuth& auth, - const char* init_msg, unsigned init_msg_len); - - // Send a RESPONSE message to the server. - Status SendResponseMessage(const char* resp_msg, unsigned resp_msg_len); - - // Perform a client-side step of the SASL negotiation. - // Input is what came from the server. Output is what we will send back to the server. - // Returns: - // Status::OK if sasl_client_step returns SASL_OK. - // Status::Incomplete if sasl_client_step returns SASL_CONTINUE - // otherwise returns an appropriate error status. - Status DoSaslStep(const string& in, const char** out, unsigned* out_len); - - // Handle case when server sends NEGOTIATE response. - Status HandleNegotiateResponse(const NegotiatePB& response); - - // Handle case when server sends CHALLENGE response. - Status HandleChallengeResponse(const NegotiatePB& response); - - // Handle case when server sends SUCCESS response. - Status HandleSuccessResponse(const NegotiatePB& response); - - // Parse error status message from raw bytes of an ErrorStatusPB. - Status ParseError(const Slice& err_data); - - string app_name_; - Socket* sock_; - std::vector<sasl_callback_t> callbacks_; - // The SASL connection object. This is initialized in Init() and - // freed after Negotiate() completes (regardless whether it was successful). - gscoped_ptr<sasl_conn_t, SaslDeleter> sasl_conn_; - SaslHelper helper_; - - string plain_auth_user_; - string plain_pass_; - gscoped_ptr<sasl_secret_t, FreeDeleter> psecret_; - - // The set of features supported by the server. - std::set<RpcFeatureFlag> server_features_; - - SaslNegotiationState::Type client_state_; - - // The mechanism we negotiated with the server. - SaslMechanism::Type negotiated_mech_; - - // Intra-negotiation state. - bool nego_ok_; // During negotiation: did we get a SASL_OK response from the SASL library? - bool nego_response_expected_; // During negotiation: Are we waiting for a server response? - - // Negotiation timeout deadline. - MonoTime deadline_; - - DISALLOW_COPY_AND_ASSIGN(SaslClient); -}; - -} // namespace rpc -} // namespace kudu - -#endif // KUDU_RPC_SASL_CLIENT_H
