blob: ab0c3b75849c453e1c3758a1145dcb80eed3a98a [file] [log] [blame]
// 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_common.h"
#include <regex.h>
#include <sasl/sasl.h>
#include <sasl/saslplug.h>
#include <cstdio>
#include <cstring>
#include <limits>
#include <mutex>
#include <ostream>
#include <string>
#include <glog/logging.h>
#include "kudu/gutil/macros.h"
#include "kudu/rpc/constants.h"
#include "kudu/security/init.h"
#include "kudu/util/mutex.h"
#include "kudu/util/net/sockaddr.h"
#include "kudu/util/rw_mutex.h"
#include "kudu/util/stopwatch.h"
#include "kudu/util/string_case.h"
#if defined(__APPLE__)
// Almost all functions in the SASL API are marked as deprecated
// since macOS 10.11.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif // #if defined(__APPLE__)
using std::set;
using std::string;
namespace kudu {
namespace rpc {
const char* const kSaslMechPlain = "PLAIN";
const char* const kSaslMechGSSAPI = "GSSAPI";
extern const size_t kSaslMaxBufSize = 1024;
// See WrapSaslCall().
static __thread string* g_auth_failure_capture = nullptr;
// Determine whether initialization was ever called
static Status sasl_init_status = Status::OK();
static bool sasl_is_initialized = false;
// If true, then we expect someone else has initialized SASL.
static bool g_disable_sasl_init = false;
// If true, we expect kerberos to be enabled.
static bool has_kerberos_keytab = false;
// Output Sasl messages.
// context: not used.
// level: logging level.
// message: message to output;
static int SaslLogCallback(void* context, int level, const char* message) {
if (message == nullptr) return SASL_BADPARAM;
switch (level) {
case SASL_LOG_NONE:
break;
case SASL_LOG_ERR:
LOG(ERROR) << "SASL: " << message;
break;
case SASL_LOG_WARN:
LOG(WARNING) << "SASL: " << message;
break;
case SASL_LOG_NOTE:
LOG(INFO) << "SASL: " << message;
break;
case SASL_LOG_FAIL:
// Capture authentication failures in a thread-local to be picked up
// by WrapSaslCall() below.
VLOG(1) << "SASL: " << message;
if (g_auth_failure_capture) {
*g_auth_failure_capture = message;
}
break;
case SASL_LOG_DEBUG:
VLOG(1) << "SASL: " << message;
break;
case SASL_LOG_TRACE:
case SASL_LOG_PASS:
VLOG(3) << "SASL: " << message;
break;
}
return SASL_OK;
}
// Get Sasl option.
// context: not used
// plugin_name: name of plugin for which an option is being requested.
// option: option requested
// result: set to result which persists until next getopt in same thread,
// unchanged if option not found
// len: length of the result
// Return SASL_FAIL if the option is not handled, this does not fail the handshake.
static int SaslGetOption(void* context, const char* plugin_name, const char* option,
const char** result, unsigned* len) {
// Handle Sasl Library options
if (plugin_name == nullptr) {
// Return the logging level that we want the sasl library to use.
if (strcmp("log_level", option) == 0) {
int level = SASL_LOG_NOTE;
if (VLOG_IS_ON(1)) {
level = SASL_LOG_DEBUG;
} else if (VLOG_IS_ON(3)) {
level = SASL_LOG_TRACE;
}
// The library's contract for this method is that the caller gets to keep
// the returned buffer until the next call by the same thread, so we use a
// threadlocal for the buffer.
static __thread char buf[4];
snprintf(buf, arraysize(buf), "%d", level);
*result = buf;
if (len != nullptr) *len = strlen(buf);
return SASL_OK;
}
// Options can default so don't complain.
VLOG(4) << "SaslGetOption: Unknown library option: " << option;
return SASL_FAIL;
}
VLOG(4) << "SaslGetOption: Unknown plugin: " << plugin_name;
return SASL_FAIL;
}
// Array of callbacks for the sasl library.
static sasl_callback_t callbacks[] = {
{ SASL_CB_LOG, reinterpret_cast<int (*)()>(&SaslLogCallback), nullptr },
{ SASL_CB_GETOPT, reinterpret_cast<int (*)()>(&SaslGetOption), nullptr },
{ SASL_CB_LIST_END, nullptr, nullptr }
// TODO(todd): provide a CANON_USER callback? This is necessary if we want
// to support some kind of auth-to-local mapping of Kerberos principals
// to local usernames. See Impala's implementation for inspiration.
};
// SASL requires mutexes for thread safety, but doesn't implement
// them itself. So, we have to hook them up to our mutex implementation.
static void* SaslMutexAlloc() {
return static_cast<void*>(new Mutex());
}
static void SaslMutexFree(void* m) {
delete static_cast<Mutex*>(m);
}
static int SaslMutexLock(void* m) {
static_cast<Mutex*>(m)->lock();
return 0; // indicates success.
}
static int SaslMutexUnlock(void* m) {
static_cast<Mutex*>(m)->unlock();
return 0; // indicates success.
}
namespace internal {
void SaslSetMutex() {
sasl_set_mutex(&SaslMutexAlloc, &SaslMutexLock, &SaslMutexUnlock, &SaslMutexFree);
}
} // namespace internal
// Sasl initialization detection methods. The OS X SASL library doesn't define
// the sasl_global_utils symbol, so we have to use less robust methods of
// detection.
#if defined(__APPLE__)
static bool SaslIsInitialized() {
return sasl_global_listmech() != nullptr;
}
static bool SaslMutexImplementationProvided() {
return SaslIsInitialized();
}
#else
// This symbol is exported by the SASL library but not defined
// in the headers. It's marked as an API in the library source,
// so seems safe to rely on.
extern "C" sasl_utils_t* sasl_global_utils;
static bool SaslIsInitialized() {
return sasl_global_utils != nullptr;
}
static bool SaslMutexImplementationProvided() {
if (!SaslIsInitialized()) return false;
void* m = sasl_global_utils->mutex_alloc();
sasl_global_utils->mutex_free(m);
// The default implementation of mutex_alloc just returns the constant pointer 0x1.
// This is a bit of an ugly heuristic, but seems unlikely that anyone would ever
// provide a valid implementation that returns an invalid pointer value.
return m != reinterpret_cast<void*>(1);
}
#endif
// Actually perform the initialization for the SASL subsystem.
// Meant to be called via GoogleOnceInit().
static void DoSaslInit(bool kerberos_keytab_provided) {
VLOG(3) << "Initializing SASL library";
has_kerberos_keytab = kerberos_keytab_provided;
bool sasl_initialized = SaslIsInitialized();
if (sasl_initialized && !g_disable_sasl_init) {
LOG(WARNING) << "SASL was initialized prior to Kudu's initialization. Skipping "
<< "initialization. Call kudu::client::DisableSaslInitialization() "
<< "to suppress this message.";
g_disable_sasl_init = true;
}
if (g_disable_sasl_init) {
if (!sasl_initialized) {
sasl_init_status = Status::RuntimeError(
"SASL initialization was disabled, but SASL was not externally initialized.");
return;
}
if (!SaslMutexImplementationProvided()) {
LOG(WARNING)
<< "SASL appears to be initialized by code outside of Kudu "
<< "but was not provided with a mutex implementation! If "
<< "manually initializing SASL, use sasl_set_mutex(3).";
}
return;
}
internal::SaslSetMutex();
int result = sasl_client_init(&callbacks[0]);
if (result != SASL_OK) {
sasl_init_status = Status::RuntimeError("Could not initialize SASL client",
sasl_errstring(result, nullptr, nullptr));
return;
}
result = sasl_server_init(&callbacks[0], kSaslAppName);
if (result != SASL_OK) {
sasl_init_status = Status::RuntimeError("Could not initialize SASL server",
sasl_errstring(result, nullptr, nullptr));
return;
}
sasl_is_initialized = true;
}
Status DisableSaslInitialization() {
if (g_disable_sasl_init) return Status::OK();
if (sasl_is_initialized) {
return Status::IllegalState("SASL already initialized. Initialization can only be disabled "
"before first usage.");
}
g_disable_sasl_init = true;
return Status::OK();
}
Status SaslInit(bool kerberos_keytab_provided) {
// Only execute SASL initialization once
static std::once_flag once;
std::call_once(once, DoSaslInit, kerberos_keytab_provided);
return sasl_init_status;
}
static string CleanSaslError(const string& err) {
// In the case of GSS failures, we often get the actual error message
// buried inside a bunch of generic cruft. Use a regexp to extract it
// out. Note that we avoid std::regex because it appears to be broken
// with older libstdcxx.
static regex_t re;
static std::once_flag once;
#if defined(__APPLE__)
static const char* kGssapiPattern = "GSSAPI Error: Miscellaneous failure \\(see text \\((.+)\\)";
#else
static const char* kGssapiPattern = "Unspecified GSS failure. +"
"Minor code may provide more information +"
"\\((.+)\\)";
#endif
std::call_once(once, []{ CHECK_EQ(0, regcomp(&re, kGssapiPattern, REG_EXTENDED)); });
regmatch_t matches[2];
if (regexec(&re, err.c_str(), arraysize(matches), matches, 0) == 0) {
return err.substr(matches[1].rm_so, matches[1].rm_eo - matches[1].rm_so);
}
return err;
}
static string SaslErrDesc(int status, sasl_conn_t* conn) {
string err;
if (conn != nullptr) {
err = sasl_errdetail(conn);
} else {
err = sasl_errstring(status, nullptr, nullptr);
}
return CleanSaslError(err);
}
Status WrapSaslCall(sasl_conn_t* conn,
const std::function<int()>& call,
const char* description) {
DCHECK(description);
SCOPED_LOG_SLOW_EXECUTION(WARNING, 250, description);
// In many cases, the GSSAPI SASL plugin will generate a nice error
// message as a message logged at SASL_LOG_FAIL logging level, but then
// return a useless one in sasl_errstring(). So, we set a global thread-local
// variable to capture any auth failure log message while we make the
// call into the library.
//
// The thread-local thing is a bit of a hack, but the logging callback
// is set globally rather than on a per-connection basis.
string err;
g_auth_failure_capture = &err;
// Take the 'kerberos_reinit_lock' here to avoid a possible race with ticket renewal.
if (has_kerberos_keytab) kudu::security::KerberosReinitLock()->ReadLock();
int rc = call();
if (has_kerberos_keytab) kudu::security::KerberosReinitLock()->ReadUnlock();
g_auth_failure_capture = nullptr;
switch (rc) {
case SASL_OK:
return Status::OK();
case SASL_CONTINUE:
return Status::Incomplete("");
case SASL_FAIL: // Generic failure (encompasses missing krb5 credentials).
case SASL_BADAUTH: // Authentication failure.
case SASL_BADMAC: // Decode failure.
case SASL_NOAUTHZ: // Authorization failure.
case SASL_NOUSER: // User not found.
case SASL_WRONGMECH: // Server doesn't support requested mechanism.
case SASL_BADSERV: { // Server failed mutual authentication.
if (err.empty()) {
err = SaslErrDesc(rc, conn);
} else {
err = CleanSaslError(err);
}
return Status::NotAuthorized(err);
}
default:
return Status::RuntimeError(SaslErrDesc(rc, conn));
}
}
bool NeedsWrap(sasl_conn_t* sasl_conn) {
const unsigned* ssf;
int rc = sasl_getprop(sasl_conn, SASL_SSF, reinterpret_cast<const void**>(&ssf));
CHECK_EQ(rc, SASL_OK) << "Failed to get SSF property on authenticated SASL connection";
return *ssf != 0;
}
uint32_t GetMaxSendBufferSize(sasl_conn_t* sasl_conn) {
const unsigned* max_buf_size;
int rc = sasl_getprop(sasl_conn, SASL_MAXOUTBUF, reinterpret_cast<const void**>(&max_buf_size));
CHECK_EQ(rc, SASL_OK)
<< "Failed to get max output buffer property on authenticated SASL connection";
return *max_buf_size;
}
Status SaslEncode(sasl_conn_t* conn, Slice plaintext, Slice* ciphertext) {
static constexpr const char* const kDesc = "SASL encoding";
const char* out;
unsigned out_len;
RETURN_NOT_OK_PREPEND(WrapSaslCall(conn, [&] {
return sasl_encode(conn,
reinterpret_cast<const char*>(plaintext.data()),
plaintext.size(),
&out, &out_len);
}, kDesc), string(kDesc) + " failed");
*ciphertext = Slice(out, out_len);
return Status::OK();
}
Status SaslDecode(sasl_conn_t* conn, Slice ciphertext, Slice* plaintext) {
static constexpr const char* const kDesc = "SASL decoding";
const char* out;
unsigned out_len;
RETURN_NOT_OK_PREPEND(WrapSaslCall(conn, [&] {
return sasl_decode(conn,
reinterpret_cast<const char*>(ciphertext.data()),
ciphertext.size(),
&out, &out_len);
}, kDesc), string(kDesc) + " failed");
*plaintext = Slice(out, out_len);
return Status::OK();
}
string SaslIpPortString(const Sockaddr& addr) {
string addr_str = addr.ToString();
size_t colon_pos = addr_str.find(':');
if (colon_pos != string::npos) {
addr_str[colon_pos] = ';';
}
return addr_str;
}
set<SaslMechanism::Type> SaslListAvailableMechs() {
set<SaslMechanism::Type> mechs;
// Array of NULL-terminated strings. Array terminated with NULL.
for (const char** mech_strings = sasl_global_listmech();
mech_strings != nullptr && *mech_strings != nullptr;
mech_strings++) {
auto mech = SaslMechanism::value_of(*mech_strings);
if (mech != SaslMechanism::INVALID) {
mechs.insert(mech);
}
}
return mechs;
}
sasl_callback_t SaslBuildCallback(int id, int (*proc)(void), void* context) {
sasl_callback_t callback;
callback.id = id;
callback.proc = proc;
callback.context = context;
return callback;
}
Status EnableProtection(sasl_conn_t* sasl_conn,
SaslProtection::Type minimum_protection,
size_t max_recv_buf_size) {
static constexpr const char* const kDesc = "setting SASL security properties";
sasl_security_properties_t sec_props;
memset(&sec_props, 0, sizeof(sec_props));
sec_props.min_ssf = minimum_protection;
sec_props.max_ssf = std::numeric_limits<sasl_ssf_t>::max();
sec_props.maxbufsize = max_recv_buf_size;
RETURN_NOT_OK_PREPEND(WrapSaslCall(sasl_conn, [&] {
return sasl_setprop(sasl_conn, SASL_SEC_PROPS, &sec_props);
}, kDesc), string(kDesc) + " failed");
return Status::OK();
}
SaslMechanism::Type SaslMechanism::value_of(const string& mech) {
if (iequals(mech, "PLAIN")) {
return PLAIN;
}
if (iequals(mech, "GSSAPI")) {
return GSSAPI;
}
return INVALID;
}
const char* SaslMechanism::name_of(SaslMechanism::Type val) {
switch (val) {
case PLAIN: return "PLAIN";
case GSSAPI: return "GSSAPI";
default:
return "INVALID";
}
}
const char* SaslProtection::name_of(SaslProtection::Type val) {
switch (val) {
case SaslProtection::kAuthentication: return "authentication";
case SaslProtection::kIntegrity: return "integrity";
case SaslProtection::kPrivacy: return "privacy";
}
LOG(FATAL) << "unknown SASL protection type: " << val;
}
} // namespace rpc
} // namespace kudu
#if defined(__APPLE__)
#pragma GCC diagnostic pop
#endif // #if defined(__APPLE__)