blob: 8e882b0fb57db6660dcff74eb46a47904b8eb612 [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/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/constants.h"
#include "kudu/rpc/sasl_client.h"
#include "kudu/rpc/sasl_common.h"
#include "kudu/rpc/sasl_server.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"
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
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.EnableAnonymous();
ASSERT_OK(server.Init(kSaslAppName));
SaslClient client(kSaslAppName, nullptr);
client.EnableAnonymous();
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 RunAnonNegotiationServer(Socket* conn) {
SaslServer sasl_server(kSaslAppName, conn);
CHECK_OK(sasl_server.EnableAnonymous());
CHECK_OK(sasl_server.Init(kSaslAppName));
CHECK_OK(sasl_server.Negotiate());
}
static void RunAnonNegotiationClient(Socket* conn) {
SaslClient sasl_client(kSaslAppName, conn);
CHECK_OK(sasl_client.EnableAnonymous());
CHECK_OK(sasl_client.Init(kSaslAppName));
CHECK_OK(sasl_client.Negotiate());
}
// Test SASL negotiation using the ANONYMOUS mechanism over a socket.
TEST_F(TestSaslRpc, TestAnonNegotiation) {
RunNegotiationTest(RunAnonNegotiationServer, RunAnonNegotiationClient);
}
////////////////////////////////////////////////////////////////////////////////
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());
// 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*/));
#if defined(__APPLE__)
string kErrorMsg = strings::Substitute("get-pricipal open($0): "
"No such file or directory (negative cache)",
kInvalidPath);
#else
string kErrorMsg = "No Kerberos credentials available";
#endif
// 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,
[kErrorMsg](const Status& s, SaslClient& client) {
CHECK(s.IsNotAuthorized());
#ifndef KRB5_VERSION_LE_1_10
CHECK_EQ(s.message().ToString(), kErrorMsg);
#endif
}));
// 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());
}),
std::bind(RunGSSAPINegotiationClient, std::placeholders::_1,
[](const Status& s, SaslClient& client) {
CHECK_OK(s);
CHECK_EQ(SaslMechanism::GSSAPI, client.negotiated_mechanism());
}));
// 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
}));
}
// Test that the pre-flight check for servers requiring Kerberos provides
// nice error messages for missing or bad keytabs.
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
}
////////////////////////////////////////////////////////////////////////////////
static void RunTimeoutExpectingServer(Socket* conn) {
SaslServer sasl_server(kSaslAppName, conn);
CHECK_OK(sasl_server.EnableAnonymous());
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.EnableAnonymous());
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.EnableAnonymous());
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.EnableAnonymous());
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);
}
////////////////////////////////////////////////////////////////////////////////
} // namespace rpc
} // namespace kudu