[master] add /ipki-ca-cert webserver end-point

This patch introduces a new /ipki-ca-cert end-point for the master's
embedded webserver to provide information on the IPKI CA certificate
used by a Kudu cluster.  This information is useful in JWT-based
authentication scenarios.  The missing part of importing certificates
into Kudu client's chain of trusted TLS certificates will be addressed
in follow-up patches.

Change-Id: I833d934cc1d916dba243a05aa96926d3b540b70d
Reviewed-on: http://gerrit.cloudera.org:8080/19906
Tested-by: Alexey Serbin <alexey@apache.org>
Reviewed-by: Abhishek Chennaka <achennaka@cloudera.com>
diff --git a/src/kudu/integration-tests/security-itest.cc b/src/kudu/integration-tests/security-itest.cc
index d3ddad2..5e22745 100644
--- a/src/kudu/integration-tests/security-itest.cc
+++ b/src/kudu/integration-tests/security-itest.cc
@@ -55,6 +55,7 @@
 #include "kudu/ranger-kms/mini_ranger_kms.h"
 #include "kudu/rpc/messenger.h"
 #include "kudu/rpc/rpc_controller.h"
+#include "kudu/security/cert.h"
 #include "kudu/security/kinit_context.h"
 #include "kudu/security/test/mini_kdc.h"
 #include "kudu/security/test/test_certs.h"
@@ -66,11 +67,14 @@
 #include "kudu/tserver/tserver.pb.h"
 #include "kudu/tserver/tserver_service.pb.h"
 #include "kudu/tserver/tserver_service.proxy.h"
+#include "kudu/util/curl_util.h"
 #include "kudu/util/env.h"
+#include "kudu/util/faststring.h"
 #include "kudu/util/mini_oidc.h"
 #include "kudu/util/monotime.h"
 #include "kudu/util/net/net_util.h"
 #include "kudu/util/net/sockaddr.h"
+#include "kudu/util/openssl_util.h"
 #include "kudu/util/path_util.h"
 #include "kudu/util/random.h"
 #include "kudu/util/random_util.h"
@@ -930,6 +934,95 @@
   SmokeTestCluster(client, /*transactional=*/false);
 }
 
+TEST_F(SecurityITest, IPKICACert) {
+  SKIP_IF_SLOW_NOT_ALLOWED();
+
+  // Need to test the functionality for both leader and follower masters.
+  cluster_opts_.num_masters = 3;
+  // No need to involve tablet servers in this scenario.
+  cluster_opts_.num_tablet_servers = 0;
+
+  ASSERT_OK(StartCluster());
+
+  shared_ptr<KuduClient> client;
+  ASSERT_OK(cluster_->CreateClient(nullptr, &client));
+
+  string authn_creds;
+  ASSERT_OK(client->ExportAuthenticationCredentials(&authn_creds));
+  client::AuthenticationCredentialsPB pb;
+  ASSERT_TRUE(pb.ParseFromString(authn_creds));
+  ASSERT_EQ(1, pb.ca_cert_ders_size());
+
+  security::Cert ca_cert_client;
+  ASSERT_OK(ca_cert_client.FromString(pb.ca_cert_ders(0),
+                                      security::DataFormat::DER));
+  string ca_cert_client_pem;
+  ASSERT_OK(ca_cert_client.ToString(&ca_cert_client_pem,
+                                    security::DataFormat::PEM));
+
+  const auto fetch_ipki_ca = [c = cluster_.get()](int master_idx, string* out) {
+    const auto& http_hp = c->master(master_idx)->bound_http_hostport();
+    string url = Substitute("http://$0/ipki-ca-cert", http_hp.ToString());
+    EasyCurl curl;
+    faststring dst;
+    auto res = curl.FetchURL(url, &dst);
+    *out = dst.ToString();
+    return res;
+  };
+
+  int leader_master_idx;
+  ASSERT_OK(cluster_->GetLeaderMasterIndex(&leader_master_idx));
+  string str;
+  ASSERT_OK(fetch_ipki_ca(leader_master_idx, &str));
+  security::Cert ca_cert;
+  ASSERT_OK(ca_cert.FromString(str, security::DataFormat::PEM));
+
+  // Using (string --> security::Cert --> string) conversion chain to compare
+  // canonical representations of the CA certificates in PEM format.
+  string ca_cert_str;
+  ASSERT_OK(ca_cert.ToString(&ca_cert_str, security::DataFormat::PEM));
+  ASSERT_EQ(ca_cert_client_pem, ca_cert_str);
+
+  const auto count_valid_certs = [&](size_t* res) {
+    size_t count = 0;
+    for (auto i = 0; i < cluster_->num_masters(); ++i) {
+      string str;
+      auto s = fetch_ipki_ca(i, &str);
+      ASSERT_TRUE(s.ok() || s.IsRemoteError());
+      if (s.IsRemoteError()) {
+        // If there wasn't a CA cert in the output, there should had been
+        // an error reported.
+        ASSERT_NE(string::npos, str.find("ERROR: "));
+        continue;
+      }
+      security::Cert ca_cert;
+      ASSERT_OK(ca_cert.FromString(str, security::DataFormat::PEM));
+
+      string ca_cert_str;
+      ASSERT_OK(ca_cert.ToString(&ca_cert_str, security::DataFormat::PEM));
+      ASSERT_EQ(ca_cert_client_pem, ca_cert_str);
+      ++count;
+    }
+    *res = count;
+  };
+
+  // The IPKI has been certainly initialized at leader master since the client
+  // was able to successfully connect to the cluster (see above).
+  {
+    size_t count = 0;
+    NO_FATALS(count_valid_certs(&count));
+    ASSERT_GE(count, 1);
+  }
+
+  // At some point, all the followers should have loaded the CA information
+  // generated by the leader master upon the very first startup.
+  ASSERT_EVENTUALLY([&] {
+    size_t count = 0;
+    NO_FATALS(count_valid_certs(&count));
+    ASSERT_EQ(cluster_opts_.num_masters, count);
+  });
+}
+
 class EncryptionPolicyTest :
     public SecurityITest,
     public ::testing::WithParamInterface<tuple<
diff --git a/src/kudu/integration-tests/webserver-crawl-itest.cc b/src/kudu/integration-tests/webserver-crawl-itest.cc
index 707d539..32e2c10 100644
--- a/src/kudu/integration-tests/webserver-crawl-itest.cc
+++ b/src/kudu/integration-tests/webserver-crawl-itest.cc
@@ -17,6 +17,7 @@
 
 #include <algorithm>
 #include <deque>
+#include <functional>
 #include <ostream>
 #include <string>
 #include <tuple>
@@ -282,7 +283,6 @@
     curl.set_verify_peer(false);
   }
 
-  faststring response;
   vector<string> headers;
   if (impersonate_knox) {
     // Pretend we're Knox when communicating with the web UI.
@@ -296,10 +296,18 @@
     int ret = FindNth(url, '/', 3);
     string host = ret == string::npos ? url : url.substr(0, ret);
 
-    // Every link should be reachable.
+    // Every link should be reachable, but some URLs are allowed to return
+    // non-2xx status codes temporarily (e.g., 503 Service Unavailable).
     SCOPED_TRACE(url);
-    ASSERT_OK(curl.FetchURL(url, &response, headers));
-    string resp_str = response.ToString();
+    faststring response;
+    if (const auto s = curl.FetchURL(url, &response, headers); !s.ok()) {
+      ASSERT_TRUE(s.IsRemoteError()) << s.ToString();
+      ASSERT_STR_MATCHES(s.ToString(), "HTTP [[:digit:]]{3}$");
+      ASSERT_EVENTUALLY([&] {
+        ASSERT_OK(curl.FetchURL(url, &response, headers));
+      });
+    }
+    const string resp_str = response.ToString();
     SCOPED_TRACE(resp_str);
 
     gq::CDocument page;
diff --git a/src/kudu/master/master_cert_authority.h b/src/kudu/master/master_cert_authority.h
index a1a279f..0039e00 100644
--- a/src/kudu/master/master_cert_authority.h
+++ b/src/kudu/master/master_cert_authority.h
@@ -66,6 +66,9 @@
   // authority with the information read from the system table.
   Status Init(std::unique_ptr<security::PrivateKey> key,
               std::unique_ptr<security::Cert> cert);
+  bool IsInitialized() const {
+    return !!ca_cert_;
+  }
 
   // Sign the given CSR 'csr_der' provided by a server in the cluster.
   // The authenticated user should be passed in 'caller'. The cert contents
@@ -90,12 +93,12 @@
   // This can be sent to participants in the cluster so they can add it to
   // their trust stores.
   const std::string& ca_cert_der() const {
-    CHECK(ca_cert_) << "must Init()";
+    DCHECK(IsInitialized()) << "must Init()";
     return ca_cert_der_;
   }
 
   const security::Cert& ca_cert() const {
-    CHECK(ca_cert_) << "must Init()";
+    DCHECK(IsInitialized()) << "must Init()";
     return *ca_cert_;
   }
 
diff --git a/src/kudu/master/master_path_handlers.cc b/src/kudu/master/master_path_handlers.cc
index c7bc028..31f78a6 100644
--- a/src/kudu/master/master_path_handlers.cc
+++ b/src/kudu/master/master_path_handlers.cc
@@ -49,15 +49,18 @@
 #include "kudu/gutil/strings/human_readable.h"
 #include "kudu/gutil/strings/join.h"
 #include "kudu/gutil/strings/numbers.h"
+#include "kudu/gutil/strings/strip.h"
 #include "kudu/gutil/strings/substitute.h"
 #include "kudu/gutil/walltime.h"
 #include "kudu/master/catalog_manager.h"
 #include "kudu/master/master.h"
 #include "kudu/master/master.pb.h"
+#include "kudu/master/master_cert_authority.h"
 #include "kudu/master/sys_catalog.h"
 #include "kudu/master/table_metrics.h"
 #include "kudu/master/ts_descriptor.h"
 #include "kudu/master/ts_manager.h"
+#include "kudu/security/cert.h"
 #include "kudu/server/monitored_task.h"
 #include "kudu/server/rpc_server.h"
 #include "kudu/server/webui_util.h"
@@ -68,6 +71,7 @@
 #include "kudu/util/metrics.h"
 #include "kudu/util/monotime.h"
 #include "kudu/util/net/net_util.h"
+#include "kudu/util/openssl_util.h"
 #include "kudu/util/pb_util.h"
 #include "kudu/util/string_case.h"
 #include "kudu/util/url-coding.h"
@@ -601,6 +605,34 @@
   }
 }
 
+void MasterPathHandlers::HandleIpkiCaCert(
+    const Webserver::WebRequest& /*req*/,
+    Webserver::PrerenderedWebResponse* resp) {
+  ostringstream& out = resp->output;
+  if (!master_->catalog_manager()->IsInitialized()) {
+    resp->status_code = HttpStatusCode::ServiceUnavailable;
+    out << "ERROR: CatalogManager is not running";
+    return;
+  }
+  const auto* ca = master_->cert_authority();
+  if (!ca || !ca->IsInitialized()) {
+    resp->status_code = HttpStatusCode::ServiceUnavailable;
+    out << "ERROR: IPKI CA isn't initialized";
+    return;
+  }
+  const auto& cert = ca->ca_cert();
+  string cert_str;
+  if (auto s = cert.ToString(&cert_str, security::DataFormat::PEM); !s.ok()) {
+    auto err = s.CloneAndPrepend("could not convert CA cert to PEM format");
+    LOG(ERROR) << err.ToString();
+    resp->status_code = HttpStatusCode::InternalServerError;
+    out << "ERROR: " << err.ToString();
+    return;
+  }
+  RemoveExtraWhitespace(&cert_str);
+  out << cert_str;
+}
+
 namespace {
 
 // Visitor for the catalog table which dumps tables and tablets in a JSON format. This
@@ -789,8 +821,8 @@
 }
 
 Status MasterPathHandlers::Register(Webserver* server) {
-  bool is_styled = true;
-  bool is_on_nav_bar = true;
+  constexpr const bool is_styled = true;
+  constexpr const bool is_on_nav_bar = true;
   server->RegisterPathHandler(
       "/tablet-servers", "Tablet Servers",
       [this](const Webserver::WebRequest& req, Webserver::WebResponse* resp) {
@@ -816,6 +848,12 @@
       },
       is_styled, is_on_nav_bar);
   server->RegisterPrerenderedPathHandler(
+      "/ipki-ca-cert", "IPKI CA certificate",
+      [this](const Webserver::WebRequest& req, Webserver::PrerenderedWebResponse* resp) {
+        this->HandleIpkiCaCert(req, resp);
+      },
+      false /*is_styled*/, true /*is_on_nav_bar*/);
+  server->RegisterPrerenderedPathHandler(
       "/dump-entities", "Dump Entities",
       [this](const Webserver::WebRequest& req, Webserver::PrerenderedWebResponse* resp) {
         this->HandleDumpEntities(req, resp);
diff --git a/src/kudu/master/master_path_handlers.h b/src/kudu/master/master_path_handlers.h
index 186e6d3..feced14 100644
--- a/src/kudu/master/master_path_handlers.h
+++ b/src/kudu/master/master_path_handlers.h
@@ -52,6 +52,8 @@
                        Webserver::WebResponse* resp);
   void HandleMasters(const Webserver::WebRequest& req,
                      Webserver::WebResponse* resp);
+  void HandleIpkiCaCert(const Webserver::WebRequest& req,
+                        Webserver::PrerenderedWebResponse* resp);
   void HandleDumpEntities(const Webserver::WebRequest& req,
                           Webserver::PrerenderedWebResponse* resp);
 
diff --git a/src/kudu/mini-cluster/webui_checker.h b/src/kudu/mini-cluster/webui_checker.h
index 315fe68..9f368a2 100644
--- a/src/kudu/mini-cluster/webui_checker.h
+++ b/src/kudu/mini-cluster/webui_checker.h
@@ -36,6 +36,7 @@
                        const std::string& tablet_id = "",
                        const std::vector<std::string>& master_pages = {
                            "/dump-entities",
+                           "/ipki-ca-cert",
                            "/masters",
                            "/mem-trackers",
                            "/metrics",