diff options
author | Tor Brede Vekterli <vekterli@yahooinc.com> | 2023-03-23 12:41:07 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-03-23 12:41:07 +0100 |
commit | d2533769f355b0475c4ca39ad77f5c70140d070b (patch) | |
tree | 43890eb32751371f8802ec5367d8d77bf4857c6d /vespalib/src | |
parent | 40e45bb57c0d6abd9b96ff35ff21115b1d66f9be (diff) | |
parent | 0b77cfcea89d0b8033c6c4614172fb78260082a8 (diff) |
Merge pull request #26538 from vespa-engine/vekterli/add-capability-checks-to-state-api-handlers
Add capability checking to state API handlers
Diffstat (limited to 'vespalib/src')
12 files changed, 243 insertions, 95 deletions
diff --git a/vespalib/src/tests/state_server/state_server_test.cpp b/vespalib/src/tests/state_server/state_server_test.cpp index 2369e0dac66..b6bfcbcc203 100644 --- a/vespalib/src/tests/state_server/state_server_test.cpp +++ b/vespalib/src/tests/state_server/state_server_test.cpp @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include <vespa/vespalib/testkit/test_kit.h> +#include <vespa/vespalib/net/connection_auth_context.h> #include <vespa/vespalib/net/http/state_server.h> #include <vespa/vespalib/net/http/simple_health_producer.h> #include <vespa/vespalib/net/http/simple_metrics_producer.h> @@ -46,15 +47,33 @@ vespalib::string getPage(int port, const vespalib::string &path, const vespalib: vespalib::string getFull(int port, const vespalib::string &path) { return getPage(port, path, "-D -"); } +vespalib::string get_json(const JsonGetHandler &handler, + const vespalib::string &host, + const vespalib::string &path, + const std::map<vespalib::string,vespalib::string> ¶ms) +{ + net::ConnectionAuthContext dummy_ctx(net::tls::PeerCredentials(), net::tls::CapabilitySet::all()); + auto res = handler.get(host, path, params, dummy_ctx); + if (res.ok()) { + return res.payload(); + } + return {}; +} + //----------------------------------------------------------------------------- struct DummyHandler : JsonGetHandler { vespalib::string result; DummyHandler(const vespalib::string &result_in) : result(result_in) {} - vespalib::string get(const vespalib::string &, const vespalib::string &, - const std::map<vespalib::string,vespalib::string> &) const override + Response get(const vespalib::string &, const vespalib::string &, + const std::map<vespalib::string,vespalib::string> &, + const net::ConnectionAuthContext &) const override { - return result; + if (!result.empty()) { + return Response::make_ok_with_json(result); + } else { + return Response::make_not_found(); + } } }; @@ -68,7 +87,7 @@ TEST_F("require that unknown url returns 404 response", HttpServer(0)) { EXPECT_EQUAL(expect, actual); } -TEST_FF("require that empty known url returns 404 response", DummyHandler(""), HttpServer(0)) { +TEST_FF("require that handler can return a 404 response", DummyHandler(""), HttpServer(0)) { auto token = f2.repo().bind(my_path, f1); std::string expect("HTTP/1.1 404 Not Found\r\n" "Connection: close\r\n" @@ -114,10 +133,11 @@ TEST_FFFF("require that handler is selected based on longest matching url prefix struct EchoHost : JsonGetHandler { ~EchoHost() override; - vespalib::string get(const vespalib::string &host, const vespalib::string &, - const std::map<vespalib::string,vespalib::string> &) const override + Response get(const vespalib::string &host, const vespalib::string &, + const std::map<vespalib::string,vespalib::string> &, + const net::ConnectionAuthContext &) const override { - return "[\"" + host + "\"]"; + return Response::make_ok_with_json("[\"" + host + "\"]"); } }; @@ -140,8 +160,9 @@ struct SamplingHandler : JsonGetHandler { mutable vespalib::string my_path; mutable std::map<vespalib::string,vespalib::string> my_params; ~SamplingHandler() override; - vespalib::string get(const vespalib::string &host, const vespalib::string &path, - const std::map<vespalib::string,vespalib::string> ¶ms) const override + Response get(const vespalib::string &host, const vespalib::string &path, + const std::map<vespalib::string,vespalib::string> ¶ms, + const net::ConnectionAuthContext &) const override { { auto guard = std::lock_guard(my_lock); @@ -149,7 +170,7 @@ struct SamplingHandler : JsonGetHandler { my_path = path; my_params = params; } - return "[]"; + return Response::make_ok_with_json("[]"); } }; @@ -218,13 +239,13 @@ TEST_FFFF("require that json handlers can be removed from repo", auto token2 = f4.bind("/foo/bar", f2); auto token3 = f4.bind("/foo/bar/baz", f3); std::map<vespalib::string,vespalib::string> params; - EXPECT_EQUAL("[1]", f4.get("", "/foo", params)); - EXPECT_EQUAL("[2]", f4.get("", "/foo/bar", params)); - EXPECT_EQUAL("[3]", f4.get("", "/foo/bar/baz", params)); + EXPECT_EQUAL("[1]", get_json(f4, "", "/foo", params)); + EXPECT_EQUAL("[2]", get_json(f4, "", "/foo/bar", params)); + EXPECT_EQUAL("[3]", get_json(f4, "", "/foo/bar/baz", params)); token2.reset(); - EXPECT_EQUAL("[1]", f4.get("", "/foo", params)); - EXPECT_EQUAL("[1]", f4.get("", "/foo/bar", params)); - EXPECT_EQUAL("[3]", f4.get("", "/foo/bar/baz", params)); + EXPECT_EQUAL("[1]", get_json(f4, "", "/foo", params)); + EXPECT_EQUAL("[1]", get_json(f4, "", "/foo/bar", params)); + EXPECT_EQUAL("[3]", get_json(f4, "", "/foo/bar/baz", params)); } TEST_FFFF("require that json handlers can be shadowed", @@ -234,12 +255,12 @@ TEST_FFFF("require that json handlers can be shadowed", auto token1 = f4.bind("/foo", f1); auto token2 = f4.bind("/foo/bar", f2); std::map<vespalib::string,vespalib::string> params; - EXPECT_EQUAL("[1]", f4.get("", "/foo", params)); - EXPECT_EQUAL("[2]", f4.get("", "/foo/bar", params)); + EXPECT_EQUAL("[1]", get_json(f4, "", "/foo", params)); + EXPECT_EQUAL("[2]", get_json(f4, "", "/foo/bar", params)); auto token3 = f4.bind("/foo/bar", f3); - EXPECT_EQUAL("[3]", f4.get("", "/foo/bar", params)); + EXPECT_EQUAL("[3]", get_json(f4, "", "/foo/bar", params)); token3.reset(); - EXPECT_EQUAL("[2]", f4.get("", "/foo/bar", params)); + EXPECT_EQUAL("[2]", get_json(f4, "", "/foo/bar", params)); } TEST_F("require that root resources can be tracked", JsonHandlerRepo()) @@ -262,14 +283,14 @@ TEST_FFFF("require that state api responds to the expected paths", StateApi(f1, f2, f3)) { f2.setTotalMetrics("{}"); // avoid empty result - EXPECT_TRUE(!f4.get(host_tag, short_root_path, empty_params).empty()); - EXPECT_TRUE(!f4.get(host_tag, root_path, empty_params).empty()); - EXPECT_TRUE(!f4.get(host_tag, health_path, empty_params).empty()); - EXPECT_TRUE(!f4.get(host_tag, metrics_path, empty_params).empty()); - EXPECT_TRUE(!f4.get(host_tag, config_path, empty_params).empty()); - EXPECT_TRUE(!f4.get(host_tag, total_metrics_path, empty_params).empty()); - EXPECT_TRUE(f4.get(host_tag, unknown_path, empty_params).empty()); - EXPECT_TRUE(f4.get(host_tag, unknown_state_path, empty_params).empty()); + EXPECT_TRUE(!get_json(f4, host_tag, short_root_path, empty_params).empty()); + EXPECT_TRUE(!get_json(f4, host_tag, root_path, empty_params).empty()); + EXPECT_TRUE(!get_json(f4, host_tag, health_path, empty_params).empty()); + EXPECT_TRUE(!get_json(f4, host_tag, metrics_path, empty_params).empty()); + EXPECT_TRUE(!get_json(f4, host_tag, config_path, empty_params).empty()); + EXPECT_TRUE(!get_json(f4, host_tag, total_metrics_path, empty_params).empty()); + EXPECT_TRUE(get_json(f4, host_tag, unknown_path, empty_params).empty()); + EXPECT_TRUE(get_json(f4, host_tag, unknown_state_path, empty_params).empty()); } TEST_FFFF("require that top-level urls are generated correctly", @@ -280,9 +301,9 @@ TEST_FFFF("require that top-level urls are generated correctly", "{\"url\":\"http://HOST/state/v1/health\"}," "{\"url\":\"http://HOST/state/v1/metrics\"}," "{\"url\":\"http://HOST/state/v1/config\"}]}", - f4.get(host_tag, root_path, empty_params)); - EXPECT_EQUAL(f4.get(host_tag, root_path, empty_params), - f4.get(host_tag, short_root_path, empty_params)); + get_json(f4, host_tag, root_path, empty_params)); + EXPECT_EQUAL(get_json(f4, host_tag, root_path, empty_params), + get_json(f4, host_tag, short_root_path, empty_params)); } TEST_FFFF("require that top-level resource list can be extended", @@ -295,7 +316,7 @@ TEST_FFFF("require that top-level resource list can be extended", "{\"url\":\"http://HOST/state/v1/metrics\"}," "{\"url\":\"http://HOST/state/v1/config\"}," "{\"url\":\"http://HOST/state/v1/custom\"}]}", - f4.get(host_tag, root_path, empty_params)); + get_json(f4, host_tag, root_path, empty_params)); } TEST_FFFF("require that health resource works as expected", @@ -303,10 +324,10 @@ TEST_FFFF("require that health resource works as expected", StateApi(f1, f2, f3)) { EXPECT_EQUAL("{\"status\":{\"code\":\"up\"}}", - f4.get(host_tag, health_path, empty_params)); + get_json(f4, host_tag, health_path, empty_params)); f1.setFailed("FAIL MSG"); EXPECT_EQUAL("{\"status\":{\"code\":\"down\",\"message\":\"FAIL MSG\"}}", - f4.get(host_tag, health_path, empty_params)); + get_json(f4, host_tag, health_path, empty_params)); } TEST_FFFF("require that metrics resource works as expected", @@ -314,14 +335,14 @@ TEST_FFFF("require that metrics resource works as expected", StateApi(f1, f2, f3)) { EXPECT_EQUAL("{\"status\":{\"code\":\"up\"}}", - f4.get(host_tag, metrics_path, empty_params)); + get_json(f4, host_tag, metrics_path, empty_params)); f1.setFailed("FAIL MSG"); EXPECT_EQUAL("{\"status\":{\"code\":\"down\",\"message\":\"FAIL MSG\"}}", - f4.get(host_tag, metrics_path, empty_params)); + get_json(f4, host_tag, metrics_path, empty_params)); f1.setOk(); f2.setMetrics("{\"foo\":\"bar\"}"); EXPECT_EQUAL("{\"status\":{\"code\":\"up\"},\"metrics\":{\"foo\":\"bar\"}}", - f4.get(host_tag, metrics_path, empty_params)); + get_json(f4, host_tag, metrics_path, empty_params)); } TEST_FFFF("require that config resource works as expected", @@ -329,17 +350,17 @@ TEST_FFFF("require that config resource works as expected", StateApi(f1, f2, f3)) { EXPECT_EQUAL("{\"config\":{}}", - f4.get(host_tag, config_path, empty_params)); + get_json(f4, host_tag, config_path, empty_params)); f3.addConfig(SimpleComponentConfigProducer::Config("foo", 3)); EXPECT_EQUAL("{\"config\":{\"generation\":3,\"foo\":{\"generation\":3}}}", - f4.get(host_tag, config_path, empty_params)); + get_json(f4, host_tag, config_path, empty_params)); f3.addConfig(SimpleComponentConfigProducer::Config("foo", 4)); f3.addConfig(SimpleComponentConfigProducer::Config("bar", 4, "error")); EXPECT_EQUAL("{\"config\":{\"generation\":4,\"bar\":{\"generation\":4,\"message\":\"error\"},\"foo\":{\"generation\":4}}}", - f4.get(host_tag, config_path, empty_params)); + get_json(f4, host_tag, config_path, empty_params)); f3.removeConfig("bar"); EXPECT_EQUAL("{\"config\":{\"generation\":4,\"foo\":{\"generation\":4}}}", - f4.get(host_tag, config_path, empty_params)); + get_json(f4, host_tag, config_path, empty_params)); } TEST_FFFF("require that state api also can return total metric", @@ -348,18 +369,18 @@ TEST_FFFF("require that state api also can return total metric", { f2.setTotalMetrics("{\"foo\":\"bar\"}"); EXPECT_EQUAL("{\"foo\":\"bar\"}", - f4.get(host_tag, total_metrics_path, empty_params)); + get_json(f4, host_tag, total_metrics_path, empty_params)); } TEST_FFFFF("require that custom handlers can be added to the state server", SimpleHealthProducer(), SimpleMetricsProducer(), SimpleComponentConfigProducer(), StateApi(f1, f2, f3), DummyHandler("[123]")) { - EXPECT_EQUAL("", f4.get(host_tag, my_path, empty_params)); + EXPECT_EQUAL("", get_json(f4, host_tag, my_path, empty_params)); auto token = f4.repo().bind(my_path, f5); - EXPECT_EQUAL("[123]", f4.get(host_tag, my_path, empty_params)); + EXPECT_EQUAL("[123]", get_json(f4, host_tag, my_path, empty_params)); token.reset(); - EXPECT_EQUAL("", f4.get(host_tag, my_path, empty_params)); + EXPECT_EQUAL("", get_json(f4, host_tag, my_path, empty_params)); } struct EchoConsumer : MetricsProducer { @@ -379,7 +400,8 @@ TEST_FFFF("require that empty v1 metrics consumer defaults to 'statereporter'", StateApi(f1, f2, f3)) { std::map<vespalib::string,vespalib::string> my_params; - EXPECT_EQUAL("{\"status\":{\"code\":\"up\"},\"metrics\":[\"statereporter\"]}", f4.get(host_tag, metrics_path, empty_params)); + EXPECT_EQUAL("{\"status\":{\"code\":\"up\"},\"metrics\":[\"statereporter\"]}", + get_json(f4, host_tag, metrics_path, empty_params)); } TEST_FFFF("require that empty total metrics consumer defaults to the empty string", @@ -387,7 +409,7 @@ TEST_FFFF("require that empty total metrics consumer defaults to the empty strin StateApi(f1, f2, f3)) { std::map<vespalib::string,vespalib::string> my_params; - EXPECT_EQUAL("[\"\"]", f4.get(host_tag, total_metrics_path, empty_params)); + EXPECT_EQUAL("[\"\"]", get_json(f4, host_tag, total_metrics_path, empty_params)); } TEST_FFFF("require that metrics consumer is passed correctly", @@ -396,8 +418,8 @@ TEST_FFFF("require that metrics consumer is passed correctly", { std::map<vespalib::string,vespalib::string> my_params; my_params["consumer"] = "ME"; - EXPECT_EQUAL("{\"status\":{\"code\":\"up\"},\"metrics\":[\"ME\"]}", f4.get(host_tag, metrics_path, my_params)); - EXPECT_EQUAL("[\"ME\"]", f4.get(host_tag, total_metrics_path, my_params)); + EXPECT_EQUAL("{\"status\":{\"code\":\"up\"},\"metrics\":[\"ME\"]}", get_json(f4, host_tag, metrics_path, my_params)); + EXPECT_EQUAL("[\"ME\"]", get_json(f4, host_tag, total_metrics_path, my_params)); } void check_json(const vespalib::string &expect_json, const vespalib::string &actual_json) { @@ -496,15 +518,15 @@ TEST("require that generic state can be explored") { EXPECT_TRUE(slime::JsonFormat::decode(json_model, slime_state) > 0); SlimeExplorer slime_explorer(slime_state.get()); GenericStateHandler state_handler(short_root_path, slime_explorer); - EXPECT_EQUAL("", state_handler.get(host_tag, unknown_path, empty_params)); - EXPECT_EQUAL("", state_handler.get(host_tag, unknown_state_path, empty_params)); - check_json(json_root, state_handler.get(host_tag, root_path, empty_params)); - check_json(json_engine, state_handler.get(host_tag, root_path + "engine", empty_params)); - check_json(json_engine_stats, state_handler.get(host_tag, root_path + "engine/stats", empty_params)); - check_json(json_list, state_handler.get(host_tag, root_path + "list", empty_params)); - check_json(json_list_one, state_handler.get(host_tag, root_path + "list/one", empty_params)); - check_json(json_list_one_size, state_handler.get(host_tag, root_path + "list/one/size", empty_params)); - check_json(json_list_two, state_handler.get(host_tag, root_path + "list/two", empty_params)); + EXPECT_EQUAL("", get_json(state_handler, host_tag, unknown_path, empty_params)); + EXPECT_EQUAL("", get_json(state_handler, host_tag, unknown_state_path, empty_params)); + check_json(json_root, get_json(state_handler, host_tag, root_path, empty_params)); + check_json(json_engine, get_json(state_handler, host_tag, root_path + "engine", empty_params)); + check_json(json_engine_stats, get_json(state_handler, host_tag, root_path + "engine/stats", empty_params)); + check_json(json_list, get_json(state_handler, host_tag, root_path + "list", empty_params)); + check_json(json_list_one, get_json(state_handler, host_tag, root_path + "list/one", empty_params)); + check_json(json_list_one_size, get_json(state_handler, host_tag, root_path + "list/one/size", empty_params)); + check_json(json_list_two, get_json(state_handler, host_tag, root_path + "list/two", empty_params)); } TEST_MAIN() { diff --git a/vespalib/src/vespa/vespalib/net/http/CMakeLists.txt b/vespalib/src/vespa/vespalib/net/http/CMakeLists.txt index b3e8d8c7a14..2c46c0682f4 100644 --- a/vespalib/src/vespa/vespalib/net/http/CMakeLists.txt +++ b/vespalib/src/vespa/vespalib/net/http/CMakeLists.txt @@ -4,6 +4,7 @@ vespa_add_library(vespalib_vespalib_net_http OBJECT component_config_producer.cpp generic_state_handler.cpp http_server.cpp + json_get_handler.cpp json_handler_repo.cpp simple_component_config_producer.cpp simple_health_producer.cpp diff --git a/vespalib/src/vespa/vespalib/net/http/generic_state_handler.cpp b/vespalib/src/vespa/vespalib/net/http/generic_state_handler.cpp index 91fc6d7e617..5cd78d6bc67 100644 --- a/vespalib/src/vespa/vespalib/net/http/generic_state_handler.cpp +++ b/vespalib/src/vespa/vespalib/net/http/generic_state_handler.cpp @@ -126,14 +126,14 @@ vespalib::string render(const StateExplorer &state, const Url &url) { return buf.get().make_string(); } -vespalib::string explore(const StateExplorer &state, const vespalib::string &host, - const std::vector<vespalib::string> &items, size_t pos) { +JsonGetHandler::Response explore(const StateExplorer &state, const vespalib::string &host, + const std::vector<vespalib::string> &items, size_t pos) { if (pos == items.size()) { - return render(state, Url(host, items)); + return JsonGetHandler::Response::make_ok_with_json(render(state, Url(host, items))); } std::unique_ptr<StateExplorer> child = state.get_child(items[pos]); if (!child) { - return ""; + return JsonGetHandler::Response::make_not_found(); } return explore(*child, host, items, pos + 1); } @@ -146,14 +146,15 @@ GenericStateHandler::GenericStateHandler(const vespalib::string &root_path, cons { } -vespalib::string +JsonGetHandler::Response GenericStateHandler::get(const vespalib::string &host, const vespalib::string &path, - const std::map<vespalib::string,vespalib::string> &) const + const std::map<vespalib::string,vespalib::string> &, + const net::ConnectionAuthContext &) const { std::vector<vespalib::string> items = split_path(path); if (!is_prefix(_root, items)) { - return ""; + return Response::make_not_found(); } return explore(_state, host, items, _root.size()); } diff --git a/vespalib/src/vespa/vespalib/net/http/generic_state_handler.h b/vespalib/src/vespa/vespalib/net/http/generic_state_handler.h index 6065aa165ec..37f5e0d4d22 100644 --- a/vespalib/src/vespa/vespalib/net/http/generic_state_handler.h +++ b/vespalib/src/vespa/vespalib/net/http/generic_state_handler.h @@ -23,9 +23,10 @@ private: public: GenericStateHandler(const vespalib::string &root_path, const StateExplorer &state); - virtual vespalib::string get(const vespalib::string &host, - const vespalib::string &path, - const std::map<vespalib::string,vespalib::string> ¶ms) const override; + Response get(const vespalib::string &host, + const vespalib::string &path, + const std::map<vespalib::string,vespalib::string> ¶ms, + const net::ConnectionAuthContext &auth_ctx) const override; }; } // namespace vespalib diff --git a/vespalib/src/vespa/vespalib/net/http/http_server.cpp b/vespalib/src/vespa/vespalib/net/http/http_server.cpp index f2c041cb648..1b707b9dad1 100644 --- a/vespalib/src/vespa/vespalib/net/http/http_server.cpp +++ b/vespalib/src/vespa/vespalib/net/http/http_server.cpp @@ -2,17 +2,18 @@ #include "http_server.h" #include <vespa/vespalib/net/crypto_engine.h> +#include <vespa/vespalib/net/connection_auth_context.h> namespace vespalib { void HttpServer::get(Portal::GetRequest req) { - vespalib::string json_result = _handler_repo.get(req.get_host(), req.get_path(), req.export_params()); - if (json_result.empty()) { - req.respond_with_error(404, "Not Found"); + auto response = _handler_repo.get(req.get_host(), req.get_path(), req.export_params(), req.auth_context()); + if (response.failed()) { + req.respond_with_error(response.status_code(), response.status_message()); } else { - req.respond_with_content("application/json", json_result); + req.respond_with_content("application/json", response.payload()); } } diff --git a/vespalib/src/vespa/vespalib/net/http/json_get_handler.cpp b/vespalib/src/vespa/vespalib/net/http/json_get_handler.cpp new file mode 100644 index 00000000000..110924785ad --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/http/json_get_handler.cpp @@ -0,0 +1,42 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "json_get_handler.h" + +namespace vespalib { + +JsonGetHandler::Response::Response(int status_code, vespalib::string status_or_payload) + : _status_code(status_code), + _status_or_payload(std::move(status_or_payload)) +{} + +JsonGetHandler::Response::Response() + : _status_code(500), + _status_or_payload("Internal Server Error") +{} + +JsonGetHandler::Response::~Response() = default; + +JsonGetHandler::Response::Response(const Response&) = default; +JsonGetHandler::Response& JsonGetHandler::Response::operator=(const Response&) = default; +JsonGetHandler::Response::Response(Response&&) noexcept = default; +JsonGetHandler::Response& JsonGetHandler::Response::operator=(Response&&) noexcept = default; + +JsonGetHandler::Response +JsonGetHandler::Response::make_ok_with_json(vespalib::string json) +{ + return {200, std::move(json)}; +} + +JsonGetHandler::Response +JsonGetHandler::Response::make_failure(int status_code, vespalib::string status_message) +{ + return {status_code, std::move(status_message)}; +} + +JsonGetHandler::Response +JsonGetHandler::Response::make_not_found() +{ + return {404, "Not Found"}; +} + +} diff --git a/vespalib/src/vespa/vespalib/net/http/json_get_handler.h b/vespalib/src/vespa/vespalib/net/http/json_get_handler.h index d257bd0285f..6a49618ed37 100644 --- a/vespalib/src/vespa/vespalib/net/http/json_get_handler.h +++ b/vespalib/src/vespa/vespalib/net/http/json_get_handler.h @@ -5,13 +5,52 @@ #include <vespa/vespalib/stllike/string.h> #include <map> +namespace vespalib::net { class ConnectionAuthContext; } + namespace vespalib { struct JsonGetHandler { - virtual vespalib::string get(const vespalib::string &host, - const vespalib::string &path, - const std::map<vespalib::string,vespalib::string> ¶ms) const = 0; - virtual ~JsonGetHandler() {} + class Response { + int _status_code; + vespalib::string _status_or_payload; + + Response(int status_code, vespalib::string status_or_payload); + public: + Response(); // By default, 500 Internal Server Error + ~Response(); + Response(const Response&); + Response& operator=(const Response&); + Response(Response&&) noexcept; + Response& operator=(Response&&) noexcept; + + [[nodiscard]] int status_code() const noexcept { return _status_code; } + [[nodiscard]] bool ok() const noexcept { return _status_code == 200; } + [[nodiscard]] bool failed() const noexcept { return _status_code != 200; } + [[nodiscard]] vespalib::stringref status_message() const noexcept { + if (_status_code == 200) { + return "OK"; + } else { + return _status_or_payload; + } + } + [[nodiscard]] vespalib::stringref payload() const noexcept { + if (_status_code == 200) { + return _status_or_payload; + } else { + return {}; + } + } + + [[nodiscard]] static Response make_ok_with_json(vespalib::string json); + [[nodiscard]] static Response make_failure(int status_code, vespalib::string status_message); + [[nodiscard]] static Response make_not_found(); + }; + + virtual Response get(const vespalib::string &host, + const vespalib::string &path, + const std::map<vespalib::string,vespalib::string> ¶ms, + const net::ConnectionAuthContext &auth_ctx) const = 0; + virtual ~JsonGetHandler() = default; }; } // namespace vespalib diff --git a/vespalib/src/vespa/vespalib/net/http/json_handler_repo.cpp b/vespalib/src/vespa/vespalib/net/http/json_handler_repo.cpp index 3f92cffb4af..db43d79cf45 100644 --- a/vespalib/src/vespa/vespalib/net/http/json_handler_repo.cpp +++ b/vespalib/src/vespa/vespalib/net/http/json_handler_repo.cpp @@ -74,18 +74,19 @@ JsonHandlerRepo::get_root_resources() const return result; } -vespalib::string +JsonGetHandler::Response JsonHandlerRepo::get(const vespalib::string &host, const vespalib::string &path, - const std::map<vespalib::string,vespalib::string> ¶ms) const + const std::map<vespalib::string,vespalib::string> ¶ms, + const net::ConnectionAuthContext &auth_ctx) const { std::lock_guard<std::mutex> guard(_state->lock); for (const auto &hook: _state->hooks) { if (path.find(hook.path_prefix) == 0) { - return hook.handler->get(host, path, params); + return hook.handler->get(host, path, params, auth_ctx); } } - return ""; + return Response::make_not_found(); } } // namespace vespalib diff --git a/vespalib/src/vespa/vespalib/net/http/json_handler_repo.h b/vespalib/src/vespa/vespalib/net/http/json_handler_repo.h index 46ef3bd762a..aa12e77160d 100644 --- a/vespalib/src/vespa/vespalib/net/http/json_handler_repo.h +++ b/vespalib/src/vespa/vespalib/net/http/json_handler_repo.h @@ -79,12 +79,13 @@ private: public: JsonHandlerRepo(); - ~JsonHandlerRepo(); + ~JsonHandlerRepo() override; Token::UP bind(vespalib::stringref path_prefix, const JsonGetHandler &get_handler); Token::UP add_root_resource(vespalib::stringref path); [[nodiscard]] std::vector<vespalib::string> get_root_resources() const; - [[nodiscard]] vespalib::string get(const vespalib::string &host, const vespalib::string &path, - const std::map<vespalib::string,vespalib::string> ¶ms) const override; + [[nodiscard]] Response get(const vespalib::string &host, const vespalib::string &path, + const std::map<vespalib::string,vespalib::string> ¶ms, + const net::ConnectionAuthContext &auth_ctx) const override; }; } // namespace vespalib diff --git a/vespalib/src/vespa/vespalib/net/http/state_api.cpp b/vespalib/src/vespa/vespalib/net/http/state_api.cpp index ca00713352f..e41e12435b9 100644 --- a/vespalib/src/vespa/vespalib/net/http/state_api.cpp +++ b/vespalib/src/vespa/vespalib/net/http/state_api.cpp @@ -1,7 +1,13 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include "state_api.h" +#include <vespa/vespalib/net/connection_auth_context.h> +#include <vespa/vespalib/net/tls/capability.h> #include <vespa/vespalib/util/jsonwriter.h> +#include <functional> + +using vespalib::net::tls::Capability; +using vespalib::net::tls::CapabilitySet; namespace vespalib { @@ -131,27 +137,59 @@ vespalib::string respond_config(ComponentConfigProducer &componentConfigProducer return json.toString(); } +JsonGetHandler::Response cap_checked(const net::ConnectionAuthContext &auth_ctx, + CapabilitySet required_caps, + std::function<vespalib::string()> fn) +{ + if (!auth_ctx.capabilities().contains_all(required_caps)) { + return JsonGetHandler::Response::make_failure(403, "Forbidden"); + } + return JsonGetHandler::Response::make_ok_with_json(fn()); +} + +JsonGetHandler::Response cap_checked(const net::ConnectionAuthContext &auth_ctx, + Capability required_cap, + std::function<vespalib::string()> fn) +{ + return cap_checked(auth_ctx, CapabilitySet::of({required_cap}), std::move(fn)); +} + } // namespace vespalib::<unnamed> -vespalib::string +JsonGetHandler::Response StateApi::get(const vespalib::string &host, const vespalib::string &path, - const std::map<vespalib::string,vespalib::string> ¶ms) const + const std::map<vespalib::string,vespalib::string> ¶ms, + const net::ConnectionAuthContext &auth_ctx) const { if (path == "/state/v1/" || path == "/state/v1") { - return respond_root(_handler_repo, host); + return cap_checked(auth_ctx, CapabilitySet::make_empty(), [&] { // TODO consider http_unclassified + return respond_root(_handler_repo, host); + }); } else if (path == "/state/v1/health") { - return respond_health(_healthProducer); + return cap_checked(auth_ctx, CapabilitySet::make_empty(), [&] { // TODO consider http_unclassified + return respond_health(_healthProducer); + }); } else if (path == "/state/v1/metrics") { // Using a 'statereporter' consumer by default removes many uninteresting per-thread // metrics but retains their aggregates. - return respond_metrics(get_consumer(params, "statereporter"), _healthProducer, _metricsProducer); + return cap_checked(auth_ctx, Capability::content_metrics_api(), [&] { + return respond_metrics(get_consumer(params, "statereporter"), _healthProducer, _metricsProducer); + }); } else if (path == "/state/v1/config") { - return respond_config(_componentConfigProducer); + return cap_checked(auth_ctx, Capability::content_state_api(), [&] { + return respond_config(_componentConfigProducer); + }); } else if (path == "/metrics/total") { - return _metricsProducer.getTotalMetrics(get_consumer(params, "")); + return cap_checked(auth_ctx, Capability::content_metrics_api(), [&] { + return _metricsProducer.getTotalMetrics(get_consumer(params, "")); + }); } else { - return _handler_repo.get(host, path, params); + // Assume this is for the nested state v1 stuff; may delegate capability check to handler later if desired. + if (!auth_ctx.capabilities().contains(Capability::content_state_api())) { + return Response::make_failure(403, "Forbidden"); + } + return _handler_repo.get(host, path, params, auth_ctx); } } diff --git a/vespalib/src/vespa/vespalib/net/http/state_api.h b/vespalib/src/vespa/vespalib/net/http/state_api.h index 2bdf320d091..ad86c2cf7c7 100644 --- a/vespalib/src/vespa/vespalib/net/http/state_api.h +++ b/vespalib/src/vespa/vespalib/net/http/state_api.h @@ -30,9 +30,10 @@ public: MetricsProducer &mp, ComponentConfigProducer &ccp); ~StateApi() override; - vespalib::string get(const vespalib::string &host, - const vespalib::string &path, - const std::map<vespalib::string,vespalib::string> ¶ms) const override; + Response get(const vespalib::string &host, + const vespalib::string &path, + const std::map<vespalib::string,vespalib::string> ¶ms, + const net::ConnectionAuthContext &auth_ctx) const override; JsonHandlerRepo &repo() { return _handler_repo; } }; diff --git a/vespalib/src/vespa/vespalib/portal/portal.h b/vespalib/src/vespa/vespalib/portal/portal.h index 6954d800c91..a25c295c083 100644 --- a/vespalib/src/vespa/vespalib/portal/portal.h +++ b/vespalib/src/vespa/vespalib/portal/portal.h @@ -83,7 +83,7 @@ private: vespalib::string prefix; GetHandler *handler; BindState(uint64_t handle_in, vespalib::string prefix_in, GetHandler &handler_in) noexcept - : handle(handle_in), prefix(prefix_in), handler(&handler_in) {} + : handle(handle_in), prefix(std::move(prefix_in)), handler(&handler_in) {} bool operator<(const BindState &rhs) const { if (prefix.size() == rhs.prefix.size()) { return (handle > rhs.handle); |