[security] moved mini_kdc into the test subdir Moved mini_kdc code into the test sub-directory. The idea of moving is inspired by Todd's comment on https://gerrit.cloudera.org/#/c/4926/
There are no functional changes in this patch. Change-Id: I5f678f59e1911fd89191bd9b5dfb840e20d746a1 Reviewed-on: http://gerrit.cloudera.org:8080/4965 Tested-by: Kudu Jenkins Reviewed-by: Will Berkeley <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/kudu/repo Commit: http://git-wip-us.apache.org/repos/asf/kudu/commit/c25aea49 Tree: http://git-wip-us.apache.org/repos/asf/kudu/tree/c25aea49 Diff: http://git-wip-us.apache.org/repos/asf/kudu/diff/c25aea49 Branch: refs/heads/master Commit: c25aea49f2eca58b01c6b5b95bc6830b5c784a44 Parents: 81f645c Author: Alexey Serbin <[email protected]> Authored: Fri Nov 4 23:09:04 2016 -0700 Committer: Will Berkeley <[email protected]> Committed: Sat Nov 5 14:30:15 2016 +0000 ---------------------------------------------------------------------- .../external_mini_cluster-test.cc | 2 +- .../integration-tests/external_mini_cluster.cc | 2 +- src/kudu/rpc/sasl_rpc-test.cc | 2 +- src/kudu/security/CMakeLists.txt | 4 +- src/kudu/security/mini_kdc-test.cc | 69 ---- src/kudu/security/mini_kdc.cc | 363 ------------------- src/kudu/security/mini_kdc.h | 127 ------- src/kudu/security/test/mini_kdc-test.cc | 69 ++++ src/kudu/security/test/mini_kdc.cc | 363 +++++++++++++++++++ src/kudu/security/test/mini_kdc.h | 127 +++++++ 10 files changed, 564 insertions(+), 564 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/kudu/blob/c25aea49/src/kudu/integration-tests/external_mini_cluster-test.cc ---------------------------------------------------------------------- diff --git a/src/kudu/integration-tests/external_mini_cluster-test.cc b/src/kudu/integration-tests/external_mini_cluster-test.cc index fbc63ca..f316754 100644 --- a/src/kudu/integration-tests/external_mini_cluster-test.cc +++ b/src/kudu/integration-tests/external_mini_cluster-test.cc @@ -27,7 +27,7 @@ #include "kudu/gutil/stl_util.h" #include "kudu/gutil/strings/substitute.h" #include "kudu/gutil/strings/util.h" -#include "kudu/security/mini_kdc.h" +#include "kudu/security/test/mini_kdc.h" #include "kudu/util/metrics.h" #include "kudu/util/net/net_util.h" #include "kudu/util/test_util.h" http://git-wip-us.apache.org/repos/asf/kudu/blob/c25aea49/src/kudu/integration-tests/external_mini_cluster.cc ---------------------------------------------------------------------- diff --git a/src/kudu/integration-tests/external_mini_cluster.cc b/src/kudu/integration-tests/external_mini_cluster.cc index 4e4420a..621c059 100644 --- a/src/kudu/integration-tests/external_mini_cluster.cc +++ b/src/kudu/integration-tests/external_mini_cluster.cc @@ -32,7 +32,7 @@ #include "kudu/gutil/strings/util.h" #include "kudu/master/master.proxy.h" #include "kudu/master/master_rpc.h" -#include "kudu/security/mini_kdc.h" +#include "kudu/security/test/mini_kdc.h" #include "kudu/server/server_base.pb.h" #include "kudu/server/server_base.proxy.h" #include "kudu/tserver/tserver_service.proxy.h" http://git-wip-us.apache.org/repos/asf/kudu/blob/c25aea49/src/kudu/rpc/sasl_rpc-test.cc ---------------------------------------------------------------------- diff --git a/src/kudu/rpc/sasl_rpc-test.cc b/src/kudu/rpc/sasl_rpc-test.cc index 5861f7b..1a5de40 100644 --- a/src/kudu/rpc/sasl_rpc-test.cc +++ b/src/kudu/rpc/sasl_rpc-test.cc @@ -34,7 +34,7 @@ #include "kudu/rpc/sasl_client.h" #include "kudu/rpc/sasl_common.h" #include "kudu/rpc/sasl_server.h" -#include "kudu/security/mini_kdc.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" http://git-wip-us.apache.org/repos/asf/kudu/blob/c25aea49/src/kudu/security/CMakeLists.txt ---------------------------------------------------------------------- diff --git a/src/kudu/security/CMakeLists.txt b/src/kudu/security/CMakeLists.txt index 3386f3e..28a9543 100644 --- a/src/kudu/security/CMakeLists.txt +++ b/src/kudu/security/CMakeLists.txt @@ -16,7 +16,7 @@ # under the License. set(SECURITY_TEST_SRCS - mini_kdc.cc + test/mini_kdc.cc ) add_library(security-test ${SECURITY_TEST_SRCS}) @@ -30,4 +30,4 @@ set(KUDU_TEST_LINK_LIBS security-test ${KUDU_MIN_TEST_LIBS}) -ADD_KUDU_TEST(mini_kdc-test) +ADD_KUDU_TEST(test/mini_kdc-test) http://git-wip-us.apache.org/repos/asf/kudu/blob/c25aea49/src/kudu/security/mini_kdc-test.cc ---------------------------------------------------------------------- diff --git a/src/kudu/security/mini_kdc-test.cc b/src/kudu/security/mini_kdc-test.cc deleted file mode 100644 index 66fcb4c..0000000 --- a/src/kudu/security/mini_kdc-test.cc +++ /dev/null @@ -1,69 +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 <string> - -#include <gtest/gtest.h> - -#include "kudu/security/mini_kdc.h" -#include "kudu/util/env.h" -#include "kudu/util/test_util.h" - -using std::string; - -namespace kudu { - -TEST(MiniKdcTest, TestBasicOperation) { - MiniKdcOptions options; - MiniKdc kdc(options); - ASSERT_OK(kdc.Start()); - ASSERT_GT(kdc.port(), 0); - ASSERT_OK(kdc.CreateUserPrincipal("alice")); - ASSERT_OK(kdc.Kinit("alice")); - - ASSERT_OK(kdc.Stop()); - ASSERT_OK(kdc.Start()); - - ASSERT_OK(kdc.CreateUserPrincipal("bob")); - ASSERT_OK(kdc.Kinit("bob")); - - string klist; - ASSERT_OK(kdc.Klist(&klist)); - ASSERT_STR_CONTAINS(klist, "[email protected]"); - ASSERT_STR_CONTAINS(klist, "[email protected]"); - ASSERT_STR_CONTAINS(klist, "krbtgt/[email protected]"); - - // Drop 'bob' credentials. We'll get a RuntimeError because klist - // exits with a non-zero exit code if there are no cached credentials. - ASSERT_OK(kdc.Kdestroy()); - ASSERT_TRUE(kdc.Klist(&klist).IsRuntimeError()); - - // Test keytab creation. - string kt_path; - ASSERT_OK(kdc.CreateServiceKeytab("kudu/foo.example.com", &kt_path)); - SCOPED_TRACE(kt_path); - ASSERT_OK(kdc.KlistKeytab(kt_path, &klist)); - ASSERT_STR_CONTAINS(klist, "kudu/[email protected]"); -} - -// Regression test to ensure that dropping a stopped MiniKdc doesn't panic. -TEST(MiniKdcTest, TestStopDrop) { - MiniKdcOptions options; - MiniKdc kdc(options); -} - -} // namespace kudu http://git-wip-us.apache.org/repos/asf/kudu/blob/c25aea49/src/kudu/security/mini_kdc.cc ---------------------------------------------------------------------- diff --git a/src/kudu/security/mini_kdc.cc b/src/kudu/security/mini_kdc.cc deleted file mode 100644 index 8cb73eb..0000000 --- a/src/kudu/security/mini_kdc.cc +++ /dev/null @@ -1,363 +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/security/mini_kdc.h" - -#include <csignal> -#include <stdlib.h> - -#include <limits> -#include <map> -#include <memory> -#include <string> - -#include <glog/logging.h> - -#include "kudu/gutil/gscoped_ptr.h" -#include "kudu/gutil/strings/numbers.h" -#include "kudu/gutil/strings/split.h" -#include "kudu/gutil/strings/strip.h" -#include "kudu/gutil/strings/substitute.h" -#include "kudu/util/env.h" -#include "kudu/util/monotime.h" -#include "kudu/util/path_util.h" -#include "kudu/util/subprocess.h" -#include "kudu/util/test_util.h" - -using std::map; -using std::string; -using std::unique_ptr; -using strings::Substitute; - -namespace kudu { - -string MiniKdcOptions::ToString() const { - return strings::Substitute("{ realm: $0, port: $1, data_root: $2 }", realm, port, data_root); -} - -MiniKdc::MiniKdc() - : MiniKdc(MiniKdcOptions()) { -} - -MiniKdc::MiniKdc(const MiniKdcOptions& options) - : options_(options) { - if (options_.realm.empty()) { - options_.realm = "KRBTEST.COM"; - } - if (options_.data_root.empty()) { - options_.data_root = JoinPathSegments(GetTestDataDirectory(), "krb5kdc"); - } -} - -MiniKdc::~MiniKdc() { - if (kdc_process_) { - WARN_NOT_OK(Stop(), "Unable to stop MiniKdc"); - } -} - -map<string, string> MiniKdc::GetEnvVars() const { - return { - {"KRB5_CONFIG", JoinPathSegments(options_.data_root, "krb5.conf")}, - {"KRB5_KDC_PROFILE", JoinPathSegments(options_.data_root, "kdc.conf")}, - {"KRB5CCNAME", "DIR:" + JoinPathSegments(options_.data_root, "krb5cc")} - }; -} - -vector<string> MiniKdc::MakeArgv(const vector<string>& in_argv) { - vector<string> real_argv = { "env" }; - for (const auto& p : GetEnvVars()) { - real_argv.push_back(Substitute("$0=$1", p.first, p.second)); - } - for (const string& a : in_argv) { - real_argv.push_back(a); - } - return real_argv; -} - -namespace { -// Attempts to find the path to the specified Kerberos binary, storing it in 'path'. -Status GetBinaryPath(const string& binary, - const vector<string>& search, - string* path) { - string p; - - // First, check specified locations which are sometimes not on the PATH. - // This is necessary to check first so that the system Heimdal kerberos - // binaries won't be found first on OS X. - for (const auto& location : search) { - p = JoinPathSegments(location, binary); - if (Env::Default()->FileExists(p)) { - *path = p; - return Status::OK(); - } - } - - // Next check if the binary is on the PATH. - Status s = Subprocess::Call({ "which", binary }, "", &p); - if (s.ok()) { - StripTrailingNewline(&p); - *path = p; - return Status::OK(); - } - - return Status::NotFound("Unable to find binary", binary); -} - -// Attempts to find the path to the specified Kerberos binary, storing it in 'path'. -Status GetBinaryPath(const string& binary, string* path) { - static const vector<string> kCommonLocations = { - "/usr/local/opt/krb5/sbin", // Homebrew - "/usr/local/opt/krb5/bin", // Homebrew - "/opt/local/sbin", // Macports - "/opt/local/bin", // Macports - "/usr/lib/mit/sbin", // SLES - "/usr/sbin", // Linux - }; - return GetBinaryPath(binary, kCommonLocations, path); -} -} // namespace - - -Status MiniKdc::Start() { - CHECK(!kdc_process_); - VLOG(1) << "Starting Kerberos KDC: " << options_.ToString(); - - if (!Env::Default()->FileExists(options_.data_root)) { - RETURN_NOT_OK(Env::Default()->CreateDir(options_.data_root)); - - RETURN_NOT_OK(CreateKdcConf()); - RETURN_NOT_OK(CreateKrb5Conf()); - - // Create the KDC database using the kdb5_util tool. - string kdb5_util_bin; - RETURN_NOT_OK(GetBinaryPath("kdb5_util", &kdb5_util_bin)); - - RETURN_NOT_OK(Subprocess::Call(MakeArgv({ - kdb5_util_bin, "create", - "-s", // Stash the master password. - "-P", "masterpw", // Set a password. - "-W", // Use weak entropy (since we don't need real security). - }))); - } - - // Start the Kerberos KDC. - string krb5kdc_bin; - RETURN_NOT_OK(GetBinaryPath("krb5kdc", &krb5kdc_bin)); - - kdc_process_.reset(new Subprocess( - "env", MakeArgv({ - krb5kdc_bin, - "-n", // Do not daemonize. - }))); - - RETURN_NOT_OK(kdc_process_->Start()); - - // If we asked for an ephemeral port, grab the actual ports and - // rewrite the configuration so that clients can connect. - if (options_.port == 0) { - RETURN_NOT_OK(WaitForKdcPorts()); - RETURN_NOT_OK(CreateKrb5Conf()); - RETURN_NOT_OK(CreateKdcConf()); - } - - return Status::OK(); -} - -Status MiniKdc::Stop() { - CHECK(kdc_process_); - VLOG(1) << "Stopping KDC"; - unique_ptr<Subprocess> proc(kdc_process_.release()); - RETURN_NOT_OK(proc->Kill(SIGKILL)); - RETURN_NOT_OK(proc->Wait()); - - return Status::OK(); -} - -// Creates a kdc.conf file according to the provided options. -Status MiniKdc::CreateKdcConf() const { - static const string kFileTemplate = R"( -[kdcdefaults] -kdc_ports = "" -kdc_tcp_ports = $2 - -[realms] -$1 = { - acl_file = $0/kadm5.acl - admin_keytab = $0/kadm5.keytab - database_name = $0/principal - key_stash_file = $0/.k5.$1 - max_renewable_life = 7d 0h 0m 0s -} - )"; - string file_contents = strings::Substitute(kFileTemplate, options_.data_root, - options_.realm, options_.port); - return WriteStringToFile(Env::Default(), file_contents, - JoinPathSegments(options_.data_root, "kdc.conf")); -} - -// Creates a krb5.conf file according to the provided options. -Status MiniKdc::CreateKrb5Conf() const { - static const string kFileTemplate = R"( -[logging] - kdc = STDERR - -[libdefaults] - default_realm = $1 - dns_lookup_kdc = false - dns_lookup_realm = false - forwardable = true - renew_lifetime = 7d - ticket_lifetime = 24h - - # In miniclusters, we start daemons on local loopback IPs that - # have no reverse DNS entries. So, disable reverse DNS. - rdns = false - - # The server side will start its GSSAPI server using the local FQDN. - # However, in tests, we connect to it via a non-matching loopback IP. - # This enables us to connect despite that mismatch. - ignore_acceptor_hostname = true - - # The KDC is configured to only use TCP, so the client should not prefer UDP. - udp_preference_limit = 0 - -[realms] - $1 = { - kdc = 127.0.0.1:$0 - } - )"; - string file_contents = strings::Substitute(kFileTemplate, options_.port, options_.realm); - return WriteStringToFile(Env::Default(), file_contents, - JoinPathSegments(options_.data_root, "krb5.conf")); -} - -Status MiniKdc::WaitForKdcPorts() { - // We have to use 'lsof' to figure out which ports the KDC bound to if we - // requested ephemeral ones. The KDC doesn't log the bound port or expose it - // in any other fashion, and re-implementing lsof involves parsing a lot of - // files in /proc/. So, requiring lsof for tests and parsing its output seems - // more straight-forward. We call lsof in a loop in case the kdc is slow to - // bind to the ports. - - string lsof; - RETURN_NOT_OK(GetBinaryPath("lsof", {"/sbin", "/usr/sbin"}, &lsof)); - - vector<string> cmd = { - lsof, "-wbn", "-Ffn", - "-p", std::to_string(kdc_process_->pid()), - "-a", "-i", "4TCP"}; - - string lsof_out; - for (int i = 1; ; i++) { - lsof_out.clear(); - Status s = Subprocess::Call(cmd, "", &lsof_out); - - if (s.ok()) { - StripTrailingNewline(&lsof_out); - break; - } else if (i > 10) { - return s; - } - - SleepFor(MonoDelta::FromMilliseconds(i * i)); - } - - // The '-Ffn' flag gets lsof to output something like: - // p19730 - // f123 - // n*:41254 - // The first line is the pid. We ignore it. - // The second line is the file descriptor number. We ignore it. - // The third line has the bind address and port. - vector<string> lines = strings::Split(lsof_out, "\n"); - int32_t port = -1; - if (lines.size() != 3 || - lines[2].substr(0, 3) != "n*:" || - !safe_strto32(lines[2].substr(3), &port) || - port <= 0) { - return Status::RuntimeError("unexpected lsof output", lsof_out); - } - CHECK(port > 0 && port < std::numeric_limits<uint16_t>::max()) - << "parsed invalid port: " << port; - options_.port = port; - VLOG(1) << "Determined bound KDC port: " << options_.port; - return Status::OK(); -} - -Status MiniKdc::CreateUserPrincipal(const string& username) { - string kadmin; - RETURN_NOT_OK(GetBinaryPath("kadmin.local", &kadmin)); - RETURN_NOT_OK(Subprocess::Call(MakeArgv({ - kadmin, "-q", Substitute("add_principal -pw $0 $0", username)}))); - return Status::OK(); -} - -Status MiniKdc::CreateServiceKeytab(const string& spn, - string* path) { - string kt_path = spn; - StripString(&kt_path, "/", '_'); - kt_path = JoinPathSegments(options_.data_root, kt_path) + ".keytab"; - - string kadmin; - RETURN_NOT_OK(GetBinaryPath("kadmin.local", &kadmin)); - RETURN_NOT_OK(Subprocess::Call(MakeArgv({ - kadmin, "-q", Substitute("add_principal -randkey $0", spn)}))); - RETURN_NOT_OK(Subprocess::Call(MakeArgv({ - kadmin, "-q", Substitute("ktadd -k $0 $1", kt_path, spn)}))); - *path = kt_path; - return Status::OK(); -} - -Status MiniKdc::Kinit(const string& username) { - string kinit; - RETURN_NOT_OK(GetBinaryPath("kinit", &kinit)); - Subprocess::Call(MakeArgv({ kinit, username }), username); - return Status::OK(); -} - -Status MiniKdc::Kdestroy() { - string kdestroy; - RETURN_NOT_OK(GetBinaryPath("kdestroy", &kdestroy)); - return Subprocess::Call(MakeArgv({ kdestroy, "-A" })); -} - -Status MiniKdc::Klist(string* output) { - string klist; - RETURN_NOT_OK(GetBinaryPath("klist", &klist)); - RETURN_NOT_OK(Subprocess::Call(MakeArgv({ klist, "-A" }), "", output)); - return Status::OK(); -} - -Status MiniKdc::KlistKeytab(const string& keytab_path, string* output) { - string klist; - RETURN_NOT_OK(GetBinaryPath("klist", &klist)); - RETURN_NOT_OK(Subprocess::Call(MakeArgv({ klist, "-k", keytab_path }), "", output)); - return Status::OK(); -} - -Status MiniKdc::SetKrb5Environment() const { - if (!kdc_process_) { - return Status::IllegalState("KDC not started"); - } - for (const auto& p : GetEnvVars()) { - CHECK_ERR(setenv(p.first.c_str(), p.second.c_str(), 1 /*overwrite*/)); - } - - return Status::OK(); -} - -} // namespace kudu http://git-wip-us.apache.org/repos/asf/kudu/blob/c25aea49/src/kudu/security/mini_kdc.h ---------------------------------------------------------------------- diff --git a/src/kudu/security/mini_kdc.h b/src/kudu/security/mini_kdc.h deleted file mode 100644 index bb5dede..0000000 --- a/src/kudu/security/mini_kdc.h +++ /dev/null @@ -1,127 +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. - -#pragma once - -#include <map> -#include <memory> -#include <string> -#include <vector> - -#include <glog/logging.h> - -#include "kudu/util/status.h" - -namespace kudu { - -class Env; -class Subprocess; - -struct MiniKdcOptions { - - // Kerberos Realm. - // Default: "KRBTEST.COM" - std::string realm; - - // Directory in which to store data. - // Default: "", which auto-generates a unique path for this KDC. - // The default may only be used from a gtest unit test. - std::string data_root; - - // KDC port. - // Default: 0 (ephemeral port). - uint16_t port = 0; - - // Returns a string representation of the options suitable for debug printing. - std::string ToString() const; -}; - -class MiniKdc { - public: - // Creates a new MiniKdc with the default options. - MiniKdc(); - - // Creates a new MiniKdc with the provided options. - explicit MiniKdc(const MiniKdcOptions& options); - - ~MiniKdc(); - - // Starts the mini Kerberos KDC. - Status Start() WARN_UNUSED_RESULT; - - // Stops the mini Kerberos KDC. - Status Stop() WARN_UNUSED_RESULT; - - uint16_t port() const { - CHECK(kdc_process_) << "must start first"; - return options_.port; - } - - // Creates a new user with the given username. - // The password is the same as the username. - Status CreateUserPrincipal(const std::string& username) WARN_UNUSED_RESULT; - - // Creates a new service principal and associated keytab, returning its - // path in 'path'. 'spn' is the desired service principal name - // (e.g. "kudu/foo.example.com"). If the principal already exists, its key - // will be reset and a new keytab will be generated. - Status CreateServiceKeytab(const std::string& spn, std::string* path); - - // Kinit a user to the mini KDC. - Status Kinit(const std::string& username) WARN_UNUSED_RESULT; - - // Destroy any credentials in the current ticket cache. - // Equivalent to 'kdestroy -A'. - Status Kdestroy() WARN_UNUSED_RESULT; - - // Call the 'klist' utility. This is useful for logging the local ticket - // cache state. - Status Klist(std::string* output) WARN_UNUSED_RESULT; - - // Call the 'klist' utility to list the contents of a specific keytab. - Status KlistKeytab(const std::string& keytab_path, - std::string* output) WARN_UNUSED_RESULT; - - // Sets the environment variables used by the krb5 library - // in the current process. This points the SASL library at the - // configuration associated with this KDC. - Status SetKrb5Environment() const; - - // Returns a map of the Kerberos environment variables which configure - // a process to use this KDC. - std::map<std::string, std::string> GetEnvVars() const; - - private: - - // Prepends required Kerberos environment variables to the process arguments. - std::vector<std::string> MakeArgv(const std::vector<std::string>& in_argv); - - // Creates a kdc.conf in the data root. - Status CreateKrb5Conf() const WARN_UNUSED_RESULT; - - // Creates a krb5.conf in the data root. - Status CreateKdcConf() const WARN_UNUSED_RESULT; - - // Determine the ports that the KDC bound to. Will wait for the KDC if it is - // still initializing. - Status WaitForKdcPorts() WARN_UNUSED_RESULT; - - std::unique_ptr<Subprocess> kdc_process_; - MiniKdcOptions options_; -}; - -} // namespace kudu http://git-wip-us.apache.org/repos/asf/kudu/blob/c25aea49/src/kudu/security/test/mini_kdc-test.cc ---------------------------------------------------------------------- diff --git a/src/kudu/security/test/mini_kdc-test.cc b/src/kudu/security/test/mini_kdc-test.cc new file mode 100644 index 0000000..3c57340 --- /dev/null +++ b/src/kudu/security/test/mini_kdc-test.cc @@ -0,0 +1,69 @@ +// 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 <string> + +#include <gtest/gtest.h> + +#include "kudu/security/test/mini_kdc.h" +#include "kudu/util/env.h" +#include "kudu/util/test_util.h" + +using std::string; + +namespace kudu { + +TEST(MiniKdcTest, TestBasicOperation) { + MiniKdcOptions options; + MiniKdc kdc(options); + ASSERT_OK(kdc.Start()); + ASSERT_GT(kdc.port(), 0); + ASSERT_OK(kdc.CreateUserPrincipal("alice")); + ASSERT_OK(kdc.Kinit("alice")); + + ASSERT_OK(kdc.Stop()); + ASSERT_OK(kdc.Start()); + + ASSERT_OK(kdc.CreateUserPrincipal("bob")); + ASSERT_OK(kdc.Kinit("bob")); + + string klist; + ASSERT_OK(kdc.Klist(&klist)); + ASSERT_STR_CONTAINS(klist, "[email protected]"); + ASSERT_STR_CONTAINS(klist, "[email protected]"); + ASSERT_STR_CONTAINS(klist, "krbtgt/[email protected]"); + + // Drop 'bob' credentials. We'll get a RuntimeError because klist + // exits with a non-zero exit code if there are no cached credentials. + ASSERT_OK(kdc.Kdestroy()); + ASSERT_TRUE(kdc.Klist(&klist).IsRuntimeError()); + + // Test keytab creation. + string kt_path; + ASSERT_OK(kdc.CreateServiceKeytab("kudu/foo.example.com", &kt_path)); + SCOPED_TRACE(kt_path); + ASSERT_OK(kdc.KlistKeytab(kt_path, &klist)); + ASSERT_STR_CONTAINS(klist, "kudu/[email protected]"); +} + +// Regression test to ensure that dropping a stopped MiniKdc doesn't panic. +TEST(MiniKdcTest, TestStopDrop) { + MiniKdcOptions options; + MiniKdc kdc(options); +} + +} // namespace kudu http://git-wip-us.apache.org/repos/asf/kudu/blob/c25aea49/src/kudu/security/test/mini_kdc.cc ---------------------------------------------------------------------- diff --git a/src/kudu/security/test/mini_kdc.cc b/src/kudu/security/test/mini_kdc.cc new file mode 100644 index 0000000..8e92080 --- /dev/null +++ b/src/kudu/security/test/mini_kdc.cc @@ -0,0 +1,363 @@ +// 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/security/test/mini_kdc.h" + +#include <csignal> +#include <stdlib.h> + +#include <limits> +#include <map> +#include <memory> +#include <string> + +#include <glog/logging.h> + +#include "kudu/gutil/gscoped_ptr.h" +#include "kudu/gutil/strings/numbers.h" +#include "kudu/gutil/strings/split.h" +#include "kudu/gutil/strings/strip.h" +#include "kudu/gutil/strings/substitute.h" +#include "kudu/util/env.h" +#include "kudu/util/monotime.h" +#include "kudu/util/path_util.h" +#include "kudu/util/subprocess.h" +#include "kudu/util/test_util.h" + +using std::map; +using std::string; +using std::unique_ptr; +using strings::Substitute; + +namespace kudu { + +string MiniKdcOptions::ToString() const { + return strings::Substitute("{ realm: $0, port: $1, data_root: $2 }", realm, port, data_root); +} + +MiniKdc::MiniKdc() + : MiniKdc(MiniKdcOptions()) { +} + +MiniKdc::MiniKdc(const MiniKdcOptions& options) + : options_(options) { + if (options_.realm.empty()) { + options_.realm = "KRBTEST.COM"; + } + if (options_.data_root.empty()) { + options_.data_root = JoinPathSegments(GetTestDataDirectory(), "krb5kdc"); + } +} + +MiniKdc::~MiniKdc() { + if (kdc_process_) { + WARN_NOT_OK(Stop(), "Unable to stop MiniKdc"); + } +} + +map<string, string> MiniKdc::GetEnvVars() const { + return { + {"KRB5_CONFIG", JoinPathSegments(options_.data_root, "krb5.conf")}, + {"KRB5_KDC_PROFILE", JoinPathSegments(options_.data_root, "kdc.conf")}, + {"KRB5CCNAME", "DIR:" + JoinPathSegments(options_.data_root, "krb5cc")} + }; +} + +vector<string> MiniKdc::MakeArgv(const vector<string>& in_argv) { + vector<string> real_argv = { "env" }; + for (const auto& p : GetEnvVars()) { + real_argv.push_back(Substitute("$0=$1", p.first, p.second)); + } + for (const string& a : in_argv) { + real_argv.push_back(a); + } + return real_argv; +} + +namespace { +// Attempts to find the path to the specified Kerberos binary, storing it in 'path'. +Status GetBinaryPath(const string& binary, + const vector<string>& search, + string* path) { + string p; + + // First, check specified locations which are sometimes not on the PATH. + // This is necessary to check first so that the system Heimdal kerberos + // binaries won't be found first on OS X. + for (const auto& location : search) { + p = JoinPathSegments(location, binary); + if (Env::Default()->FileExists(p)) { + *path = p; + return Status::OK(); + } + } + + // Next check if the binary is on the PATH. + Status s = Subprocess::Call({ "which", binary }, "", &p); + if (s.ok()) { + StripTrailingNewline(&p); + *path = p; + return Status::OK(); + } + + return Status::NotFound("Unable to find binary", binary); +} + +// Attempts to find the path to the specified Kerberos binary, storing it in 'path'. +Status GetBinaryPath(const string& binary, string* path) { + static const vector<string> kCommonLocations = { + "/usr/local/opt/krb5/sbin", // Homebrew + "/usr/local/opt/krb5/bin", // Homebrew + "/opt/local/sbin", // Macports + "/opt/local/bin", // Macports + "/usr/lib/mit/sbin", // SLES + "/usr/sbin", // Linux + }; + return GetBinaryPath(binary, kCommonLocations, path); +} +} // namespace + + +Status MiniKdc::Start() { + CHECK(!kdc_process_); + VLOG(1) << "Starting Kerberos KDC: " << options_.ToString(); + + if (!Env::Default()->FileExists(options_.data_root)) { + RETURN_NOT_OK(Env::Default()->CreateDir(options_.data_root)); + + RETURN_NOT_OK(CreateKdcConf()); + RETURN_NOT_OK(CreateKrb5Conf()); + + // Create the KDC database using the kdb5_util tool. + string kdb5_util_bin; + RETURN_NOT_OK(GetBinaryPath("kdb5_util", &kdb5_util_bin)); + + RETURN_NOT_OK(Subprocess::Call(MakeArgv({ + kdb5_util_bin, "create", + "-s", // Stash the master password. + "-P", "masterpw", // Set a password. + "-W", // Use weak entropy (since we don't need real security). + }))); + } + + // Start the Kerberos KDC. + string krb5kdc_bin; + RETURN_NOT_OK(GetBinaryPath("krb5kdc", &krb5kdc_bin)); + + kdc_process_.reset(new Subprocess( + "env", MakeArgv({ + krb5kdc_bin, + "-n", // Do not daemonize. + }))); + + RETURN_NOT_OK(kdc_process_->Start()); + + // If we asked for an ephemeral port, grab the actual ports and + // rewrite the configuration so that clients can connect. + if (options_.port == 0) { + RETURN_NOT_OK(WaitForKdcPorts()); + RETURN_NOT_OK(CreateKrb5Conf()); + RETURN_NOT_OK(CreateKdcConf()); + } + + return Status::OK(); +} + +Status MiniKdc::Stop() { + CHECK(kdc_process_); + VLOG(1) << "Stopping KDC"; + unique_ptr<Subprocess> proc(kdc_process_.release()); + RETURN_NOT_OK(proc->Kill(SIGKILL)); + RETURN_NOT_OK(proc->Wait()); + + return Status::OK(); +} + +// Creates a kdc.conf file according to the provided options. +Status MiniKdc::CreateKdcConf() const { + static const string kFileTemplate = R"( +[kdcdefaults] +kdc_ports = "" +kdc_tcp_ports = $2 + +[realms] +$1 = { + acl_file = $0/kadm5.acl + admin_keytab = $0/kadm5.keytab + database_name = $0/principal + key_stash_file = $0/.k5.$1 + max_renewable_life = 7d 0h 0m 0s +} + )"; + string file_contents = strings::Substitute(kFileTemplate, options_.data_root, + options_.realm, options_.port); + return WriteStringToFile(Env::Default(), file_contents, + JoinPathSegments(options_.data_root, "kdc.conf")); +} + +// Creates a krb5.conf file according to the provided options. +Status MiniKdc::CreateKrb5Conf() const { + static const string kFileTemplate = R"( +[logging] + kdc = STDERR + +[libdefaults] + default_realm = $1 + dns_lookup_kdc = false + dns_lookup_realm = false + forwardable = true + renew_lifetime = 7d + ticket_lifetime = 24h + + # In miniclusters, we start daemons on local loopback IPs that + # have no reverse DNS entries. So, disable reverse DNS. + rdns = false + + # The server side will start its GSSAPI server using the local FQDN. + # However, in tests, we connect to it via a non-matching loopback IP. + # This enables us to connect despite that mismatch. + ignore_acceptor_hostname = true + + # The KDC is configured to only use TCP, so the client should not prefer UDP. + udp_preference_limit = 0 + +[realms] + $1 = { + kdc = 127.0.0.1:$0 + } + )"; + string file_contents = strings::Substitute(kFileTemplate, options_.port, options_.realm); + return WriteStringToFile(Env::Default(), file_contents, + JoinPathSegments(options_.data_root, "krb5.conf")); +} + +Status MiniKdc::WaitForKdcPorts() { + // We have to use 'lsof' to figure out which ports the KDC bound to if we + // requested ephemeral ones. The KDC doesn't log the bound port or expose it + // in any other fashion, and re-implementing lsof involves parsing a lot of + // files in /proc/. So, requiring lsof for tests and parsing its output seems + // more straight-forward. We call lsof in a loop in case the kdc is slow to + // bind to the ports. + + string lsof; + RETURN_NOT_OK(GetBinaryPath("lsof", {"/sbin", "/usr/sbin"}, &lsof)); + + vector<string> cmd = { + lsof, "-wbn", "-Ffn", + "-p", std::to_string(kdc_process_->pid()), + "-a", "-i", "4TCP"}; + + string lsof_out; + for (int i = 1; ; i++) { + lsof_out.clear(); + Status s = Subprocess::Call(cmd, "", &lsof_out); + + if (s.ok()) { + StripTrailingNewline(&lsof_out); + break; + } else if (i > 10) { + return s; + } + + SleepFor(MonoDelta::FromMilliseconds(i * i)); + } + + // The '-Ffn' flag gets lsof to output something like: + // p19730 + // f123 + // n*:41254 + // The first line is the pid. We ignore it. + // The second line is the file descriptor number. We ignore it. + // The third line has the bind address and port. + vector<string> lines = strings::Split(lsof_out, "\n"); + int32_t port = -1; + if (lines.size() != 3 || + lines[2].substr(0, 3) != "n*:" || + !safe_strto32(lines[2].substr(3), &port) || + port <= 0) { + return Status::RuntimeError("unexpected lsof output", lsof_out); + } + CHECK(port > 0 && port < std::numeric_limits<uint16_t>::max()) + << "parsed invalid port: " << port; + options_.port = port; + VLOG(1) << "Determined bound KDC port: " << options_.port; + return Status::OK(); +} + +Status MiniKdc::CreateUserPrincipal(const string& username) { + string kadmin; + RETURN_NOT_OK(GetBinaryPath("kadmin.local", &kadmin)); + RETURN_NOT_OK(Subprocess::Call(MakeArgv({ + kadmin, "-q", Substitute("add_principal -pw $0 $0", username)}))); + return Status::OK(); +} + +Status MiniKdc::CreateServiceKeytab(const string& spn, + string* path) { + string kt_path = spn; + StripString(&kt_path, "/", '_'); + kt_path = JoinPathSegments(options_.data_root, kt_path) + ".keytab"; + + string kadmin; + RETURN_NOT_OK(GetBinaryPath("kadmin.local", &kadmin)); + RETURN_NOT_OK(Subprocess::Call(MakeArgv({ + kadmin, "-q", Substitute("add_principal -randkey $0", spn)}))); + RETURN_NOT_OK(Subprocess::Call(MakeArgv({ + kadmin, "-q", Substitute("ktadd -k $0 $1", kt_path, spn)}))); + *path = kt_path; + return Status::OK(); +} + +Status MiniKdc::Kinit(const string& username) { + string kinit; + RETURN_NOT_OK(GetBinaryPath("kinit", &kinit)); + Subprocess::Call(MakeArgv({ kinit, username }), username); + return Status::OK(); +} + +Status MiniKdc::Kdestroy() { + string kdestroy; + RETURN_NOT_OK(GetBinaryPath("kdestroy", &kdestroy)); + return Subprocess::Call(MakeArgv({ kdestroy, "-A" })); +} + +Status MiniKdc::Klist(string* output) { + string klist; + RETURN_NOT_OK(GetBinaryPath("klist", &klist)); + RETURN_NOT_OK(Subprocess::Call(MakeArgv({ klist, "-A" }), "", output)); + return Status::OK(); +} + +Status MiniKdc::KlistKeytab(const string& keytab_path, string* output) { + string klist; + RETURN_NOT_OK(GetBinaryPath("klist", &klist)); + RETURN_NOT_OK(Subprocess::Call(MakeArgv({ klist, "-k", keytab_path }), "", output)); + return Status::OK(); +} + +Status MiniKdc::SetKrb5Environment() const { + if (!kdc_process_) { + return Status::IllegalState("KDC not started"); + } + for (const auto& p : GetEnvVars()) { + CHECK_ERR(setenv(p.first.c_str(), p.second.c_str(), 1 /*overwrite*/)); + } + + return Status::OK(); +} + +} // namespace kudu http://git-wip-us.apache.org/repos/asf/kudu/blob/c25aea49/src/kudu/security/test/mini_kdc.h ---------------------------------------------------------------------- diff --git a/src/kudu/security/test/mini_kdc.h b/src/kudu/security/test/mini_kdc.h new file mode 100644 index 0000000..bb5dede --- /dev/null +++ b/src/kudu/security/test/mini_kdc.h @@ -0,0 +1,127 @@ +// 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. + +#pragma once + +#include <map> +#include <memory> +#include <string> +#include <vector> + +#include <glog/logging.h> + +#include "kudu/util/status.h" + +namespace kudu { + +class Env; +class Subprocess; + +struct MiniKdcOptions { + + // Kerberos Realm. + // Default: "KRBTEST.COM" + std::string realm; + + // Directory in which to store data. + // Default: "", which auto-generates a unique path for this KDC. + // The default may only be used from a gtest unit test. + std::string data_root; + + // KDC port. + // Default: 0 (ephemeral port). + uint16_t port = 0; + + // Returns a string representation of the options suitable for debug printing. + std::string ToString() const; +}; + +class MiniKdc { + public: + // Creates a new MiniKdc with the default options. + MiniKdc(); + + // Creates a new MiniKdc with the provided options. + explicit MiniKdc(const MiniKdcOptions& options); + + ~MiniKdc(); + + // Starts the mini Kerberos KDC. + Status Start() WARN_UNUSED_RESULT; + + // Stops the mini Kerberos KDC. + Status Stop() WARN_UNUSED_RESULT; + + uint16_t port() const { + CHECK(kdc_process_) << "must start first"; + return options_.port; + } + + // Creates a new user with the given username. + // The password is the same as the username. + Status CreateUserPrincipal(const std::string& username) WARN_UNUSED_RESULT; + + // Creates a new service principal and associated keytab, returning its + // path in 'path'. 'spn' is the desired service principal name + // (e.g. "kudu/foo.example.com"). If the principal already exists, its key + // will be reset and a new keytab will be generated. + Status CreateServiceKeytab(const std::string& spn, std::string* path); + + // Kinit a user to the mini KDC. + Status Kinit(const std::string& username) WARN_UNUSED_RESULT; + + // Destroy any credentials in the current ticket cache. + // Equivalent to 'kdestroy -A'. + Status Kdestroy() WARN_UNUSED_RESULT; + + // Call the 'klist' utility. This is useful for logging the local ticket + // cache state. + Status Klist(std::string* output) WARN_UNUSED_RESULT; + + // Call the 'klist' utility to list the contents of a specific keytab. + Status KlistKeytab(const std::string& keytab_path, + std::string* output) WARN_UNUSED_RESULT; + + // Sets the environment variables used by the krb5 library + // in the current process. This points the SASL library at the + // configuration associated with this KDC. + Status SetKrb5Environment() const; + + // Returns a map of the Kerberos environment variables which configure + // a process to use this KDC. + std::map<std::string, std::string> GetEnvVars() const; + + private: + + // Prepends required Kerberos environment variables to the process arguments. + std::vector<std::string> MakeArgv(const std::vector<std::string>& in_argv); + + // Creates a kdc.conf in the data root. + Status CreateKrb5Conf() const WARN_UNUSED_RESULT; + + // Creates a krb5.conf in the data root. + Status CreateKdcConf() const WARN_UNUSED_RESULT; + + // Determine the ports that the KDC bound to. Will wait for the KDC if it is + // still initializing. + Status WaitForKdcPorts() WARN_UNUSED_RESULT; + + std::unique_ptr<Subprocess> kdc_process_; + MiniKdcOptions options_; +}; + +} // namespace kudu
