| // 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_server.h" |
| |
| #include <glog/logging.h> |
| #include <google/protobuf/message_lite.h> |
| #include <limits> |
| #include <sasl/sasl.h> |
| #include <set> |
| #include <string> |
| |
| #include "kudu/gutil/endian.h" |
| #include "kudu/gutil/map-util.h" |
| #include "kudu/gutil/stringprintf.h" |
| #include "kudu/gutil/strings/split.h" |
| #include "kudu/rpc/blocking_ops.h" |
| #include "kudu/rpc/auth_store.h" |
| #include "kudu/rpc/constants.h" |
| #include "kudu/rpc/serialization.h" |
| #include "kudu/util/net/sockaddr.h" |
| #include "kudu/util/net/socket.h" |
| #include "kudu/util/trace.h" |
| |
| namespace kudu { |
| namespace rpc { |
| |
| static int SaslServerGetoptCb(void* sasl_server, const char* plugin_name, const char* option, |
| const char** result, unsigned* len) { |
| return static_cast<SaslServer*>(sasl_server) |
| ->GetOptionCb(plugin_name, option, result, len); |
| } |
| |
| static int SaslServerPlainAuthCb(sasl_conn_t *conn, void *sasl_server, const char *user, |
| const char *pass, unsigned passlen, struct propctx *propctx) { |
| return static_cast<SaslServer*>(sasl_server) |
| ->PlainAuthCb(conn, user, pass, passlen, propctx); |
| } |
| |
| SaslServer::SaslServer(string app_name, int fd) |
| : app_name_(std::move(app_name)), |
| sock_(fd), |
| helper_(SaslHelper::SERVER), |
| server_state_(SaslNegotiationState::NEW), |
| negotiated_mech_(SaslMechanism::INVALID), |
| deadline_(MonoTime::Max()) { |
| callbacks_.push_back(SaslBuildCallback(SASL_CB_GETOPT, |
| reinterpret_cast<int (*)()>(&SaslServerGetoptCb), this)); |
| callbacks_.push_back(SaslBuildCallback(SASL_CB_SERVER_USERDB_CHECKPASS, |
| reinterpret_cast<int (*)()>(&SaslServerPlainAuthCb), this)); |
| callbacks_.push_back(SaslBuildCallback(SASL_CB_LIST_END, nullptr, nullptr)); |
| } |
| |
| SaslServer::~SaslServer() { |
| sock_.Release(); // Do not close the underlying socket when this object is destroyed. |
| } |
| |
| Status SaslServer::EnableAnonymous() { |
| DCHECK_EQ(server_state_, SaslNegotiationState::INITIALIZED); |
| return helper_.EnableAnonymous(); |
| } |
| |
| Status SaslServer::EnablePlain(gscoped_ptr<AuthStore> authstore) { |
| DCHECK_EQ(server_state_, SaslNegotiationState::INITIALIZED); |
| RETURN_NOT_OK(helper_.EnablePlain()); |
| authstore_.swap(authstore); |
| return Status::OK(); |
| } |
| |
| SaslMechanism::Type SaslServer::negotiated_mechanism() const { |
| DCHECK_EQ(server_state_, SaslNegotiationState::NEGOTIATED); |
| return negotiated_mech_; |
| } |
| |
| const std::string& SaslServer::plain_auth_user() const { |
| DCHECK_EQ(server_state_, SaslNegotiationState::NEGOTIATED); |
| DCHECK_EQ(negotiated_mech_, SaslMechanism::PLAIN); |
| return plain_auth_user_; |
| } |
| |
| void SaslServer::set_local_addr(const Sockaddr& addr) { |
| DCHECK_EQ(server_state_, SaslNegotiationState::NEW); |
| helper_.set_local_addr(addr); |
| } |
| |
| void SaslServer::set_remote_addr(const Sockaddr& addr) { |
| DCHECK_EQ(server_state_, SaslNegotiationState::NEW); |
| helper_.set_remote_addr(addr); |
| } |
| |
| void SaslServer::set_server_fqdn(const string& domain_name) { |
| DCHECK_EQ(server_state_, SaslNegotiationState::NEW); |
| helper_.set_server_fqdn(domain_name); |
| } |
| |
| void SaslServer::set_deadline(const MonoTime& deadline) { |
| DCHECK_NE(server_state_, SaslNegotiationState::NEGOTIATED); |
| deadline_ = deadline; |
| } |
| |
| // calls sasl_server_init() and sasl_server_new() |
| Status SaslServer::Init(const string& service_type) { |
| RETURN_NOT_OK(SaslInit(app_name_.c_str())); |
| |
| // Ensure we are not called more than once. |
| if (server_state_ != SaslNegotiationState::NEW) { |
| return Status::IllegalState("Init() may only be called once per SaslServer object."); |
| } |
| |
| // TODO: Support security flags. |
| unsigned secflags = 0; |
| |
| sasl_conn_t* sasl_conn = nullptr; |
| int result = sasl_server_new( |
| service_type.c_str(), // Registered name of the service using SASL. Required. |
| helper_.server_fqdn(), // The fully qualified domain name of this server. |
| nullptr, // Permits multiple user realms on server. NULL == use default. |
| 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 (PREDICT_FALSE(result != SASL_OK)) { |
| return Status::RuntimeError("Unable to create new SASL server", |
| SaslErrDesc(result, sasl_conn_.get())); |
| } |
| sasl_conn_.reset(sasl_conn); |
| |
| server_state_ = SaslNegotiationState::INITIALIZED; |
| return Status::OK(); |
| } |
| |
| Status SaslServer::Negotiate() { |
| DVLOG(4) << "Called SaslServer::Negotiate()"; |
| |
| // Ensure we are called exactly once, and in the right order. |
| if (server_state_ == SaslNegotiationState::NEW) { |
| return Status::IllegalState("SaslServer: Init() must be called before calling Negotiate()"); |
| } else if (server_state_ == SaslNegotiationState::NEGOTIATED) { |
| return Status::IllegalState("SaslServer: Negotiate() may only be called once per object."); |
| } |
| |
| // Ensure we can use blocking calls on the socket during negotiation. |
| RETURN_NOT_OK(EnsureBlockingMode(&sock_)); |
| |
| faststring recv_buf; |
| |
| // Read connection header |
| RETURN_NOT_OK(ValidateConnectionHeader(&recv_buf)); |
| |
| nego_ok_ = false; |
| while (!nego_ok_) { |
| TRACE("Waiting for next SASL message..."); |
| RequestHeader header; |
| Slice param_buf; |
| RETURN_NOT_OK(ReceiveFramedMessageBlocking(&sock_, &recv_buf, &header, ¶m_buf, deadline_)); |
| |
| SaslMessagePB request; |
| RETURN_NOT_OK(ParseSaslMsgRequest(header, param_buf, &request)); |
| |
| switch (request.state()) { |
| // NEGOTIATE: They want a list of available mechanisms. |
| case SaslMessagePB::NEGOTIATE: |
| RETURN_NOT_OK(HandleNegotiateRequest(request)); |
| break; |
| |
| // INITIATE: They want to initiate negotiation based on their specified mechanism. |
| case SaslMessagePB::INITIATE: |
| RETURN_NOT_OK(HandleInitiateRequest(request)); |
| break; |
| |
| // RESPONSE: Client sent a new request as a follow-up to a CHALLENGE response. |
| case SaslMessagePB::RESPONSE: |
| RETURN_NOT_OK(HandleResponseRequest(request)); |
| break; |
| |
| // Client sent us some unsupported SASL request. |
| default: { |
| TRACE("SASL Server: Received unsupported request from client"); |
| Status s = Status::InvalidArgument("RPC server doesn't support SASL state in request", |
| SaslMessagePB::SaslState_Name(request.state())); |
| RETURN_NOT_OK(SendSaslError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); |
| return s; |
| } |
| } |
| } |
| |
| TRACE("SASL Server: Successful negotiation"); |
| server_state_ = SaslNegotiationState::NEGOTIATED; |
| return Status::OK(); |
| } |
| |
| Status SaslServer::ValidateConnectionHeader(faststring* recv_buf) { |
| TRACE("Waiting for connection header"); |
| size_t num_read; |
| const size_t conn_header_len = kMagicNumberLength + kHeaderFlagsLength; |
| recv_buf->resize(conn_header_len); |
| RETURN_NOT_OK(sock_.BlockingRecv(recv_buf->data(), conn_header_len, &num_read, deadline_)); |
| DCHECK_EQ(conn_header_len, num_read); |
| |
| RETURN_NOT_OK(serialization::ValidateConnHeader(*recv_buf)); |
| TRACE("Connection header received"); |
| return Status::OK(); |
| } |
| |
| Status SaslServer::ParseSaslMsgRequest(const RequestHeader& header, const Slice& param_buf, |
| SaslMessagePB* request) { |
| Status s = helper_.SanityCheckSaslCallId(header.call_id()); |
| if (!s.ok()) { |
| RETURN_NOT_OK(SendSaslError(ErrorStatusPB::FATAL_INVALID_RPC_HEADER, s)); |
| } |
| |
| s = helper_.ParseSaslMessage(param_buf, request); |
| if (!s.ok()) { |
| RETURN_NOT_OK(SendSaslError(ErrorStatusPB::FATAL_DESERIALIZING_REQUEST, s)); |
| return s; |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status SaslServer::SendSaslMessage(const SaslMessagePB& msg) { |
| DCHECK_NE(server_state_, SaslNegotiationState::NEW) |
| << "Must not send SASL messages before calling Init()"; |
| DCHECK_NE(server_state_, SaslNegotiationState::NEGOTIATED) |
| << "Must not send SASL messages after Negotiate() succeeds"; |
| |
| // Create header with SASL-specific callId |
| ResponseHeader header; |
| header.set_call_id(kSaslCallId); |
| return helper_.SendSaslMessage(&sock_, header, msg, deadline_); |
| } |
| |
| Status SaslServer::SendSaslError(ErrorStatusPB::RpcErrorCodePB code, const Status& err) { |
| DCHECK_NE(server_state_, SaslNegotiationState::NEW) |
| << "Must not send SASL messages before calling Init()"; |
| DCHECK_NE(server_state_, SaslNegotiationState::NEGOTIATED) |
| << "Must not send SASL messages after Negotiate() succeeds"; |
| if (err.ok()) { |
| return Status::InvalidArgument("Cannot send error message using OK status"); |
| } |
| |
| // Create header with SASL-specific callId |
| ResponseHeader header; |
| header.set_call_id(kSaslCallId); |
| header.set_is_error(true); |
| |
| // Get RPC error code from Status object |
| ErrorStatusPB msg; |
| msg.set_code(code); |
| msg.set_message(err.ToString()); |
| |
| RETURN_NOT_OK(helper_.SendSaslMessage(&sock_, header, msg, deadline_)); |
| TRACE("Sent SASL error: $0", ErrorStatusPB::RpcErrorCodePB_Name(code)); |
| return Status::OK(); |
| } |
| |
| Status SaslServer::HandleNegotiateRequest(const SaslMessagePB& request) { |
| TRACE("SASL Server: Received NEGOTIATE request from client"); |
| |
| // Fill in the set of features supported by the client. |
| for (int flag : request.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(kSupportedServerRpcFeatureFlags, feature_flag)) { |
| client_features_.insert(feature_flag); |
| } |
| } |
| |
| set<string> server_mechs = helper_.LocalMechs(); |
| if (PREDICT_FALSE(server_mechs.empty())) { |
| // This will happen if no mechanisms are enabled before calling Init() |
| Status s = Status::IllegalState("SASL server mechanism list is empty!"); |
| LOG(ERROR) << s.ToString(); |
| TRACE("SASL Server: Sending FATAL_UNAUTHORIZED response to client"); |
| RETURN_NOT_OK(SendSaslError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); |
| return s; |
| } |
| |
| RETURN_NOT_OK(SendNegotiateResponse(server_mechs)); |
| return Status::OK(); |
| } |
| |
| Status SaslServer::SendNegotiateResponse(const set<string>& server_mechs) { |
| SaslMessagePB response; |
| response.set_state(SaslMessagePB::NEGOTIATE); |
| |
| for (const string& mech : server_mechs) { |
| SaslMessagePB::SaslAuth* auth = response.add_auths(); |
| |
| // The 'method' field is deprecated, but older versions of Kudu marked it 'required'. |
| // So, we have to set it to something to keep compatibility. At some point, we can |
| // consider removing it and breaking compatibility with Kudu <=0.6. |
| auth->set_method(""); |
| auth->set_mechanism(mech); |
| } |
| |
| // Tell the client which features we support. |
| for (RpcFeatureFlag feature : kSupportedServerRpcFeatureFlags) { |
| response.add_supported_features(feature); |
| } |
| |
| RETURN_NOT_OK(SendSaslMessage(response)); |
| TRACE("Sent NEGOTIATE response"); |
| return Status::OK(); |
| } |
| |
| |
| Status SaslServer::HandleInitiateRequest(const SaslMessagePB& request) { |
| TRACE("SASL Server: Received INITIATE request from client"); |
| |
| if (request.auths_size() != 1) { |
| Status s = Status::NotAuthorized(StringPrintf( |
| "SASL INITIATE request must include exactly one SaslAuth section, found: %d", |
| request.auths_size())); |
| RETURN_NOT_OK(SendSaslError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); |
| return s; |
| } |
| |
| const SaslMessagePB::SaslAuth& auth = request.auths(0); |
| TRACE("SASL Server: Client requested to use mechanism: $0", auth.mechanism()); |
| |
| // Security issue to display this. Commented out but left for debugging purposes. |
| //DVLOG(3) << "SASL server: Client token: " << request.token(); |
| |
| const char* server_out = nullptr; |
| uint32_t server_out_len = 0; |
| TRACE("SASL Server: Calling sasl_server_start()"); |
| int result = sasl_server_start( |
| sasl_conn_.get(), // The SASL connection context created by init() |
| auth.mechanism().c_str(), // The mechanism requested by the client. |
| request.token().c_str(), // Optional string the client gave us. |
| request.token().length(), // Client string len. |
| &server_out, // The output of the SASL library, might not be NULL terminated |
| &server_out_len); // Output len. |
| |
| if (PREDICT_FALSE(result != SASL_OK && result != SASL_CONTINUE)) { |
| Status s = Status::NotAuthorized("Unable to negotiate SASL connection", |
| SaslErrDesc(result, sasl_conn_.get())); |
| RETURN_NOT_OK(SendSaslError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); |
| return s; |
| } |
| negotiated_mech_ = SaslMechanism::value_of(auth.mechanism()); |
| |
| // We have a valid mechanism match |
| if (result == SASL_OK) { |
| nego_ok_ = true; |
| RETURN_NOT_OK(SendSuccessResponse(server_out, server_out_len)); |
| } else { // result == SASL_CONTINUE |
| RETURN_NOT_OK(SendChallengeResponse(server_out, server_out_len)); |
| } |
| return Status::OK(); |
| } |
| |
| Status SaslServer::SendChallengeResponse(const char* challenge, unsigned clen) { |
| if (clen < 1) { |
| Status s = Status::NotAuthorized("SASL library did not provide challenge token!"); |
| RETURN_NOT_OK(SendSaslError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); |
| return s; |
| } |
| |
| SaslMessagePB response; |
| response.set_state(SaslMessagePB::CHALLENGE); |
| response.mutable_token()->assign(challenge, clen); |
| TRACE("SASL Server: Sending CHALLENGE response to client"); |
| RETURN_NOT_OK(SendSaslMessage(response)); |
| return Status::OK(); |
| } |
| |
| Status SaslServer::SendSuccessResponse(const char* token, unsigned tlen) { |
| SaslMessagePB response; |
| response.set_state(SaslMessagePB::SUCCESS); |
| if (PREDICT_FALSE(tlen > 0)) { |
| response.mutable_token()->assign(token, tlen); |
| } |
| TRACE("SASL Server: Sending SUCCESS response to client"); |
| RETURN_NOT_OK(SendSaslMessage(response)); |
| return Status::OK(); |
| } |
| |
| |
| Status SaslServer::HandleResponseRequest(const SaslMessagePB& request) { |
| TRACE("SASL Server: Received RESPONSE request from client"); |
| |
| if (!request.has_token()) { |
| Status s = Status::InvalidArgument("No token in CHALLENGE RESPONSE from client"); |
| RETURN_NOT_OK(SendSaslError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); |
| return s; |
| } |
| |
| const char* server_out = nullptr; |
| uint32_t server_out_len = 0; |
| TRACE("SASL Server: Calling sasl_server_step()"); |
| int result = sasl_server_step( |
| sasl_conn_.get(), // The SASL connection context created by init() |
| request.token().c_str(), // Optional string the client gave us |
| request.token().length(), // Client string len |
| &server_out, // The output of the SASL library, might not be NULL terminated |
| &server_out_len); // Output len |
| |
| if (result != SASL_OK && result != SASL_CONTINUE) { |
| Status s = Status::NotAuthorized("Unable to negotiate SASL connection", |
| SaslErrDesc(result, sasl_conn_.get())); |
| RETURN_NOT_OK(SendSaslError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); |
| return s; |
| } |
| |
| SaslMessagePB msg; |
| if (result == SASL_OK) { |
| nego_ok_ = true; |
| RETURN_NOT_OK(SendSuccessResponse(server_out, server_out_len)); |
| } else { // result == SASL_CONTINUE |
| RETURN_NOT_OK(SendChallengeResponse(server_out, server_out_len)); |
| } |
| return Status::OK(); |
| } |
| |
| int SaslServer::GetOptionCb(const char* plugin_name, const char* option, |
| const char** result, unsigned* len) { |
| return helper_.GetOptionCb(plugin_name, option, result, len); |
| } |
| |
| int SaslServer::PlainAuthCb(sasl_conn_t *conn, const char *user, const char *pass, |
| unsigned passlen, struct propctx *propctx) { |
| TRACE("SASL Server: Checking PLAIN auth credentials"); |
| if (PREDICT_FALSE(!helper_.IsPlainEnabled())) { |
| LOG(DFATAL) << "Password authentication callback called while PLAIN auth disabled"; |
| return SASL_BADPARAM; |
| } |
| if (PREDICT_FALSE(!authstore_)) { |
| LOG(DFATAL) << "AuthStore not initialized"; |
| return SASL_FAIL; |
| } |
| Status s = authstore_->Authenticate(user, string(pass, passlen)); |
| TRACE("SASL Server: PLAIN user authentication status: $0", s.ToString()); |
| if (!s.ok()) { |
| LOG(INFO) << "Failed login for user: " << user; |
| return SASL_FAIL; |
| } |
| plain_auth_user_ = user; // Store username of authenticated user. |
| return SASL_OK; |
| } |
| |
| } // namespace rpc |
| } // namespace kudu |