From 4c7d963f61c4f805b6dcf403a581eaa45e5d2b66 Mon Sep 17 00:00:00 2001 From: leolin49 Date: Wed, 4 Dec 2024 15:55:11 +0800 Subject: [PATCH] feat: add prometheus authentication --- CMakeLists.txt | 1 + LICENSE | 15 +++ cmake/config/trpc_config.cmake | 6 +- cmake/jwt_cpp.cmake | 38 +++++++ docs/zh/prometheus_metrics.md | 57 ++++++++++ .../com_github_thalhammer_jwt_cpp/BUILD | 0 .../jwt_cpp.BUILD | 12 ++ trpc/admin/BUILD | 8 ++ trpc/admin/admin_service.cc | 4 +- trpc/admin/prometheus_handler.cc | 105 ++++++++++++++++++ trpc/admin/prometheus_handler.h | 18 +++ trpc/metrics/prometheus/prometheus_conf.cc | 25 +++++ trpc/metrics/prometheus/prometheus_conf.h | 12 ++ .../prometheus/prometheus_conf_parser.h | 5 + .../prometheus/prometheus_conf_test.cc | 1 - trpc/metrics/prometheus/prometheus_metrics.cc | 1 - trpc/naming/common/util/hash/hash_func.cc | 4 +- trpc/naming/common/util/hash/md5.cc | 4 +- trpc/naming/common/util/hash/md5.h | 2 +- .../hash/consistenthash_load_balance.cc | 4 +- trpc/workspace.bzl | 9 ++ 21 files changed, 319 insertions(+), 12 deletions(-) create mode 100644 cmake/jwt_cpp.cmake create mode 100644 third_party/com_github_thalhammer_jwt_cpp/BUILD create mode 100644 third_party/com_github_thalhammer_jwt_cpp/jwt_cpp.BUILD diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ba21ec5..9f5ac837 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,6 +158,7 @@ include(snappy) include(lz4) include(toml11) include(flatbuffers) +include(jwt_cpp) #--------------------------------------------------------------------------------------- # Set complie options and include other libs if options are ON diff --git a/LICENSE b/LICENSE index 828d1e17..dbdcb44a 100644 --- a/LICENSE +++ b/LICENSE @@ -454,6 +454,21 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI As an exception, if, as a result of your compiling your source code, portions of this Software are embedded into a machine-executable object form of such source code, you may redistribute such embedded portions in such object form without including the above copyright and permission notices. +Open Source Software Licensed under the MIT with exceptions: +-------------------------------------------------------------------- +1. jwt-cpp +Copyright (c) 2018 Dominik Thalhammer + + +Terms of the MIT with exceptions: +-------------------------------------------------------------------- +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + Open Source Software Licensed under the MIT: -------------------------------------------------------------------- 1. murmurhash3 diff --git a/cmake/config/trpc_config.cmake b/cmake/config/trpc_config.cmake index b9d90e37..ccde1e72 100644 --- a/cmake/config/trpc_config.cmake +++ b/cmake/config/trpc_config.cmake @@ -55,7 +55,8 @@ set(INCLUDE_PATHS ${TRPC_ROOT_PATH} ${TRPC_ROOT_PATH}/cmake_third_party/nghttp2/lib/includes ${TRPC_ROOT_PATH}/cmake_third_party/picohttpparser ${TRPC_ROOT_PATH}/cmake_third_party/snappy - ${TRPC_ROOT_PATH}/cmake_third_party/lz4) + ${TRPC_ROOT_PATH}/cmake_third_party/lz4 + ${TRPC_ROOT_PATH}/cmake_third_party/jwt_cpp) # When use tRPC as a third-party library, selectively inject the header files at including any-lib.cmake. set(TARGET_INCLUDE_PATHS ${TRPC_ROOT_PATH}) @@ -117,4 +118,5 @@ set(LIB_SSL ssl crypto) set(LIB_METRICS_PROMETHEUS prometheus-cpp-core prometheus-cpp-pull - prometheus-cpp-push) + prometheus-cpp-push + jwt-cpp) diff --git a/cmake/jwt_cpp.cmake b/cmake/jwt_cpp.cmake new file mode 100644 index 00000000..5733ff98 --- /dev/null +++ b/cmake/jwt_cpp.cmake @@ -0,0 +1,38 @@ +# +# +# Tencent is pleased to support the open source community by making tRPC available. +# +# Copyright (C) 2023 THL A29 Limited, a Tencent company. +# All rights reserved. +# +# If you have downloaded a copy of the tRPC source code from Tencent, +# please note that tRPC source code is licensed under the Apache 2.0 License, +# A copy of the Apache 2.0 License is included in this file. +# +# + +include(FetchContent) + +if(NOT DEFINED JWT_VET) + set(JWT_VET 0.7.0) +endif() + +set(JWT_URL https://github.com/Thalhammer/jwt-cpp/archive/refs/tags/v${JWT_VET}.tar.gz) + +FetchContent_Declare( + jwt-cpp + URL ${JWT_URL} + SOURCE_DIR ${TRPC_ROOT_PATH}/cmake_third_party/jwt_cpp +) + +FetchContent_GetProperties(jwt-cpp) + +if(NOT jwt_cpp_POPULATED) + FetchContent_Populate(jwt-cpp) + add_subdirectory(${TRPC_ROOT_PATH}/cmake_third_party/jwt_cpp) + add_library(trpc_jwt_cpp ALIAS jwt-cpp) + set(TARGET_INCLUDE_PATHS ${TARGET_INCLUDE_PATHS} + ${TRPC_ROOT_PATH}/cmake_third_party/jwt_cpp) + set(TARGET_LINK_LIBS ${TARGET_LINK_LIBS} jwt_cpp) +endif() + diff --git a/docs/zh/prometheus_metrics.md b/docs/zh/prometheus_metrics.md index 45639599..12512427 100644 --- a/docs/zh/prometheus_metrics.md +++ b/docs/zh/prometheus_metrics.md @@ -398,3 +398,60 @@ std::vector<::prometheus::MetricFamily> Collect(); ## 通过 admin 获取 如果服务开启了 [admin 功能](./admin_service.md),则可以通过访问 `http://admin_ip:admin_port/metrics` 获取序列化为字符串后的 Prometheus 数据。 + +# 鉴权 + +Prometheus插件鉴权分为两种模式:pull模式 和 push模式,不同模式下的配置方式有所区别。 + +## pull模式 + +在pull模式下,使用Json Web Token(JWT)方式来鉴权。需要同时配置**trpc的Prometheus插件**和**Prometheus服务器**。 + +### 插件配置 + +插件配置样例如下: + +```yaml +plugins: + metrics: + prometheus: + auth_cfg: + iss: admin # issuer 签发人 + sub: prometheus-pull # subject 主题 + aud: trpc-server # audience 受众 + secret: test # 密钥 +``` + +需要配置**bearer_token**字段,该token可以通过[JWT官方工具](https://jwt.io/)生成。在payload中填写相应的iss,sub和aud字段,verify signature中填写secret字段,加密算法使用默认的 HS256。 + +## push模式 + +在push模式下,为了和pushgateway兼容,鉴权使用**username**和**password**的形式。 + +### 插件配置 + +插件配置样例如下: + +```yaml +plugins: + metrics: + prometheus: + auth_cfg: + username: admin + password: test +``` + +### Pushgateway服务器配置 + +需要在Pushgateway服务器启动时,通过带有通过**bcrypt**加密的密文的配置文件启动。Pushgateway启动的配置文件如下: + +```yaml +basic_auth_users: + admin: $2y$05$5uq4H5p8JyfQm.e16o3xduW6tkI2bTRpArTK4MF4dEuvncpz/bqy. +``` + +密码的密文可以通过htpasswd工具生成: +```shell +> htpasswd -nbB admin test +admin:$2y$05$5uq4H5p8JyfQm.e16o3xduW6tkI2bTRpArTK4MF4dEuvncpz/bqy. +``` diff --git a/third_party/com_github_thalhammer_jwt_cpp/BUILD b/third_party/com_github_thalhammer_jwt_cpp/BUILD new file mode 100644 index 00000000..e69de29b diff --git a/third_party/com_github_thalhammer_jwt_cpp/jwt_cpp.BUILD b/third_party/com_github_thalhammer_jwt_cpp/jwt_cpp.BUILD new file mode 100644 index 00000000..c4c5b647 --- /dev/null +++ b/third_party/com_github_thalhammer_jwt_cpp/jwt_cpp.BUILD @@ -0,0 +1,12 @@ +package( + default_visibility = ["//visibility:public"], +) + +cc_library( + name = "jwt-cpp", + hdrs = glob(["**/*.h"]), + deps = [ + "@com_github_openssl_openssl//:libcrypto", + "@com_github_openssl_openssl//:libssl", + ], +) diff --git a/trpc/admin/BUILD b/trpc/admin/BUILD index 2e781841..dacfe702 100644 --- a/trpc/admin/BUILD +++ b/trpc/admin/BUILD @@ -354,13 +354,21 @@ cc_library( ":admin_handler", ":base_funcs", "//trpc/util:prometheus", + "//trpc/common/config:trpc_config", + "//trpc/log:trpc_log", + "//trpc/util/http:base64", + "//trpc/util/string:string_helper", ] + select({ "//conditions:default": [], "//trpc:trpc_include_prometheus": [ "@com_github_jupp0r_prometheus_cpp//pull", + "@com_github_thalhammer_jwt_cpp//:jwt-cpp", + "//trpc/metrics/prometheus:prometheus_metrics", ], "//trpc:include_metrics_prometheus": [ "@com_github_jupp0r_prometheus_cpp//pull", + "@com_github_thalhammer_jwt_cpp//:jwt-cpp", + "//trpc/metrics/prometheus:prometheus_metrics", ], }), ) diff --git a/trpc/admin/admin_service.cc b/trpc/admin/admin_service.cc index 1ac14769..0e66a681 100644 --- a/trpc/admin/admin_service.cc +++ b/trpc/admin/admin_service.cc @@ -117,7 +117,9 @@ AdminService::AdminService() { #ifdef TRPC_BUILD_INCLUDE_PROMETHEUS // Prometheus metrics. - RegisterCmd(http::OperationType::GET, "/metrics", std::make_shared()); + auto prometheus_handle_ptr = std::make_shared(); + prometheus_handle_ptr->Init(); + RegisterCmd(http::OperationType::GET, "/metrics", prometheus_handle_ptr); #endif RegisterCmd(http::OperationType::POST, "/client_detach", std::make_shared()); diff --git a/trpc/admin/prometheus_handler.cc b/trpc/admin/prometheus_handler.cc index 54f0c83f..16476c49 100644 --- a/trpc/admin/prometheus_handler.cc +++ b/trpc/admin/prometheus_handler.cc @@ -16,15 +16,120 @@ #include "prometheus/text_serializer.h" +namespace { +bool JwtValidate(const std::string& token, std::map& auth_cfg) { + try { + auto decoded_token = jwt::decode(token); + + auto verifier = jwt::verify().allow_algorithm(jwt::algorithm::hs256{auth_cfg["secret"]}); + + // Verify if the config exists. + if (auth_cfg.count("iss")) { + verifier.with_issuer(auth_cfg["iss"]); + } + if (auth_cfg.count("sub")) { + verifier.with_subject(auth_cfg["sub"]); + } + if (auth_cfg.count("aud")) { + verifier.with_audience(auth_cfg["aud"]); + } + + verifier.verify(decoded_token); + return true; + } catch (const std::exception& e) { + TRPC_FMT_ERROR("Jwt validate error: {}", e.what()); + return false; + } +} +} // namespace + namespace trpc::admin { PrometheusHandler::PrometheusHandler() { description_ = "[GET /metrics] get prometheus metrics"; } +void PrometheusHandler::Init() { + PrometheusConfig prometheus_conf; + bool ret = TrpcConfig::GetInstance()->GetPluginConfig( + "metrics", trpc::prometheus::kPrometheusMetricsName, prometheus_conf); + if (!ret) { + TRPC_LOG_WARN( + "Failed to obtain Prometheus plugin configuration from the framework configuration file. Default configuration " + "will be used."); + } + auth_cfg_ = prometheus_conf.auth_cfg; +} + +bool PrometheusHandler::CheckTokenAuth(std::string bearer_token) { + auto splited = Split(bearer_token, ' '); + if (splited.size() != 2) { + TRPC_FMT_ERROR("error token: {}", bearer_token); + return false; + } + auto method = splited[0]; + if (method != "Bearer") { + TRPC_FMT_ERROR("error auth method: {}", method); + return false; + } + std::string token = std::string(splited[1]); + if (!JwtValidate(token, auth_cfg_)) { + TRPC_FMT_ERROR("error token: {}", token); + return false; + } + return true; +} + +bool PrometheusHandler::CheckBasicAuth(std::string token) { + auto splited = Split(token, ' '); + if (splited.size() != 2) { + TRPC_FMT_ERROR("error token: {}", token); + return false; + } + if (splited[0] != "Basic") { + TRPC_FMT_ERROR("error token: {}", token); + return false; + } + + std::string username_pwd = http::Base64Decode(std::begin(splited[1]), std::end(splited[1])); + auto sp = Split(username_pwd, ':'); + if (sp.size() != 2) { + TRPC_FMT_ERROR("error token: {}", token); + return false; + } + + auto username = sp[0], pwd = sp[1]; + if (username != auth_cfg_["username"] || pwd != auth_cfg_["password"]) { + TRPC_FMT_ERROR("error username or password: username: {}, password: {}", username, pwd); + return false; + } + return true; +} + void PrometheusHandler::CommandHandle(http::HttpRequestPtr req, rapidjson::Value& result, rapidjson::Document::AllocatorType& alloc) { static std::unique_ptr<::prometheus::Serializer> serializer = std::make_unique<::prometheus::TextSerializer>(); std::string prometheus_str = serializer->Serialize(trpc::prometheus::Collect()); + + std::string token = req->GetHeader("authorization"); + + if (!auth_cfg_.empty()) { + if (auth_cfg_.count("username") && auth_cfg_.count("password")) { + // push mode + // use the basic auth if already config the username and password. + if (!CheckBasicAuth(token)) { + result.AddMember("message", "wrong request without right username or password", alloc); + return; + } + } else { + // pull mode + // use the json web token auth. + if (!CheckTokenAuth(token)) { + result.AddMember("message", "wrong request without right token", alloc); + return; + } + } + } + result.AddMember(rapidjson::StringRef("trpc-html"), rapidjson::Value(prometheus_str, alloc).Move(), alloc); } diff --git a/trpc/admin/prometheus_handler.h b/trpc/admin/prometheus_handler.h index d7e23f6e..ad3955a2 100644 --- a/trpc/admin/prometheus_handler.h +++ b/trpc/admin/prometheus_handler.h @@ -14,8 +14,16 @@ #ifdef TRPC_BUILD_INCLUDE_PROMETHEUS #pragma once +#include + #include "trpc/admin/admin_handler.h" +#include "trpc/common/config/trpc_config.h" +#include "trpc/log/trpc_log.h" +#include "trpc/metrics/prometheus/prometheus_metrics.h" +#include "trpc/util/http/base64.h" #include "trpc/util/prometheus.h" +#include "trpc/util/string/string_helper.h" +#include "trpc/util/time.h" namespace trpc::admin { @@ -24,8 +32,18 @@ class PrometheusHandler : public AdminHandlerBase { public: PrometheusHandler(); + void Init(); + void CommandHandle(http::HttpRequestPtr req, rapidjson::Value& result, rapidjson::Document::AllocatorType& alloc) override; + + private: + bool CheckTokenAuth(std::string token); + + bool CheckBasicAuth(std::string token); + + // Yaml-cpp only supports conversion to std::map. + std::map auth_cfg_; }; } // namespace trpc::admin diff --git a/trpc/metrics/prometheus/prometheus_conf.cc b/trpc/metrics/prometheus/prometheus_conf.cc index cbff1da7..ab96dff3 100644 --- a/trpc/metrics/prometheus/prometheus_conf.cc +++ b/trpc/metrics/prometheus/prometheus_conf.cc @@ -34,3 +34,28 @@ void PrometheusConfig::Display() const { } } // namespace trpc + +namespace YAML { + +YAML::Node convert::encode(const trpc::PrometheusConfig& config) { + YAML::Node node; + node["histogram_module_cfg"] = config.histogram_module_cfg; + node["const_labels"] = config.const_labels; + node["auth_cfg"] = config.auth_cfg; + return node; +} + +bool convert::decode(const YAML::Node& node, trpc::PrometheusConfig& config) { + if (node["histogram_module_cfg"]) { + config.histogram_module_cfg = node["histogram_module_cfg"].as>(); + } + if (node["const_labels"]) { + config.const_labels = node["const_labels"].as>(); + } + if (node["auth_cfg"]) { + config.auth_cfg = node["auth_cfg"].as>(); + } + return true; +} + +} // namespace YAML diff --git a/trpc/metrics/prometheus/prometheus_conf.h b/trpc/metrics/prometheus/prometheus_conf.h index 6175c58b..1b2d2716 100644 --- a/trpc/metrics/prometheus/prometheus_conf.h +++ b/trpc/metrics/prometheus/prometheus_conf.h @@ -29,7 +29,19 @@ struct PrometheusConfig { /// The default label attached to each RPC metrics data std::map const_labels; + std::map auth_cfg; + void Display() const; }; } // namespace trpc + +namespace YAML { + +template <> +struct convert { + static YAML::Node encode(const trpc::PrometheusConfig& config); + static bool decode(const YAML::Node& node, trpc::PrometheusConfig& config); +}; + +} // namespace YAML diff --git a/trpc/metrics/prometheus/prometheus_conf_parser.h b/trpc/metrics/prometheus/prometheus_conf_parser.h index 960cc56d..985965ed 100644 --- a/trpc/metrics/prometheus/prometheus_conf_parser.h +++ b/trpc/metrics/prometheus/prometheus_conf_parser.h @@ -25,6 +25,7 @@ struct convert { YAML::Node node; node["histogram_module_cfg"] = conf.histogram_module_cfg; node["const_labels"] = conf.const_labels; + node["auth_cfg"] = conf.auth_cfg; return node; } @@ -37,6 +38,10 @@ struct convert { if (node["const_labels"]) { conf.const_labels = node["const_labels"].as>(); } + + if (node["auth_cfg"]) { + conf.auth_cfg = node["auth_cfg"].as>(); + } return true; } }; diff --git a/trpc/metrics/prometheus/prometheus_conf_test.cc b/trpc/metrics/prometheus/prometheus_conf_test.cc index da298125..65117161 100644 --- a/trpc/metrics/prometheus/prometheus_conf_test.cc +++ b/trpc/metrics/prometheus/prometheus_conf_test.cc @@ -12,7 +12,6 @@ // #include "trpc/metrics/prometheus/prometheus_conf.h" -#include "trpc/metrics/prometheus/prometheus_conf_parser.h" #include "gtest/gtest.h" #include "yaml-cpp/yaml.h" diff --git a/trpc/metrics/prometheus/prometheus_metrics.cc b/trpc/metrics/prometheus/prometheus_metrics.cc index d16c0aad..490c0732 100644 --- a/trpc/metrics/prometheus/prometheus_metrics.cc +++ b/trpc/metrics/prometheus/prometheus_metrics.cc @@ -15,7 +15,6 @@ #include "trpc/metrics/prometheus/prometheus_metrics.h" #include "trpc/common/config/trpc_config.h" -#include "trpc/metrics/prometheus/prometheus_conf_parser.h" namespace trpc { diff --git a/trpc/naming/common/util/hash/hash_func.cc b/trpc/naming/common/util/hash/hash_func.cc index 199e3e4f..0609ad7d 100644 --- a/trpc/naming/common/util/hash/hash_func.cc +++ b/trpc/naming/common/util/hash/hash_func.cc @@ -13,7 +13,7 @@ #include "trpc/naming/common/util/hash/hash_func.h" -#include +#include #include #include "trpc/naming/common/util/hash/city.h" @@ -100,4 +100,4 @@ std::uint64_t Hash(const std::string& input, const HashFuncName& hash_func, uint return GetHash(input, hash_func) % num; } -} // namespace trpc \ No newline at end of file +} // namespace trpc diff --git a/trpc/naming/common/util/hash/md5.cc b/trpc/naming/common/util/hash/md5.cc index ed1b73e0..23ec6fda 100644 --- a/trpc/naming/common/util/hash/md5.cc +++ b/trpc/naming/common/util/hash/md5.cc @@ -36,8 +36,8 @@ // documentation and/or software. #include "trpc/naming/common/util/hash/md5.h" -#include #include +#include /* * MD5 context setup */ @@ -269,4 +269,4 @@ void Md5_32(const void* key, int len, uint32_t /*seed*/, void* out) { Md5((unsigned char*)key, len, (unsigned char*)hash); *(uint32_t*)out = hash[0]; -} \ No newline at end of file +} diff --git a/trpc/naming/common/util/hash/md5.h b/trpc/naming/common/util/hash/md5.h index 47e0dfbc..780e2dd3 100644 --- a/trpc/naming/common/util/hash/md5.h +++ b/trpc/naming/common/util/hash/md5.h @@ -40,7 +40,7 @@ #pragma once -#include +#include #ifndef GET_ULONG_LE #define GET_ULONG_LE(n, b, i) \ diff --git a/trpc/naming/common/util/loadbalance/hash/consistenthash_load_balance.cc b/trpc/naming/common/util/loadbalance/hash/consistenthash_load_balance.cc index c125fb22..bb648d52 100644 --- a/trpc/naming/common/util/loadbalance/hash/consistenthash_load_balance.cc +++ b/trpc/naming/common/util/loadbalance/hash/consistenthash_load_balance.cc @@ -14,8 +14,8 @@ #include "trpc/naming/common/util/loadbalance/hash/consistenthash_load_balance.h" #include -#include #include +#include #include #include @@ -152,4 +152,4 @@ int ConsistentHashLoadBalance::Next(LoadBalanceResult& result) { return 0; } -} // namespace trpc \ No newline at end of file +} // namespace trpc diff --git a/trpc/workspace.bzl b/trpc/workspace.bzl index 6fad32fc..7fa24388 100644 --- a/trpc/workspace.bzl +++ b/trpc/workspace.bzl @@ -350,3 +350,12 @@ def trpc_workspace(path_prefix = "", repo_name = "", **kwargs): remote = "https://github.com/trpc-group/trpc.git", tag = "v{ver}".format(ver = kwargs.get("com_github_trpc_protocol_ver", "1.0.0")), ) + + # com_github_thalhammer_jwt_cpp + http_archive( + name = "com_github_thalhammer_jwt_cpp", + strip_prefix = "jwt-cpp-0.7.0", + urls = ["https://github.com/Thalhammer/jwt-cpp/archive/refs/tags/v0.7.0.tar.gz"], + sha256 = "b9eb270e3ba8221e4b2bc38723c9a1cb4fa6c241a42908b9a334daff31137406", + build_file = clean_dep("//third_party/com_github_thalhammer_jwt_cpp:jwt_cpp.BUILD"), + )