diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 00000000000..75a93323f7c --- /dev/null +++ b/.bazelrc @@ -0,0 +1,60 @@ +# Copied from https://github.com/envoyproxy/envoy/blob/master/tools/bazel.rc +# Envoy specific Bazel build/test options. + +build --workspace_status_command=tools/bazel_get_workspace_status + +# Basic ASAN/UBSAN that works for gcc +build:asan --define ENVOY_CONFIG_ASAN=1 +build:asan --copt -fsanitize=address,undefined +build:asan --linkopt -fsanitize=address,undefined +build:asan --copt -fno-sanitize=vptr +build:asan --linkopt -fno-sanitize=vptr +build:asan --linkopt -ldl +build:asan --define tcmalloc=disabled +build:asan --build_tag_filters=-no_asan +build:asan --test_tag_filters=-no_asan +build:asan --define signal_trace=disabled + +# Clang 5.0 ASAN +build:clang-asan --define ENVOY_CONFIG_ASAN=1 +build:clang-asan --copt -D__SANITIZE_ADDRESS__ +build:clang-asan --copt -fsanitize=address,undefined +build:clang-asan --linkopt -fsanitize=address,undefined +build:clang-asan --copt -fno-sanitize=vptr +build:clang-asan --linkopt -fno-sanitize=vptr +build:clang-asan --copt -fno-sanitize-recover=all +build:clang-asan --linkopt -ldl +build:clang-asan --define tcmalloc=disabled +build:clang-asan --build_tag_filters=-no_asan +build:clang-asan --test_tag_filters=-no_asan +build:clang-asan --define signal_trace=disabled +build:clang-asan --test_env=ASAN_SYMBOLIZER_PATH + +# Clang 5.0 TSAN +build:clang-tsan --define ENVOY_CONFIG_TSAN=1 +build:clang-tsan --copt -fsanitize=thread +build:clang-tsan --linkopt -fsanitize=thread +build:clang-tsan --define tcmalloc=disabled + +# Clang 5.0 MSAN - broken today since we need to rebuild lib[std]c++ and external deps with MSAN +# support (see https://github.com/envoyproxy/envoy/issues/443). +build:clang-msan --define ENVOY_CONFIG_MSAN=1 +build:clang-msan --copt -fsanitize=memory +build:clang-msan --linkopt -fsanitize=memory +build:clang-msan --define tcmalloc=disabled +build:clang-msan --copt -fsanitize-memory-track-origins=2 + +# Test options +test --test_env=HEAPCHECK=normal --test_env=PPROF_PATH + +# Release builds without debug symbols. +build:release -c opt +build:release --strip=always + +# Release builds with debug symbols +build:release-symbol -c opt + +# Add compile option for all C++ files +build --cxxopt -Wnon-virtual-dtor +build --cxxopt -Wformat +build --cxxopt -Wformat-security diff --git a/.circleci/Dockerfile b/.circleci/Dockerfile index 1b331f529fc..63e353f1877 100644 --- a/.circleci/Dockerfile +++ b/.circleci/Dockerfile @@ -19,7 +19,11 @@ RUN sudo apt-get update && \ clang-6.0 clang-format-6.0 rsync ninja-build # ~100M, depends on g++, zlib1g-dev, bash-completions +<<<<<<< HEAD RUN curl -Lo /tmp/bazel.deb https://github.com/bazelbuild/bazel/releases/download/0.15.2/bazel_0.15.2-linux-x86_64.deb && \ +======= +RUN curl -Lo /tmp/bazel.deb https://github.com/bazelbuild/bazel/releases/download/0.18.0/bazel_0.18.0-linux-x86_64.deb && \ +>>>>>>> upstream/release-1.1 sudo dpkg -i /tmp/bazel.deb && rm /tmp/bazel.deb diff --git a/.circleci/Makefile b/.circleci/Makefile index aa7ddab12a2..cdcf618f25b 100644 --- a/.circleci/Makefile +++ b/.circleci/Makefile @@ -2,7 +2,11 @@ HUB ?= PROJECT ?= istio # Using same naming convention as istio/istio +<<<<<<< HEAD VERSION ?= go1.10-bazel0.15-clang6.0 +======= +VERSION ?= go1.10-bazel0.18-clang6.0 +>>>>>>> upstream/release-1.1 IMG ?= ci # Build a local image, can be used for testing with circleci command line. diff --git a/.circleci/config.yml b/.circleci/config.yml index db59eae9322..b91adec4b24 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2 jobs: build: docker: - - image: istio/ci:go1.10-bazel0.15-clang6.0 + - image: istio/ci:go1.10-bazel0.18-clang6.0 environment: - BAZEL_TEST_ARGS: "--test_env=ENVOY_IP_TEST_VERSIONS=v4only --test_output=all" resource_class: xlarge @@ -30,7 +30,7 @@ jobs: destination: /proxy/bin linux_asan: docker: - - image: istio/ci:go1.10-bazel0.15-clang6.0 + - image: istio/ci:go1.10-bazel0.18-clang6.0 environment: - BAZEL_TEST_ARGS: "--test_env=ENVOY_IP_TEST_VERSIONS=v4only --test_output=all" resource_class: xlarge @@ -49,7 +49,7 @@ jobs: - /home/circleci/.cache/bazel linux_tsan: docker: - - image: istio/ci:go1.10-bazel0.15-clang6.0 + - image: istio/ci:go1.10-bazel0.18-clang6.0 environment: - BAZEL_TEST_ARGS: "--test_env=ENVOY_IP_TEST_VERSIONS=v4only --test_output=all" resource_class: xlarge @@ -80,12 +80,12 @@ jobs: - checkout - restore_cache: keys: - - macos_fastbuild_v2-bazel-cache-{{ checksum "WORKSPACE" }} + - macos_fastbuild-bazel-cache-{{ checksum "WORKSPACE" }} - run: rm ~/.gitconfig - run: make build_envoy - run: make test - save_cache: - key: macos_fastbuild_v2-bazel-cache-{{ checksum "WORKSPACE" }} + key: macos_fastbuild-bazel-cache-{{ checksum "WORKSPACE" }} paths: - /Users/distiller/.cache/bazel diff --git a/BUILD b/BUILD index 37252ae00af..f92e4715753 100644 --- a/BUILD +++ b/BUILD @@ -33,6 +33,3 @@ genrule( visibility = ["//visibility:public"], ) -load("@io_bazel_rules_go//go:def.bzl", "go_prefix") - -go_prefix("istio.io/proxy") diff --git a/Makefile b/Makefile index 501d2ebb14f..6ccc580258e 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ ARTIFACTS_DIR ?= $(LOCAL_ARTIFACTS_DIR) BAZEL_STARTUP_ARGS ?= BAZEL_BUILD_ARGS ?= BAZEL_TEST_ARGS ?= +BAZEL_TARGETS ?= //... HUB ?= TAG ?= ifeq "$(origin CC)" "default" @@ -30,7 +31,7 @@ CXX := clang++-6.0 endif build: - CC=$(CC) CXX=$(CXX) bazel $(BAZEL_STARTUP_ARGS) build $(BAZEL_BUILD_ARGS) //... + CC=$(CC) CXX=$(CXX) bazel $(BAZEL_STARTUP_ARGS) build $(BAZEL_BUILD_ARGS) $(BAZEL_TARGETS) @bazel shutdown # Build only envoy - fast @@ -43,19 +44,20 @@ clean: @bazel shutdown test: - CC=$(CC) CXX=$(CXX) bazel $(BAZEL_STARTUP_ARGS) test $(BAZEL_TEST_ARGS) //... + CC=$(CC) CXX=$(CXX) bazel $(BAZEL_STARTUP_ARGS) test $(BAZEL_TEST_ARGS) $(BAZEL_TARGETS) @bazel shutdown test_asan: - CC=$(CC) CXX=$(CXX) bazel $(BAZEL_STARTUP_ARGS) test $(BAZEL_TEST_ARGS) --config=clang-asan //... + CC=$(CC) CXX=$(CXX) bazel $(BAZEL_STARTUP_ARGS) test $(BAZEL_TEST_ARGS) --config=clang-asan $(BAZEL_TARGETS) @bazel shutdown test_tsan: - CC=$(CC) CXX=$(CXX) bazel $(BAZEL_STARTUP_ARGS) test $(BAZEL_TEST_ARGS) --config=clang-tsan //... + CC=$(CC) CXX=$(CXX) bazel $(BAZEL_STARTUP_ARGS) test $(BAZEL_TEST_ARGS) --config=clang-tsan $(BAZEL_TARGETS) @bazel shutdown check: @script/check-license-headers + @script/check-repositories @script/check-style artifacts: build diff --git a/WORKSPACE b/WORKSPACE index a229ef4d242..89342046ac8 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -15,6 +15,9 @@ ################################################################################ # +# http_archive is not a native function since bazel 0.19 +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + load( "//:repositories.bzl", "googletest_repositories", @@ -30,12 +33,16 @@ bind( ) # When updating envoy sha manually please update the sha in istio.deps file also -ENVOY_SHA = "15577326dbb4ebacf34990d1686be6f706ac02e4" +# +# Determine SHA256 `wget https://github.com/envoyproxy/envoy/archive/COMMIT.tar.gz && sha256sum COMMIT.tar.gz` +ENVOY_SHA = "b3be5713f2100ab5c40316e73ce34581245bd26a" +ENVOY_SHA256 = "79629284ae143d66b873c08883dc6382fac2e8ed45f6f3521f7e7282b6650216" http_archive( name = "envoy", strip_prefix = "envoy-" + ENVOY_SHA, - url = "https://github.com/istio/envoy/archive/" + ENVOY_SHA + ".zip", + url = "https://github.com/envoyproxy/envoy/archive/" + ENVOY_SHA + ".tar.gz", + sha256 = ENVOY_SHA256, ) load("@envoy//bazel:repositories.bzl", "envoy_dependencies") @@ -48,14 +55,16 @@ load("@envoy_api//bazel:repositories.bzl", "api_dependencies") api_dependencies() load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains") -load("@com_lyft_protoc_gen_validate//bazel:go_proto_library.bzl", "go_proto_repositories") -go_proto_repositories(shared=0) go_rules_dependencies() go_register_toolchains() -load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") -git_repository( +# Nov 28, 2017 (bazel 0.8.0 support) +RULES_PROTOBUF_SHA = "563b674a2ce6650d459732932ea2bc98c9c9a9bf" +RULES_PROTOBUF_SHA256 = "338e0d65cd709c6a6f9b5702466e641d536479be8b564d1e12a5d1de22a5cff6" + +http_archive( name = "org_pubref_rules_protobuf", - commit = "563b674a2ce6650d459732932ea2bc98c9c9a9bf", # Nov 28, 2017 (bazel 0.8.0 support) - remote = "https://github.com/pubref/rules_protobuf", + strip_prefix = "rules_protobuf-" + RULES_PROTOBUF_SHA, + url = "https://github.com/pubref/rules_protobuf/archive/" + RULES_PROTOBUF_SHA + ".tar.gz", + sha256 = RULES_PROTOBUF_SHA256, ) diff --git a/cc_gogo_protobuf.bzl b/cc_gogo_protobuf.bzl index 4f549fd2e29..18e5563ca23 100644 --- a/cc_gogo_protobuf.bzl +++ b/cc_gogo_protobuf.bzl @@ -14,8 +14,10 @@ # ################################################################################ # +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") -load("@bazel_tools//tools/build_defs/repo:git.bzl", "new_git_repository") +GOGO_PROTO_SHA = "100ba4e885062801d56799d78530b73b178a78f3" +GOGO_PROTO_SHA256 = "b04eb8eddd2d15d8b12d111d4ef7816fca6e5c5d495adf45fb8478278aa80f79" def cc_gogoproto_repositories(bind=True): BUILD = """ @@ -56,10 +58,11 @@ cc_proto_library( ], ) """ - new_git_repository( + http_archive( name = "gogoproto_git", - commit = "100ba4e885062801d56799d78530b73b178a78f3", - remote = "https://github.com/gogo/protobuf", + strip_prefix = "protobuf-" + GOGO_PROTO_SHA, + url = "https://github.com/gogo/protobuf/archive/" + GOGO_PROTO_SHA + ".tar.gz", + sha256 = GOGO_PROTO_SHA256, build_file_content = BUILD, ) diff --git a/googleapis.bzl b/googleapis.bzl index 62cbd0e33ab..22ca9128793 100644 --- a/googleapis.bzl +++ b/googleapis.bzl @@ -14,8 +14,11 @@ # ################################################################################ # +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") -load("@bazel_tools//tools/build_defs/repo:git.bzl", "new_git_repository") +# Oct 21, 2016 (only release pre-dates sha) +GOOGLEAPIS_SHA = "13ac2436c5e3d568bd0e938f6ed58b77a48aba15" +GOOGLEAPIS_SHA256 = "f48956fb8c55617ed052c20884465f06b9a57b807164431185be397ea46993ca" def googleapis_repositories(bind=True): GOOGLEAPIS_BUILD_FILE = """ @@ -37,11 +40,12 @@ cc_proto_library( ) """ - new_git_repository( + http_archive( name = "com_github_googleapis_googleapis", build_file_content = GOOGLEAPIS_BUILD_FILE, - commit = "13ac2436c5e3d568bd0e938f6ed58b77a48aba15", # Oct 21, 2016 (only release pre-dates sha) - remote = "https://github.com/googleapis/googleapis.git", + strip_prefix = "googleapis-" + GOOGLEAPIS_SHA, + url = "https://github.com/googleapis/googleapis/archive/" + GOOGLEAPIS_SHA + ".tar.gz", + sha256 = GOOGLEAPIS_SHA256, ) if bind: diff --git a/include/istio/control/http/controller.h b/include/istio/control/http/controller.h index cf2c8757b05..0ab4ae0febb 100644 --- a/include/istio/control/http/controller.h +++ b/include/istio/control/http/controller.h @@ -18,6 +18,8 @@ #include "include/istio/control/http/request_handler.h" #include "include/istio/mixerclient/client.h" +#include "include/istio/utils/attribute_names.h" +#include "include/istio/utils/local_attributes.h" #include "mixer/v1/config/client/client_config.pb.h" namespace istio { @@ -69,8 +71,9 @@ class Controller { // * some functions provided by the environment (Envoy) // * optional service config cache size. struct Options { - Options(const ::istio::mixer::v1::config::client::HttpClientConfig& config) - : config(config) {} + Options(const ::istio::mixer::v1::config::client::HttpClientConfig& config, + const ::istio::utils::LocalNode& local_node) + : config(config), local_node(local_node) {} // Mixer filter config const ::istio::mixer::v1::config::client::HttpClientConfig& config; @@ -81,6 +84,8 @@ class Controller { // The LRU cache size for service config. // If not set or is 0 default value, the cache size is 1000. int service_config_cache_size{}; + + const ::istio::utils::LocalNode& local_node; }; // The factory function to create a new instance of the controller. diff --git a/include/istio/control/http/report_data.h b/include/istio/control/http/report_data.h index 11b1e7e9f59..8cf7326fe0f 100644 --- a/include/istio/control/http/report_data.h +++ b/include/istio/control/http/report_data.h @@ -19,6 +19,8 @@ #include #include +#include "google/protobuf/struct.pb.h" + namespace istio { namespace control { namespace http { @@ -42,27 +44,32 @@ class ReportData { int response_code; std::string response_flags; }; - virtual void GetReportInfo(ReportInfo* info) const = 0; + virtual void GetReportInfo(ReportInfo *info) const = 0; // Get destination ip/port. - virtual bool GetDestinationIpPort(std::string* ip, int* port) const = 0; + virtual bool GetDestinationIpPort(std::string *ip, int *port) const = 0; // Get Rbac attributes. struct RbacReportInfo { std::string permissive_resp_code; std::string permissive_policy_id; }; - virtual bool GetRbacReportInfo(RbacReportInfo* report_info) const = 0; + virtual bool GetRbacReportInfo(RbacReportInfo *report_info) const = 0; // Get upstream host UID. This value overrides the value in the report bag. - virtual bool GetDestinationUID(std::string* uid) const = 0; + virtual bool GetDestinationUID(std::string *uid) const = 0; // gRPC status info struct GrpcStatus { std::string status; std::string message; }; - virtual bool GetGrpcStatus(GrpcStatus* status) const = 0; + virtual bool GetGrpcStatus(GrpcStatus *status) const = 0; + + // Get dynamic metadata generated by Envoy filters. + // Useful for logging info generated by custom codecs. + virtual const ::google::protobuf::Map + &GetDynamicFilterState() const = 0; }; } // namespace http diff --git a/include/istio/control/http/request_handler.h b/include/istio/control/http/request_handler.h index ed863c7f468..916b6191dc0 100644 --- a/include/istio/control/http/request_handler.h +++ b/include/istio/control/http/request_handler.h @@ -42,24 +42,10 @@ class RequestHandler { // Make a Report call. It will: // * check service config to see if Report is required + // * extract check attributes if not done yet. // * extract more report attributes // * make a Report call. - virtual void Report(ReportData* report_data) = 0; - - // Extract the request attributes for Report() call. - // This is called at Report time when Check() is not called. - // Normally request attributes are extracted at Check() call. - // This is for cases the requests are rejected by http filters - // before mixer, such as fault injection, or auth. - // - // Usage: at Envoy filter::log() function - // if (!hander) { - // handle = control->CreateHandler(); - // handler->ExtractRequestAttributes(); - // } - // handler->Report(); - // - virtual void ExtractRequestAttributes(CheckData* check_data) = 0; + virtual void Report(CheckData* check_data, ReportData* report_data) = 0; }; } // namespace http diff --git a/include/istio/control/tcp/controller.h b/include/istio/control/tcp/controller.h index ff2e40a203e..fa2ce3255fe 100644 --- a/include/istio/control/tcp/controller.h +++ b/include/istio/control/tcp/controller.h @@ -18,6 +18,7 @@ #include "include/istio/control/tcp/request_handler.h" #include "include/istio/mixerclient/client.h" +#include "include/istio/utils/local_attributes.h" #include "mixer/v1/config/client/client_config.pb.h" namespace istio { @@ -39,14 +40,17 @@ class Controller { // * mixer_config: the mixer client config. // * some functions provided by the environment (Envoy) struct Options { - Options(const ::istio::mixer::v1::config::client::TcpClientConfig& config) - : config(config) {} + Options(const ::istio::mixer::v1::config::client::TcpClientConfig& config, + const ::istio::utils::LocalNode& local_node) + : config(config), local_node(local_node) {} // Mixer filter config const ::istio::mixer::v1::config::client::TcpClientConfig& config; // Some plaform functions for mixer client library. ::istio::mixerclient::Environment env; + + const ::istio::utils::LocalNode& local_node; }; // The factory function to create a new instance of the controller. diff --git a/include/istio/control/tcp/report_data.h b/include/istio/control/tcp/report_data.h index 7535b5904ba..91a6b7241c0 100644 --- a/include/istio/control/tcp/report_data.h +++ b/include/istio/control/tcp/report_data.h @@ -19,6 +19,8 @@ #include #include +#include "google/protobuf/struct.pb.h" + namespace istio { namespace control { namespace tcp { @@ -30,7 +32,7 @@ class ReportData { virtual ~ReportData() {} // Get upstream tcp connection IP and port. IP is returned in format of bytes. - virtual bool GetDestinationIpPort(std::string* ip, int* port) const = 0; + virtual bool GetDestinationIpPort(std::string *ip, int *port) const = 0; // Get additional report data. struct ReportInfo { @@ -38,10 +40,10 @@ class ReportData { uint64_t received_bytes; std::chrono::nanoseconds duration; }; - virtual void GetReportInfo(ReportInfo* info) const = 0; + virtual void GetReportInfo(ReportInfo *info) const = 0; // Get upstream host UID. This value overrides the value in the report bag. - virtual bool GetDestinationUID(std::string* uid) const = 0; + virtual bool GetDestinationUID(std::string *uid) const = 0; // ConnectionEvent is used to indicates the tcp connection event in Report // call. @@ -50,6 +52,12 @@ class ReportData { CLOSE, CONTINUE, }; + + // Get dynamic metadata generated by Envoy filters. + // Useful for logging info generated by custom codecs. + virtual const ::google::protobuf::Map<::std::string, + ::google::protobuf::Struct> + &GetDynamicFilterState() const = 0; }; } // namespace tcp diff --git a/include/istio/mixerclient/check_response.h b/include/istio/mixerclient/check_response.h index bd9690533af..86000628b1e 100644 --- a/include/istio/mixerclient/check_response.h +++ b/include/istio/mixerclient/check_response.h @@ -17,7 +17,7 @@ #define ISTIO_MIXERCLIENT_CHECK_RESPONSE_H #include "google/protobuf/stubs/status.h" -#include "mixer/v1/check.pb.h" +#include "mixer/v1/mixer.pb.h" namespace istio { namespace mixerclient { diff --git a/include/istio/mixerclient/environment.h b/include/istio/mixerclient/environment.h index 9e24f9e8d15..499e5bd269b 100644 --- a/include/istio/mixerclient/environment.h +++ b/include/istio/mixerclient/environment.h @@ -18,7 +18,7 @@ #include "check_response.h" #include "google/protobuf/stubs/status.h" -#include "mixer/v1/service.pb.h" +#include "mixer/v1/mixer.pb.h" #include "timer.h" namespace istio { diff --git a/include/istio/mixerclient/options.h b/include/istio/mixerclient/options.h index cac81b392c4..f24a0a17f23 100644 --- a/include/istio/mixerclient/options.h +++ b/include/istio/mixerclient/options.h @@ -45,8 +45,8 @@ struct CheckOptions { // Options controlling report batch. struct ReportOptions { // Default constructor. - // Default to batch up to 1000 reports or 1 seconds. - ReportOptions() : max_batch_entries(1000), max_batch_time_ms(1000) {} + // Default to batch up to 500 reports or 1 seconds. + ReportOptions() : max_batch_entries(100), max_batch_time_ms(1000) {} // Constructor. ReportOptions(int max_batch_entries, int max_batch_time_ms) diff --git a/include/istio/utils/BUILD b/include/istio/utils/BUILD index 92bb7e084bd..788c4e8a8c1 100644 --- a/include/istio/utils/BUILD +++ b/include/istio/utils/BUILD @@ -18,7 +18,8 @@ cc_library( name = "headers_lib", hdrs = [ "attributes_builder.h", - "md5.h", + "concat_hash.h", + "local_attributes.h", "protobuf.h", "status.h", ], diff --git a/include/istio/utils/attribute_names.h b/include/istio/utils/attribute_names.h index 84e7415837d..3806cc4f607 100644 --- a/include/istio/utils/attribute_names.h +++ b/include/istio/utils/attribute_names.h @@ -27,8 +27,15 @@ struct AttributeName { // https://github.com/istio/istio/issues/4689 static const char kSourceUser[]; static const char kSourcePrincipal[]; + static const char kSourceNamespace[]; + static const char kSourceUID[]; static const char kDestinationPrincipal[]; + static const char kDestinationServiceName[]; + static const char kDestinationServiceUID[]; + static const char kDestinationServiceHost[]; + static const char kDestinationServiceNamespace[]; + static const char kRequestHeaders[]; static const char kRequestHost[]; static const char kRequestMethod[]; @@ -62,9 +69,10 @@ struct AttributeName { static const char kDestinationIp[]; static const char kDestinationPort[]; static const char kDestinationUID[]; + static const char kDestinationNamespace[]; static const char kOriginIp[]; - static const char kConnectionReceviedBytes[]; - static const char kConnectionReceviedTotalBytes[]; + static const char kConnectionReceivedBytes[]; + static const char kConnectionReceivedTotalBytes[]; static const char kConnectionSendBytes[]; static const char kConnectionSendTotalBytes[]; static const char kConnectionDuration[]; @@ -76,8 +84,10 @@ struct AttributeName { // Context attributes static const char kContextProtocol[]; + static const char kContextReporterKind[]; static const char kContextTime[]; static const char kContextProxyErrorCode[]; + static const char kContextReporterUID[]; // Check error code and message. static const char kCheckErrorCode[]; diff --git a/include/istio/utils/attributes_builder.h b/include/istio/utils/attributes_builder.h index eae9836f4d0..c48d872775c 100644 --- a/include/istio/utils/attributes_builder.h +++ b/include/istio/utils/attributes_builder.h @@ -32,32 +32,32 @@ namespace utils { // .Add("key2", value2); class AttributesBuilder { public: - AttributesBuilder(::istio::mixer::v1::Attributes* attributes) + AttributesBuilder(::istio::mixer::v1::Attributes *attributes) : attributes_(attributes) {} - void AddString(const std::string& key, const std::string& str) { + void AddString(const std::string &key, const std::string &str) { (*attributes_->mutable_attributes())[key].set_string_value(str); } - void AddBytes(const std::string& key, const std::string& bytes) { + void AddBytes(const std::string &key, const std::string &bytes) { (*attributes_->mutable_attributes())[key].set_bytes_value(bytes); } - void AddInt64(const std::string& key, int64_t value) { + void AddInt64(const std::string &key, int64_t value) { (*attributes_->mutable_attributes())[key].set_int64_value(value); } - void AddDouble(const std::string& key, double value) { + void AddDouble(const std::string &key, double value) { (*attributes_->mutable_attributes())[key].set_double_value(value); } - void AddBool(const std::string& key, bool value) { + void AddBool(const std::string &key, bool value) { (*attributes_->mutable_attributes())[key].set_bool_value(value); } void AddTimestamp( - const std::string& key, - const std::chrono::time_point& value) { + const std::string &key, + const std::chrono::time_point &value) { auto time_stamp = (*attributes_->mutable_attributes())[key].mutable_timestamp_value(); long long nanos = std::chrono::duration_cast( @@ -67,16 +67,16 @@ class AttributesBuilder { time_stamp->set_nanos(nanos % 1000000000); } - void AddDuration(const std::string& key, - const std::chrono::nanoseconds& value) { + void AddDuration(const std::string &key, + const std::chrono::nanoseconds &value) { auto duration = (*attributes_->mutable_attributes())[key].mutable_duration_value(); duration->set_seconds(value.count() / 1000000000); duration->set_nanos(value.count() % 1000000000); } - void AddStringMap(const std::string& key, - const std::map& string_map) { + void AddStringMap(const std::string &key, + const std::map &string_map) { if (string_map.size() == 0) { return; } @@ -84,13 +84,13 @@ class AttributesBuilder { .mutable_string_map_value() ->mutable_entries(); entries->clear(); - for (const auto& map_it : string_map) { + for (const auto &map_it : string_map) { (*entries)[map_it.first] = map_it.second; } } - void AddProtoStructStringMap(const std::string& key, - const google::protobuf::Struct& struct_map) { + void AddProtoStructStringMap(const std::string &key, + const google::protobuf::Struct &struct_map) { if (struct_map.fields().empty()) { return; } @@ -98,25 +98,61 @@ class AttributesBuilder { .mutable_string_map_value() ->mutable_entries(); entries->clear(); - for (const auto& field : struct_map.fields()) { - // Ignore all fields that are not string. + for (const auto &field : struct_map.fields()) { + // Ignore all fields that are not string or string list. switch (field.second.kind_case()) { case google::protobuf::Value::kStringValue: (*entries)[field.first] = field.second.string_value(); break; + case google::protobuf::Value::kListValue: + if (field.second.list_value().values_size() > 0) { + // The items in the list is converted into a + // comma separated string + std::string s; + for (int i = 0; i < field.second.list_value().values_size(); i++) { + s += field.second.list_value().values().Get(i).string_value(); + if (i + 1 < field.second.list_value().values_size()) { + s += ","; + } + } + (*entries)[field.first] = s; + } + break; default: break; } } + + if (entries->empty()) { + attributes_->mutable_attributes()->erase(key); + } + } + + // Serializes all the keys in a map and builds attributes. + // for example, foo.bar.com: struct {str:abc, list:[c,d,e]} will be emitted as + // foo.bar.com: string_map[str:abc, list: c,d,e] + // Only extracts strings and lists. + // TODO: add the ability to pack bools and nums as strings and recurse down + // the struct. + void FlattenMapOfStringToStruct( + const ::google::protobuf::Map<::std::string, ::google::protobuf::Struct> + &filter_state) { + if (filter_state.empty()) { + return; + } + + for (const auto &filter : filter_state) { + AddProtoStructStringMap(filter.first, filter.second); + } } - bool HasAttribute(const std::string& key) const { - const auto& attrs_map = attributes_->attributes(); + bool HasAttribute(const std::string &key) const { + const auto &attrs_map = attributes_->attributes(); return attrs_map.find(key) != attrs_map.end(); } private: - ::istio::mixer::v1::Attributes* attributes_; + ::istio::mixer::v1::Attributes *attributes_; }; } // namespace utils diff --git a/include/istio/utils/concat_hash.h b/include/istio/utils/concat_hash.h new file mode 100644 index 00000000000..8906f07089d --- /dev/null +++ b/include/istio/utils/concat_hash.h @@ -0,0 +1,65 @@ +/* Copyright 2017 Istio Authors. All Rights Reserved. + * + * Licensed 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. + */ + +#ifndef ISTIO_UTILS_CONCAT_HASH_H_ +#define ISTIO_UTILS_CONCAT_HASH_H_ + +#include +#include +#include + +namespace istio { +namespace utils { + +// The hash type for Check cache. +typedef std::size_t HashType; + +// This class concatenates multiple values into a string as hash +class ConcatHash { + public: + ConcatHash(size_t reserve_size) { hash_.reserve(reserve_size); } + + // Updates the context with data. + ConcatHash& Update(const void* data, size_t size) { + hash_.append(static_cast(data), size); + return *this; + } + + // A helper function for int + ConcatHash& Update(int d) { return Update(&d, sizeof(d)); } + + // A helper function for const char* + ConcatHash& Update(const char* str) { + hash_.append(str); + return *this; + } + + // A helper function for const string + ConcatHash& Update(const std::string& str) { + hash_.append(str); + return *this; + } + + // Returns the hash of the concated string. + HashType getHash() const { return std::hash{}(hash_); } + + private: + std::string hash_; +}; + +} // namespace utils +} // namespace istio + +#endif // ISTIO_UTILS_CONCAT_HASH_H_ diff --git a/include/istio/utils/local_attributes.h b/include/istio/utils/local_attributes.h new file mode 100644 index 00000000000..0935e3c93b3 --- /dev/null +++ b/include/istio/utils/local_attributes.h @@ -0,0 +1,58 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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. + */ + +#ifndef ISTIO_UTILS_LOCAL_ATTRIBUTES_H +#define ISTIO_UTILS_LOCAL_ATTRIBUTES_H + +#include "mixer/v1/attributes.pb.h" + +namespace istio { +namespace utils { + +struct LocalAttributes { + // local inbound attributes + ::istio::mixer::v1::Attributes inbound; + + // local outbound attributes + ::istio::mixer::v1::Attributes outbound; + + // local forward attributes + ::istio::mixer::v1::Attributes forward; +}; + +// LocalNode is abstract information about the node from Mixer's perspective. +struct LocalNode { + // like kubernetes://podname.namespace + std::string uid; + + // namespace + std::string ns; +}; + +void CreateLocalAttributes(const LocalNode& local, + LocalAttributes* local_attributes); + +// create preserialized header to send to proxy that is fronting mixer. +// This header is used for istio self monitoring. +bool SerializeForwardedAttributes(const LocalNode& local, + std::string* serialized_forward_attributes); + +// check if this listener is outbound based on "context.reporter.kind" attribute +bool IsOutbound(const ::istio::mixer::v1::Attributes& attributes); + +} // namespace utils +} // namespace istio + +#endif // ISTIO_UTILS_LOCAL_ATTRIBUTES_H diff --git a/include/istio/utils/md5.h b/include/istio/utils/md5.h deleted file mode 100644 index 19b1c9dbe8f..00000000000 --- a/include/istio/utils/md5.h +++ /dev/null @@ -1,68 +0,0 @@ -/* Copyright 2017 Istio Authors. All Rights Reserved. - * - * Licensed 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. - */ - -#ifndef ISTIO_UTILS_MD5_H_ -#define ISTIO_UTILS_MD5_H_ - -#include -#include -#include "openssl/md5.h" - -namespace istio { -namespace utils { - -// Define a MD5 Digest by calling OpenSSL -class MD5 { - public: - MD5(); - - // Updates the context with data. - MD5& Update(const void* data, size_t size); - - // A helper function for const char* - MD5& Update(const char* str) { return Update(str, strlen(str)); } - - // A helper function for const string - MD5& Update(const std::string& str) { return Update(str.data(), str.size()); } - - // A helper function for int - MD5& Update(int d) { return Update(&d, sizeof(d)); } - - // The MD5 digest is always 128 bits = 16 bytes - static const int kDigestLength = 16; - - // Returns the digest as string. - std::string Digest(); - - // A short form of generating MD5 for a string - std::string operator()(const void* data, size_t size); - - // Converts a binary digest string to a printable string. - // It is for debugging and unit-test only. - static std::string DebugString(const std::string& digest); - - private: - // MD5 context. - MD5_CTX ctx_; - // The final MD5 digest. - unsigned char digest_[kDigestLength]; - // A flag to indicate if MD5_final is called or not. - bool finalized_; -}; - -} // namespace utils -} // namespace istio - -#endif // ISTIO_UTILS_MD5_H_ diff --git a/istio.deps b/istio.deps index e4d5ea71c25..eb49329ec52 100644 --- a/istio.deps +++ b/istio.deps @@ -4,13 +4,13 @@ "name": "ISTIO_API", "repoName": "api", "file": "repositories.bzl", - "lastStableSHA": "214c7598afb74f7f4dea49f77e45832c49382a15" + "lastStableSHA": "aec9db9d9a57faf688b4d5606fddede85d4d3855" }, { "_comment": "", "name": "ENVOY_SHA", "repoName": "envoyproxy/envoy", "file": "WORKSPACE", - "lastStableSHA": "73bd3d95cd0b6a23de0b6357f1b3065b9014651a" + "lastStableSHA": "b3be5713f2100ab5c40316e73ce34581245bd26a" } ] diff --git a/protobuf.bzl b/protobuf.bzl index c445757d231..786bb25f222 100644 --- a/protobuf.bzl +++ b/protobuf.bzl @@ -14,14 +14,19 @@ # ################################################################################ # -load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +# Match SHA used by Envoy +PROTOBUF_SHA = "7492b5681231c79f0265793fa57dc780ae2481d6" +PROTOBUF_SHA256 = "46f1da3a6a6db66dd240cf95a5553198f7c6e98e6ac942fceb8a1cf03291d96e" def protobuf_repositories(load_repo=True, bind=True): if load_repo: - git_repository( + http_archive( name = "com_google_protobuf", - commit = "6a4fec616ec4b20f54d5fb530808b855cb664390", # Match SHA used by Envoy - remote = "https://github.com/google/protobuf.git", + strip_prefix = "protobuf-" + PROTOBUF_SHA, + url = "https://github.com/google/protobuf/archive/" + PROTOBUF_SHA + ".tar.gz", + sha256 = PROTOBUF_SHA256, ) if bind: diff --git a/prow/proxy-postsubmit-periodic.sh b/prow/proxy-postsubmit-periodic.sh new file mode 100755 index 00000000000..3cc3b83b6c3 --- /dev/null +++ b/prow/proxy-postsubmit-periodic.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# +# Copyright 2017 Istio Authors. All Rights Reserved. +# +# Licensed 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. + +prow/proxy-postsubmit.sh diff --git a/repositories.bzl b/repositories.bzl index 719413dab4a..6b9a1dde189 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -14,27 +14,12 @@ # ################################################################################ # -load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") -def boringssl_repositories(bind=True): - git_repository( - name = "boringssl", - commit = "12c35d69008ae6b8486e435447445240509f7662", # 2016-10-24 - remote = "https://boringssl.googlesource.com/boringssl", - ) - - if bind: - native.bind( - name = "boringssl_crypto", - actual = "@boringssl//:crypto", - ) +GOOGLETEST = "d225acc90bc3a8c420a9bcd1f033033c1ccd7fe0" +GOOGLETEST_SHA256 = "01508c8f47c99509130f128924f07f3a60be05d039cff571bb11d60bb11a3581" - native.bind( - name = "libssl", - actual = "@boringssl//:ssl", - ) - -def googletest_repositories(bind=True): +def googletest_repositories(bind = True): BUILD = """ # Copyright 2017 Istio Authors. All Rights Reserved. # @@ -90,11 +75,12 @@ cc_library( visibility = ["//visibility:public"], ) """ - native.new_git_repository( + http_archive( name = "googletest_git", build_file_content = BUILD, - commit = "d225acc90bc3a8c420a9bcd1f033033c1ccd7fe0", - remote = "https://github.com/google/googletest.git", + strip_prefix = "googletest-" + GOOGLETEST, + url = "https://github.com/google/googletest/archive/" + GOOGLETEST + ".tar.gz", + sha256 = GOOGLETEST_SHA256, ) if bind: @@ -113,9 +99,10 @@ cc_library( actual = "@googletest_git//:googletest_prod", ) -ISTIO_API = "214c7598afb74f7f4dea49f77e45832c49382a15" +ISTIO_API = "aec9db9d9a57faf688b4d5606fddede85d4d3855" +ISTIO_API_SHA256 = "52a23e3453b0e639879e34365f9b80d0c7888851ed51034aad89268d4100e908" -def mixerapi_repositories(bind=True): +def mixerapi_repositories(bind = True): BUILD = """ # Copyright 2018 Istio Authors. All Rights Reserved. # @@ -192,6 +179,19 @@ cc_proto_library( ], ) +cc_proto_library( + name = "tcp_cluster_rewrite_config_cc_proto", + srcs = glob( + ["envoy/config/filter/network/tcp_cluster_rewrite/v2alpha1/*.proto", ], + ), + default_runtime = "//external:protobuf", + protoc = "//external:protoc", + visibility = ["//visibility:public"], + deps = [ + "//external:cc_gogoproto", + ], +) + filegroup( name = "global_dictionary_file", srcs = ["mixer/v1/global_dictionary.yaml"], @@ -199,11 +199,12 @@ filegroup( ) """ - native.new_git_repository( + http_archive( name = "mixerapi_git", build_file_content = BUILD, - commit = ISTIO_API, - remote = "https://github.com/istio/api.git", + strip_prefix = "api-" + ISTIO_API, + url = "https://github.com/istio/api/archive/" + ISTIO_API + ".tar.gz", + sha256 = ISTIO_API_SHA256, ) if bind: native.bind( @@ -222,15 +223,19 @@ filegroup( name = "jwt_auth_config_cc_proto", actual = "@mixerapi_git//:jwt_auth_config_cc_proto", ) + native.bind( + name = "tcp_cluster_rewrite_config_cc_proto", + actual = "@mixerapi_git//:tcp_cluster_rewrite_config_cc_proto", + ) load(":protobuf.bzl", "protobuf_repositories") load(":cc_gogo_protobuf.bzl", "cc_gogoproto_repositories") load(":x_tools_imports.bzl", "go_x_tools_imports_repositories") load(":googleapis.bzl", "googleapis_repositories") -def mixerapi_dependencies(): - protobuf_repositories(load_repo=True, bind=True) - cc_gogoproto_repositories() - go_x_tools_imports_repositories() - googleapis_repositories() - mixerapi_repositories() +def mixerapi_dependencies(): + protobuf_repositories(load_repo = True, bind = True) + cc_gogoproto_repositories() + go_x_tools_imports_repositories() + googleapis_repositories() + mixerapi_repositories() diff --git a/script/check-repositories b/script/check-repositories new file mode 100755 index 00000000000..e8c4cca0162 --- /dev/null +++ b/script/check-repositories @@ -0,0 +1,40 @@ +#!/bin/bash +# +# Copyright 2018 Istio Authors. All Rights Reserved. +# +# Licensed 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. +# +################################################################################ + +set -eu + +# Check whether any git repositories are defined. +# Git repository definition contains `commit` and `remote` fields. +if grep -nr "commit =\|remote =" --include=WORKSPACE --include=*.bzl .; then + echo "Using git repositories is not allowed." + echo "To ensure that all dependencies can be stored offline in distdir, only HTTP repositories are allowed." + exit 1 +fi + +# Check whether number of defined `url =` and `sha256 =` kwargs in repository +# definitions is equal. +urls_count=$(grep -nr "url =" --include=WORKSPACE --include=*.bzl | wc -l) +sha256sums_count=$(grep -nr "sha256 =" --include=WORKSPACE --include=*.bzl | wc -l) + +if [[ $urls_count != $sha256sums_count ]]; then + echo "Found more defined repository URLs than SHA256 sums, which means that there are some repositories without sums." + echo "Dependencies without SHA256 sums cannot be stored in distdir." + echo "Please ensure that every repository has a SHA256 sum." + echo "Repositories are defined in the WORKSPACE file." + exit 1 +fi diff --git a/src/envoy/BUILD b/src/envoy/BUILD index 43abb6b554f..3debd1edbe3 100644 --- a/src/envoy/BUILD +++ b/src/envoy/BUILD @@ -28,8 +28,10 @@ envoy_cc_binary( "//src/envoy/http/authn:filter_lib", "//src/envoy/http/jwt_auth:http_filter_factory", "//src/envoy/http/mixer:filter_lib", + "//src/envoy/tcp/forward_downstream_sni:config_lib", "//src/envoy/tcp/mixer:filter_lib", - "//src/envoy/alts:alts_socket_factory", + "//src/envoy/tcp/sni_verifier:config_lib", + "//src/envoy/tcp/tcp_cluster_rewrite:config_lib", "@envoy//source/exe:envoy_main_entry_lib", ], ) diff --git a/src/envoy/alts/BUILD b/src/envoy/alts/BUILD deleted file mode 100644 index 0076e57d8e6..00000000000 --- a/src/envoy/alts/BUILD +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright 2018 Istio Authors. All Rights Reserved. -# -# Licensed 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. -# -################################################################################ -# -load( - "@envoy//bazel:envoy_build_system.bzl", - "envoy_cc_library", -) - -load( - "@envoy_api//bazel:api_build_system.bzl", - "api_proto_library", -) - -api_proto_library( - name = "alts_socket_proto", - srcs = [":alts_socket.proto"], - visibility = ["//visibility:public"], - require_py = 0, -) - -envoy_cc_library( - name = "grpc_tsi_wrapper", - repository = "@envoy", - visibility = ["//visibility:private"], - hdrs = [ - "transport_security_interface_wrapper.h", - ], - external_deps = [ - "grpc", - ], -) - -envoy_cc_library( - name = "tsi_handshaker", - repository = "@envoy", - visibility = ["//visibility:public"], - srcs = [ - "tsi_handshaker.cc", - ], - hdrs = [ - "tsi_handshaker.h", - ], - deps = [ - ":grpc_tsi_wrapper", - "@envoy//source/exe:envoy_common_lib", - ], -) - -envoy_cc_library( - name = "tsi_frame_protector", - repository = "@envoy", - visibility = ["//visibility:public"], - srcs = [ - "tsi_frame_protector.cc", - ], - hdrs = [ - "tsi_frame_protector.h", - ], - deps = [ - ":grpc_tsi_wrapper", - "@envoy//source/exe:envoy_common_lib", - ], -) - -envoy_cc_library( - name = "tsi_transport_socket", - repository = "@envoy", - visibility = ["//visibility:public"], - srcs = [ - "tsi_transport_socket.cc", - ], - hdrs = [ - "tsi_transport_socket.h", - ], - deps = [ - ":tsi_frame_protector", - ":tsi_handshaker", - "@envoy//source/exe:envoy_common_lib", - ], -) - -envoy_cc_library( - name = "alts_socket_factory", - repository = "@envoy", - visibility = ["//visibility:public"], - srcs = [ - "alts_socket_factory.cc", - ], - hdrs = [ - "alts_socket_factory.h", - ], - deps = [ - ":grpc_tsi_wrapper", - ":tsi_transport_socket", - ":alts_socket_proto_cc", - "@envoy//source/exe:envoy_common_lib", - ], -) diff --git a/src/envoy/alts/README.md b/src/envoy/alts/README.md deleted file mode 100644 index 5eeaa79bb60..00000000000 --- a/src/envoy/alts/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# ALTS support (experimental) - -*The code in this directory is experimental. Do not use in production* - -A prototype of -[ALTS](https://cloud.google.com/security/encryption-in-transit/application-layer-transport-security/) -support for Istio/Envoy. It depends on ALTS stack in gRPC library and implemented as Envoy's -[transport socket](https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/core/base.proto#core-transportsocket). - -An example config is in `example.yaml`. Note: If you want to enable the peer validation, please -uncomment and replace the content of `peer_service_accounts` with the actual service account in your -environment. Please make sure the service account is correct otherwise the ALTS connection will be -closed due to validation failure. diff --git a/src/envoy/alts/alts_socket.proto b/src/envoy/alts/alts_socket.proto deleted file mode 100644 index 7f7bf7a6c5e..00000000000 --- a/src/envoy/alts/alts_socket.proto +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2018 Istio Authors -// -// Licensed 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. - -syntax = "proto3"; - -package envoy.security.v2; - -import "google/protobuf/duration.proto"; -import "validate/validate.proto"; - -message AltsSocket { - // The location of a handshaker service, this is usually 169.254.169.254:8080 - // on GCE - string handshaker_service = 1 [(validate.rules).string.min_bytes = 1]; - - // The acceptable service accounts from peer, peers not in the list will be - // rejected in the handshake validation step. - // If empty, no validation will be performed. - repeated string peer_service_accounts = 2; -} diff --git a/src/envoy/alts/alts_socket_factory.cc b/src/envoy/alts/alts_socket_factory.cc deleted file mode 100644 index a4e24ab43d8..00000000000 --- a/src/envoy/alts/alts_socket_factory.cc +++ /dev/null @@ -1,158 +0,0 @@ -/* Copyright 2018 Istio Authors. All Rights Reserved. - * - * Licensed 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 "src/envoy/alts/alts_socket_factory.h" -#include "absl/strings/str_join.h" -#include "common/common/assert.h" -#include "common/protobuf/protobuf.h" -#include "common/protobuf/utility.h" -#include "envoy/registry/registry.h" -#include "envoy/server/transport_socket_config.h" -#include "src/envoy/alts/alts_socket.pb.h" -#include "src/envoy/alts/alts_socket.pb.validate.h" -#include "src/envoy/alts/transport_security_interface_wrapper.h" -#include "src/envoy/alts/tsi_handshaker.h" -#include "src/envoy/alts/tsi_transport_socket.h" - -namespace Envoy { -namespace Server { -namespace Configuration { - -using ::google::protobuf::RepeatedPtrField; - -// Returns true if the peer's service account is found in peers, otherwise -// returns false and fills out err with an error message. -static bool doValidate(const tsi_peer &peer, - const std::unordered_set &peers, - std::string &err) { - for (size_t i = 0; i < peer.property_count; ++i) { - std::string name = std::string(peer.properties[i].name); - std::string value = std::string(peer.properties[i].value.data, - peer.properties[i].value.length); - if (name.compare(TSI_ALTS_SERVICE_ACCOUNT_PEER_PROPERTY) == 0 && - peers.find(value) != peers.end()) { - return true; - } - } - - err = "Couldn't find peer's service account in peer_service_accounts: " + - absl::StrJoin(peers, ","); - return false; -} - -ProtobufTypes::MessagePtr -AltsTransportSocketConfigFactory::createEmptyConfigProto() { - return std::make_unique(); -} - -std::string -Envoy::Server::Configuration::AltsTransportSocketConfigFactory::name() const { - return "alts"; -} - -Network::TransportSocketFactoryPtr -UpstreamAltsTransportSocketConfigFactory::createTransportSocketFactory( - const Protobuf::Message &message, TransportSocketFactoryContext &) { - auto config = - MessageUtil::downcastAndValidate( - message); - - std::string handshaker_service = config.handshaker_service(); - const auto &peer_service_accounts = config.peer_service_accounts(); - std::unordered_set peers(peer_service_accounts.cbegin(), - peer_service_accounts.cend()); - - Security::HandshakeValidator validator; - // Skip validation if peers is empty. - if (!peers.empty()) { - validator = [peers](const tsi_peer &peer, std::string &err) { - return doValidate(peer, peers, err); - }; - } - - return std::make_unique( - [handshaker_service](Event::Dispatcher &dispatcher) { - grpc_alts_credentials_options *options = - grpc_alts_credentials_client_options_create(); - - tsi_handshaker *handshaker = nullptr; - - // Specifying target name as empty since TSI won't take care of - // validating peer identity in this use case. The validation will be - // implemented in TsiSocket later. - alts_tsi_handshaker_create(options, "", handshaker_service.c_str(), - true /* is_client */, &handshaker); - - ASSERT(handshaker != nullptr); - - grpc_alts_credentials_options_destroy(options); - - return std::make_unique(handshaker, - dispatcher); - }, - validator); -} - -Network::TransportSocketFactoryPtr -DownstreamAltsTransportSocketConfigFactory::createTransportSocketFactory( - const Protobuf::Message &message, TransportSocketFactoryContext &, - const std::vector &) { - auto config = - MessageUtil::downcastAndValidate( - message); - - std::string handshaker_service = config.handshaker_service(); - const auto &peer_service_accounts = config.peer_service_accounts(); - std::unordered_set peers(peer_service_accounts.cbegin(), - peer_service_accounts.cend()); - - Security::HandshakeValidator validator; - // Skip validation if peers is empty. - if (!peers.empty()) { - validator = [peers](const tsi_peer &peer, std::string &err) { - return doValidate(peer, peers, err); - }; - } - - return std::make_unique( - [handshaker_service](Event::Dispatcher &dispatcher) { - grpc_alts_credentials_options *options = - grpc_alts_credentials_server_options_create(); - - tsi_handshaker *handshaker = nullptr; - - alts_tsi_handshaker_create(options, nullptr, handshaker_service.c_str(), - false /* is_client */, &handshaker); - - ASSERT(handshaker != nullptr); - - grpc_alts_credentials_options_destroy(options); - - return std::make_unique(handshaker, - dispatcher); - }, - validator); -} - -static Registry::RegisterFactory - upstream_registered_; - -static Registry::RegisterFactory - downstream_registered_; -} // namespace Configuration -} // namespace Server -} // namespace Envoy diff --git a/src/envoy/alts/alts_socket_factory.h b/src/envoy/alts/alts_socket_factory.h deleted file mode 100644 index 797f5d87787..00000000000 --- a/src/envoy/alts/alts_socket_factory.h +++ /dev/null @@ -1,47 +0,0 @@ -/* Copyright 2018 Istio Authors. All Rights Reserved. - * - * Licensed 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 "envoy/server/transport_socket_config.h" - -namespace Envoy { -namespace Server { -namespace Configuration { - -// ALTS config registry -class AltsTransportSocketConfigFactory - : public virtual TransportSocketConfigFactory { - public: - ProtobufTypes::MessagePtr createEmptyConfigProto() override; - std::string name() const override; -}; - -class UpstreamAltsTransportSocketConfigFactory - : public AltsTransportSocketConfigFactory, - public UpstreamTransportSocketConfigFactory { - public: - Network::TransportSocketFactoryPtr createTransportSocketFactory( - const Protobuf::Message &, TransportSocketFactoryContext &) override; -}; - -class DownstreamAltsTransportSocketConfigFactory - : public AltsTransportSocketConfigFactory, - public DownstreamTransportSocketConfigFactory { - public: - Network::TransportSocketFactoryPtr createTransportSocketFactory( - const Protobuf::Message &, TransportSocketFactoryContext &, - const std::vector &) override; -}; -} // namespace Configuration -} // namespace Server -} // namespace Envoy diff --git a/src/envoy/alts/example.yaml b/src/envoy/alts/example.yaml deleted file mode 100644 index d157684969b..00000000000 --- a/src/envoy/alts/example.yaml +++ /dev/null @@ -1,59 +0,0 @@ -static_resources: - listeners: - - address: - socket_address: - address: 127.0.0.1 - port_value: 5000 - filter_chains: - - filters: - - name: envoy.tcp_proxy - config: - stat_prefix: client_tcp - cluster: server_envoy - - address: - socket_address: - address: 127.0.0.1 - port_value: 5005 - filter_chains: - - transport_socket: - name: alts - config: - handshaker_service: "169.254.169.254:8080" - # If you want to enable the peer validation, please uncomment peer_service_accounts and - # replace it with the actual service account used in your environment. - # peer_service_accounts: ["test-service-account"] - filters: - - name: envoy.tcp_proxy - config: - stat_prefix: server_tcp - cluster: tcp_backend - clusters: - - name: server_envoy - transport_socket: - name: alts - config: - handshaker_service: "169.254.169.254:8080" - # If you want to enable the peer validation, please uncomment peer_service_accounts and - # replace it with the actual service account used in your environment. - # peer_service_accounts: ["test-service-account"] - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - hosts: - - socket_address: - address: 127.0.0.1 - port_value: 5005 - - name: tcp_backend - connect_timeout: 0.25s - type: strict_dns - lb_policy: round_robin - hosts: - - socket_address: - address: 127.0.0.1 - port_value: 5050 -admin: - access_log_path: "/dev/null" - address: - socket_address: - address: 0.0.0.0 - port_value: 8001 diff --git a/src/envoy/alts/tsi_frame_protector.cc b/src/envoy/alts/tsi_frame_protector.cc deleted file mode 100644 index a4454e5e614..00000000000 --- a/src/envoy/alts/tsi_frame_protector.cc +++ /dev/null @@ -1,94 +0,0 @@ -/* Copyright 2018 Istio Authors. All Rights Reserved. - * - * Licensed 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 "src/envoy/alts/tsi_frame_protector.h" - -#include "common/common/assert.h" - -namespace Envoy { -namespace Security { - -TsiFrameProtector::TsiFrameProtector(tsi_frame_protector *frame_protector) - : frame_protector_(frame_protector) {} - -tsi_result TsiFrameProtector::protect(Buffer::Instance &input, - Buffer::Instance &output) { - ASSERT(frame_protector_); - - // TODO(lizan): tune size later - unsigned char protected_buffer[4096]; - size_t protected_buffer_size = sizeof(protected_buffer); - while (input.length() > 0) { - auto *message_bytes = - reinterpret_cast(input.linearize(input.length())); - size_t protected_buffer_size_to_send = protected_buffer_size; - size_t processed_message_size = input.length(); - tsi_result result = tsi_frame_protector_protect( - frame_protector_.get(), message_bytes, &processed_message_size, - protected_buffer, &protected_buffer_size_to_send); - if (result != TSI_OK) { - ASSERT(result != TSI_INVALID_ARGUMENT && result != TSI_UNIMPLEMENTED); - return result; - } - output.add(protected_buffer, protected_buffer_size_to_send); - input.drain(processed_message_size); - } - - ASSERT(input.length() == 0); - size_t still_pending_size; - do { - size_t protected_buffer_size_to_send = protected_buffer_size; - tsi_result result = tsi_frame_protector_protect_flush( - frame_protector_.get(), protected_buffer, - &protected_buffer_size_to_send, &still_pending_size); - if (result != TSI_OK) { - ASSERT(result != TSI_INVALID_ARGUMENT && result != TSI_UNIMPLEMENTED); - return result; - } - output.add(protected_buffer, protected_buffer_size_to_send); - } while (still_pending_size > 0); - - return TSI_OK; -} - -tsi_result TsiFrameProtector::unprotect(Buffer::Instance &input, - Buffer::Instance &output) { - ASSERT(frame_protector_); - - // TODO(lizan): Tune the buffer size. - unsigned char unprotected_buffer[4096]; - size_t unprotected_buffer_size = sizeof(unprotected_buffer); - - while (input.length() > 0) { - auto *message_bytes = - reinterpret_cast(input.linearize(input.length())); - size_t unprotected_buffer_size_to_send = unprotected_buffer_size; - size_t processed_message_size = input.length(); - tsi_result result = tsi_frame_protector_unprotect( - frame_protector_.get(), message_bytes, &processed_message_size, - unprotected_buffer, &unprotected_buffer_size_to_send); - if (result != TSI_OK) { - ASSERT(result != TSI_INVALID_ARGUMENT && result != TSI_UNIMPLEMENTED); - return result; - } - output.add(unprotected_buffer, unprotected_buffer_size_to_send); - input.drain(processed_message_size); - } - - return TSI_OK; -} - -} // namespace Security -} // namespace Envoy diff --git a/src/envoy/alts/tsi_frame_protector.h b/src/envoy/alts/tsi_frame_protector.h deleted file mode 100644 index ccd36a6a9e5..00000000000 --- a/src/envoy/alts/tsi_frame_protector.h +++ /dev/null @@ -1,65 +0,0 @@ -/* Copyright 2018 Istio Authors. All Rights Reserved. - * - * Licensed 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 "common/common/c_smart_ptr.h" -#include "envoy/buffer/buffer.h" -#include "envoy/event/dispatcher.h" - -#include "src/envoy/alts/transport_security_interface_wrapper.h" - -namespace Envoy { -namespace Security { - -typedef CSmartPtr - CFrameProtectorPtr; - -/** - * A C++ wrapper for tsi_frame_protector interface. - * For detail of tsi_frame_protector, see - * https://github.com/grpc/grpc/blob/v1.10.0/src/core/tsi/transport_security_interface.h#L70 - * - * TODO(lizan): migrate to tsi_zero_copy_grpc_protector for further optimization - */ -class TsiFrameProtector final { - public: - explicit TsiFrameProtector(tsi_frame_protector* frame_protector); - - /** - * Wrapper for tsi_frame_protector_protect - * @param input supplies the input buffer, the method will drain it when it is - * protected. - * @param output supplies the output buffer - * @return tsi_result the status. - */ - tsi_result protect(Buffer::Instance& input, Buffer::Instance& output); - - /** - * Wrapper for tsi_frame_protector_unprotect - * @param input supplies the input buffer, the method will drain it when it is - * protected. - * @param output supplies the output buffer - * @return tsi_result the status. - */ - tsi_result unprotect(Buffer::Instance& input, Buffer::Instance& output); - - private: - CFrameProtectorPtr frame_protector_; -}; - -typedef std::unique_ptr TsiFrameProtectorPtr; - -} // namespace Security -} // namespace Envoy diff --git a/src/envoy/alts/tsi_handshaker.cc b/src/envoy/alts/tsi_handshaker.cc deleted file mode 100644 index d5139470c39..00000000000 --- a/src/envoy/alts/tsi_handshaker.cc +++ /dev/null @@ -1,86 +0,0 @@ -/* Copyright 2018 Istio Authors. All Rights Reserved. - * - * Licensed 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 "src/envoy/alts/tsi_handshaker.h" -#include "common/buffer/buffer_impl.h" -#include "common/common/assert.h" - -namespace Envoy { -namespace Security { - -void TsiHandshaker::onNextDone(tsi_result status, void *user_data, - const unsigned char *bytes_to_send, - size_t bytes_to_send_size, - tsi_handshaker_result *handshaker_result) { - TsiHandshaker *handshaker = static_cast(user_data); - - Buffer::InstancePtr to_send = std::make_unique(); - if (bytes_to_send_size > 0) { - to_send->add(bytes_to_send, bytes_to_send_size); - } - - auto next_result = new TsiHandshakerCallbacks::NextResult{ - status, std::move(to_send), {handshaker_result}}; - - handshaker->dispatcher_.post([handshaker, next_result]() { - ASSERT(handshaker->calling_); - handshaker->calling_ = false; - - TsiHandshakerCallbacks::NextResultPtr next_result_ptr{next_result}; - - if (handshaker->delete_on_done_) { - handshaker->dispatcher_.deferredDelete( - Event::DeferredDeletablePtr{handshaker}); - return; - } - handshaker->callbacks_->onNextDone(std::move(next_result_ptr)); - }); -} - -TsiHandshaker::TsiHandshaker(tsi_handshaker *handshaker, - Event::Dispatcher &dispatcher) - : handshaker_(handshaker), dispatcher_(dispatcher) {} - -TsiHandshaker::~TsiHandshaker() { ASSERT(!calling_); } - -tsi_result TsiHandshaker::next(Envoy::Buffer::Instance &received) { - ASSERT(!calling_); - calling_ = true; - - uint64_t received_size = received.length(); - const unsigned char *bytes_to_send = nullptr; - size_t bytes_to_send_size = 0; - tsi_handshaker_result *result = nullptr; - tsi_result status = - tsi_handshaker_next(handshaker_.get(), - reinterpret_cast( - received.linearize(received_size)), - received_size, &bytes_to_send, &bytes_to_send_size, - &result, onNextDone, this); - - if (status != TSI_ASYNC) { - onNextDone(status, this, bytes_to_send, bytes_to_send_size, result); - } - return status; -} - -void TsiHandshaker::deferredDelete() { - if (calling_) { - delete_on_done_ = true; - } else { - dispatcher_.deferredDelete(Event::DeferredDeletablePtr{this}); - } -} -} // namespace Security -} // namespace Envoy diff --git a/src/envoy/alts/tsi_handshaker.h b/src/envoy/alts/tsi_handshaker.h deleted file mode 100644 index 01e8310aba8..00000000000 --- a/src/envoy/alts/tsi_handshaker.h +++ /dev/null @@ -1,112 +0,0 @@ -/* Copyright 2018 Istio Authors. All Rights Reserved. - * - * Licensed 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 -#include - -#include "common/common/c_smart_ptr.h" -#include "envoy/buffer/buffer.h" -#include "envoy/event/dispatcher.h" - -#include "src/envoy/alts/transport_security_interface_wrapper.h" - -namespace Envoy { -namespace Security { - -typedef CSmartPtr - TsiHandshakerResultPtr; -typedef CSmartPtr CHandshakerPtr; - -/** - * An interface to get callback from TsiHandshaker. - * TsiHandshaker will call this callback in the thread which its dispatcher - * posts. - */ -class TsiHandshakerCallbacks { - public: - virtual ~TsiHandshakerCallbacks() {} - - struct NextResult { - // A enum of the result - tsi_result status_; - - // The buffer to be sent to the peer - Buffer::InstancePtr to_send_; - - // A pointer to tsi_handshaker_result struct. Owned by instance. - TsiHandshakerResultPtr result_; - }; - - typedef std::unique_ptr NextResultPtr; - - /** - * Called when `next` is done, this may be called in line in `next` if the - * handshaker is not - * asynchronous. - * @param result - */ - virtual void onNextDone(NextResultPtr&& result) PURE; -}; - -/** - * A C++ wrapper for tsi_handshaker interface. - * For detail of tsi_handshaker, see - * https://github.com/grpc/grpc/blob/v1.10.0/src/core/tsi/transport_security_interface.h#L236 - */ -class TsiHandshaker final : public Event::DeferredDeletable { - public: - explicit TsiHandshaker(tsi_handshaker* handshaker, - Event::Dispatcher& dispatcher); - ~TsiHandshaker(); - - /** - * Conduct next step of handshake, see - * https://github.com/grpc/grpc/blob/v1.10.0/src/core/tsi/transport_security_interface.h#L416 - * @param received the buffer received from peer. - */ - tsi_result next(Buffer::Instance& received); - - /** - * Set handshaker callbacks, this must be called before calling next. - * @param callbacks supplies the callback instance. - */ - void setHandshakerCallbacks(TsiHandshakerCallbacks& callbacks) { - callbacks_ = &callbacks; - } - - /** - * Delete the handshaker when it is ready. This must be called after releasing - * from a smart - * pointer. The actual delete happens after ongoing next call are processed. - */ - void deferredDelete(); - - private: - static void onNextDone(tsi_result status, void* user_data, - const unsigned char* bytes_to_send, - size_t bytes_to_send_size, - tsi_handshaker_result* handshaker_result); - - CHandshakerPtr handshaker_; - TsiHandshakerCallbacks* callbacks_{nullptr}; - bool calling_{false}; - bool delete_on_done_{false}; - Event::Dispatcher& dispatcher_; -}; - -typedef std::unique_ptr TsiHandshakerPtr; -} // namespace Security -} // namespace Envoy diff --git a/src/envoy/alts/tsi_transport_socket.cc b/src/envoy/alts/tsi_transport_socket.cc deleted file mode 100644 index 9e61ce1efd9..00000000000 --- a/src/envoy/alts/tsi_transport_socket.cc +++ /dev/null @@ -1,228 +0,0 @@ -/* Copyright 2018 Istio Authors. All Rights Reserved. - * - * Licensed 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 "src/envoy/alts/tsi_transport_socket.h" - -#include "common/common/assert.h" -#include "common/common/enum_to_int.h" - -namespace Envoy { -namespace Security { - -TsiSocket::TsiSocket(HandshakerFactory handshaker_factory, - HandshakeValidator handshake_validator) - : handshaker_factory_(handshaker_factory), - handshake_validator_(handshake_validator), - raw_buffer_callbacks_(*this) { - raw_buffer_socket_.setTransportSocketCallbacks(raw_buffer_callbacks_); -} - -TsiSocket::~TsiSocket() { ASSERT(!handshaker_); } - -void TsiSocket::setTransportSocketCallbacks( - Envoy::Network::TransportSocketCallbacks &callbacks) { - callbacks_ = &callbacks; - - handshaker_ = handshaker_factory_(callbacks.connection().dispatcher()); - handshaker_->setHandshakerCallbacks(*this); -} - -std::string TsiSocket::protocol() const { return ""; } - -Network::PostIoAction TsiSocket::doHandshake() { - ASSERT(!handshake_complete_); - ENVOY_CONN_LOG(debug, "TSI: doHandshake", callbacks_->connection()); - - if (!handshaker_next_calling_) { - doHandshakeNext(); - } - return Network::PostIoAction::KeepOpen; -} - -void TsiSocket::doHandshakeNext() { - ENVOY_CONN_LOG(debug, "TSI: doHandshake next: received: {}", - callbacks_->connection(), raw_read_buffer_.length()); - handshaker_next_calling_ = true; - Buffer::OwnedImpl handshaker_buffer; - handshaker_buffer.move(raw_read_buffer_); - handshaker_->next(handshaker_buffer); -} - -Network::PostIoAction TsiSocket::doHandshakeNextDone( - NextResultPtr &&next_result) { - ASSERT(next_result); - - ENVOY_CONN_LOG(debug, "TSI: doHandshake next done: status: {} to_send: {}", - callbacks_->connection(), next_result->status_, - next_result->to_send_->length()); - - tsi_result status = next_result->status_; - tsi_handshaker_result *handshaker_result = next_result->result_.get(); - - if (status != TSI_INCOMPLETE_DATA && status != TSI_OK) { - ENVOY_CONN_LOG(debug, "TSI: Handshake failed: status: {}", - callbacks_->connection(), status); - return Network::PostIoAction::Close; - } - - if (next_result->to_send_->length() > 0) { - raw_write_buffer_.move(*next_result->to_send_); - } - - if (status == TSI_OK && handshaker_result != nullptr) { - tsi_peer peer; - tsi_handshaker_result_extract_peer(handshaker_result, &peer); - ENVOY_CONN_LOG(debug, "TSI: Handshake successful: peer properties: {}", - callbacks_->connection(), peer.property_count); - for (size_t i = 0; i < peer.property_count; ++i) { - ENVOY_CONN_LOG(debug, " {}: {}", callbacks_->connection(), - peer.properties[i].name, - std::string(peer.properties[i].value.data, - peer.properties[i].value.length)); - } - if (handshake_validator_) { - std::string err; - bool peer_validated = handshake_validator_(peer, err); - if (peer_validated) { - ENVOY_CONN_LOG(info, "TSI: Handshake validation succeeded.", - callbacks_->connection()); - } else { - ENVOY_CONN_LOG(warn, "TSI: Handshake validation failed: {}", - callbacks_->connection(), err); - tsi_peer_destruct(&peer); - return Network::PostIoAction::Close; - } - } else { - ENVOY_CONN_LOG(info, "TSI: Handshake validation skipped.", - callbacks_->connection()); - } - tsi_peer_destruct(&peer); - - const unsigned char *unused_bytes; - size_t unused_byte_size; - - status = tsi_handshaker_result_get_unused_bytes( - handshaker_result, &unused_bytes, &unused_byte_size); - ASSERT(status == TSI_OK); - if (unused_byte_size > 0) { - raw_read_buffer_.add(unused_bytes, unused_byte_size); - } - ENVOY_CONN_LOG(debug, "TSI: Handshake successful: unused_bytes: {}", - callbacks_->connection(), unused_byte_size); - - tsi_frame_protector *frame_protector; - status = tsi_handshaker_result_create_frame_protector( - handshaker_result, NULL, &frame_protector); - ASSERT(status == TSI_OK); - frame_protector_ = std::make_unique(frame_protector); - - handshake_complete_ = true; - callbacks_->raiseEvent(Network::ConnectionEvent::Connected); - } - - if (raw_read_buffer_.length() > 0) { - callbacks_->setReadBufferReady(); - } - return Network::PostIoAction::KeepOpen; -} - -Network::IoResult TsiSocket::doRead(Buffer::Instance &buffer) { - Network::IoResult result = raw_buffer_socket_.doRead(raw_read_buffer_); - ENVOY_CONN_LOG(debug, "TSI: raw read result action {} bytes {} end_stream {}", - callbacks_->connection(), enumToInt(result.action_), - result.bytes_processed_, result.end_stream_read_); - if (result.action_ == Network::PostIoAction::Close && - result.bytes_processed_ == 0) { - return result; - } - - if (!handshake_complete_) { - Network::PostIoAction action = doHandshake(); - if (action == Network::PostIoAction::Close || !handshake_complete_) { - return {action, 0, false}; - } - } - - if (handshake_complete_) { - ASSERT(frame_protector_); - - uint64_t read_size = raw_read_buffer_.length(); - ENVOY_CONN_LOG(debug, "TSI: unprotecting buffer size: {}", - callbacks_->connection(), raw_read_buffer_.length()); - tsi_result status = frame_protector_->unprotect(raw_read_buffer_, buffer); - ENVOY_CONN_LOG(debug, "TSI: unprotected buffer left: {} result: {}", - callbacks_->connection(), raw_read_buffer_.length(), - tsi_result_to_string(status)); - result.bytes_processed_ = read_size - raw_read_buffer_.length(); - } - - ENVOY_CONN_LOG(debug, "TSI: do read result action {} bytes {} end_stream {}", - callbacks_->connection(), enumToInt(result.action_), - result.bytes_processed_, result.end_stream_read_); - return result; -} - -Network::IoResult TsiSocket::doWrite(Buffer::Instance &buffer, - bool end_stream) { - if (!handshake_complete_) { - Network::PostIoAction action = doHandshake(); - if (action == Network::PostIoAction::Close) { - return {action, 0, false}; - } - } - - if (handshake_complete_) { - ASSERT(frame_protector_); - ENVOY_CONN_LOG(debug, "TSI: protecting buffer size: {}", - callbacks_->connection(), buffer.length()); - tsi_result status = frame_protector_->protect(buffer, raw_write_buffer_); - ENVOY_CONN_LOG(debug, "TSI: protected buffer left: {} result: {}", - callbacks_->connection(), buffer.length(), - tsi_result_to_string(status)); - } - - ENVOY_CONN_LOG(debug, "TSI: raw_write length {} end_stream {}", - callbacks_->connection(), raw_write_buffer_.length(), - end_stream); - return raw_buffer_socket_.doWrite(raw_write_buffer_, - end_stream && (buffer.length() == 0)); -} - -void TsiSocket::closeSocket(Network::ConnectionEvent) { - handshaker_.release()->deferredDelete(); -} - -void TsiSocket::onConnected() { ASSERT(!handshake_complete_); } - -void TsiSocket::onNextDone(NextResultPtr &&result) { - handshaker_next_calling_ = false; - - Network::PostIoAction action = doHandshakeNextDone(std::move(result)); - if (action == Network::PostIoAction::Close) { - callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); - } -} - -TsiSocketFactory::TsiSocketFactory(HandshakerFactory handshaker_factory, - HandshakeValidator handshake_validator) - : handshaker_factory_(std::move(handshaker_factory)), - handshake_validator_(std::move(handshake_validator)) {} - -bool TsiSocketFactory::implementsSecureTransport() const { return true; } - -Network::TransportSocketPtr TsiSocketFactory::createTransportSocket() const { - return std::make_unique(handshaker_factory_, handshake_validator_); -} -} // namespace Security -} // namespace Envoy diff --git a/src/envoy/http/authn/authenticator_base.cc b/src/envoy/http/authn/authenticator_base.cc index 9262d249e25..53a877d17cd 100644 --- a/src/envoy/http/authn/authenticator_base.cc +++ b/src/envoy/http/authn/authenticator_base.cc @@ -29,6 +29,19 @@ namespace Http { namespace Istio { namespace AuthN { +namespace { +// The default header name for an exchanged token +static const std::string kExchangedTokenHeaderName = "ingress-authorization"; + +// Returns whether the header for an exchanged token is found +bool FindHeaderOfExchangedToken(const iaapi::Jwt& jwt) { + return (jwt.jwt_headers_size() == 1 && + LowerCaseString(kExchangedTokenHeaderName) == + LowerCaseString(jwt.jwt_headers(0))); +} + +} // namespace + AuthenticatorBase::AuthenticatorBase(FilterContext* filter_context) : filter_context_(*filter_context) {} @@ -68,7 +81,27 @@ bool AuthenticatorBase::validateX509(const iaapi::MutualTls& mtls, bool AuthenticatorBase::validateJwt(const iaapi::Jwt& jwt, Payload* payload) { std::string jwt_payload; if (filter_context()->getJwtPayload(jwt.issuer(), &jwt_payload)) { - return AuthnUtils::ProcessJwtPayload(jwt_payload, payload->mutable_jwt()); + std::string payload_to_process = jwt_payload; + std::string original_payload; + if (FindHeaderOfExchangedToken(jwt)) { + if (AuthnUtils::ExtractOriginalPayload(jwt_payload, &original_payload)) { + // When the header of an exchanged token is found and the token + // contains the claim of the original payload, the original payload + // is extracted and used as the token payload. + payload_to_process = original_payload; + } else { + // When the header of an exchanged token is found but the token + // does not contain the claim of the original payload, it + // is regarded as an invalid exchanged token. + ENVOY_LOG( + error, + "Expect exchanged-token with original payload claim. Received: {}", + jwt_payload); + return false; + } + } + return AuthnUtils::ProcessJwtPayload(payload_to_process, + payload->mutable_jwt()); } return false; } diff --git a/src/envoy/http/authn/authenticator_base_test.cc b/src/envoy/http/authn/authenticator_base_test.cc index 068e5db1edb..1a476f065e7 100644 --- a/src/envoy/http/authn/authenticator_base_test.cc +++ b/src/envoy/http/authn/authenticator_base_test.cc @@ -44,12 +44,37 @@ const std::string kSecIstioAuthUserinfoHeaderValue = { "iss": "issuer@foo.com", "sub": "sub@foo.com", - "aud": "aud1", + "aud": ["aud1", "aud2"], "non-string-will-be-ignored": 1512754205, "some-other-string-claims": "some-claims-kept" } )"; +const std::string kExchangedTokenHeaderName = "ingress-authorization"; + +const std::string kExchangedTokenPayload = + R"( + { + "iss": "token-service", + "sub": "subject", + "aud": ["aud1", "aud2"], + "original_claims": { + "iss": "https://accounts.example.com", + "sub": "example-subject", + "email": "user@example.com" + } + } + )"; + +const std::string kExchangedTokenPayloadNoOriginalClaims = + R"( + { + "iss": "token-service", + "sub": "subject", + "aud": ["aud1", "aud2"] + } + )"; + class MockAuthenticatorBase : public AuthenticatorBase { public: MockAuthenticatorBase(FilterContext* filter_context) @@ -64,9 +89,10 @@ class ValidateX509Test : public testing::TestWithParam, NiceMock connection_{}; NiceMock ssl_{}; + Envoy::Http::HeaderMapImpl header_{}; FilterConfig filter_config_{}; FilterContext filter_context_{ - envoy::api::v2::core::Metadata::default_instance(), &connection_, + envoy::api::v2::core::Metadata::default_instance(), header_, &connection_, istio::envoy::config::filter::http::authn::v2alpha1::FilterConfig:: default_instance()}; @@ -168,8 +194,9 @@ class ValidateJwtTest : public testing::Test, envoy::api::v2::core::Metadata dynamic_metadata_; NiceMock connection_{}; // NiceMock ssl_{}; + Envoy::Http::HeaderMapImpl header_{}; FilterConfig filter_config_{}; - FilterContext filter_context_{dynamic_metadata_, &connection_, + FilterContext filter_context_{dynamic_metadata_, header_, &connection_, filter_config_}; MockAuthenticatorBase authenticator_{&filter_context_}; @@ -273,20 +300,100 @@ TEST_F(ValidateJwtTest, JwtPayloadAvailable) { R"({ "jwt": { "user": "issuer@foo.com/sub@foo.com", - "audiences": ["aud1"], + "audiences": ["aud1", "aud2"], "presenter": "", "claims": { - "aud": "aud1", - "iss": "issuer@foo.com", - "sub": "sub@foo.com", - "some-other-string-claims": "some-claims-kept" + "aud": ["aud1", "aud2"], + "iss": ["issuer@foo.com"], + "some-other-string-claims": ["some-claims-kept"], + "sub": ["sub@foo.com"], + }, + "raw_claims": "\n {\n \"iss\": \"issuer@foo.com\",\n \"sub\": \"sub@foo.com\",\n \"aud\": [\"aud1\", \"aud2\"],\n \"non-string-will-be-ignored\": 1512754205,\n \"some-other-string-claims\": \"some-claims-kept\"\n }\n ", + } + } + )", + &expected_payload, google::protobuf::util::JsonParseOptions{}); + + EXPECT_TRUE(authenticator_.validateJwt(jwt_, payload_)); + EXPECT_TRUE(MessageDifferencer::Equals(expected_payload, *payload_)); +} + +TEST_F(ValidateJwtTest, OriginalPayloadOfExchangedToken) { + jwt_.set_issuer("token-service"); + jwt_.add_jwt_headers(kExchangedTokenHeaderName); + + (*dynamic_metadata_.mutable_filter_metadata())[Utils::IstioFilterName::kJwt] + .MergeFrom( + MessageUtil::keyValueStruct("token-service", kExchangedTokenPayload)); + + Payload expected_payload; + JsonStringToMessage( + R"({ + "jwt": { + "user": "https://accounts.example.com/example-subject", + "claims": { + "iss": ["https://accounts.example.com"], + "sub": ["example-subject"], + "email": ["user@example.com"] + }, + "raw_claims": "{\"email\":\"user@example.com\",\"iss\":\"https://accounts.example.com\",\"sub\":\"example-subject\"}" + } + } + )", + &expected_payload, google::protobuf::util::JsonParseOptions{}); + + EXPECT_TRUE(authenticator_.validateJwt(jwt_, payload_)); + // On different platforms, the order of fields in raw_claims may be + // different. E.g., on MacOs, the raw_claims in the payload_ can be: + // raw_claims: + // "{\"email\":\"user@example.com\",\"sub\":\"example-subject\",\"iss\":\"https://accounts.example.com\"}" + // Therefore, raw_claims is skipped to avoid a flaky test. + MessageDifferencer diff; + const google::protobuf::FieldDescriptor* field = + expected_payload.jwt().GetDescriptor()->FindFieldByName("raw_claims"); + diff.IgnoreField(field); + EXPECT_TRUE(diff.Compare(expected_payload, *payload_)); +} + +TEST_F(ValidateJwtTest, OriginalPayloadOfExchangedTokenMissing) { + jwt_.set_issuer("token-service"); + jwt_.add_jwt_headers(kExchangedTokenHeaderName); + + (*dynamic_metadata_.mutable_filter_metadata())[Utils::IstioFilterName::kJwt] + .MergeFrom(MessageUtil::keyValueStruct( + "token-service", kExchangedTokenPayloadNoOriginalClaims)); + + // When no original_claims in an exchanged token, the token + // is treated as invalid. + EXPECT_FALSE(authenticator_.validateJwt(jwt_, payload_)); +} + +TEST_F(ValidateJwtTest, OriginalPayloadOfExchangedTokenNotInIntendedHeader) { + jwt_.set_issuer("token-service"); + + (*dynamic_metadata_.mutable_filter_metadata())[Utils::IstioFilterName::kJwt] + .MergeFrom( + MessageUtil::keyValueStruct("token-service", kExchangedTokenPayload)); + + Payload expected_payload; + JsonStringToMessage( + R"({ + "jwt": { + "user": "token-service/subject", + "audiences": ["aud1", "aud2"], + "claims": { + "iss": ["token-service"], + "sub": ["subject"], + "aud": ["aud1", "aud2"] }, - raw_claims: "\n {\n \"iss\": \"issuer@foo.com\",\n \"sub\": \"sub@foo.com\",\n \"aud\": \"aud1\",\n \"non-string-will-be-ignored\": 1512754205,\n \"some-other-string-claims\": \"some-claims-kept\"\n }\n " + "raw_claims":"\n {\n \"iss\": \"token-service\",\n \"sub\": \"subject\",\n \"aud\": [\"aud1\", \"aud2\"],\n \"original_claims\": {\n \"iss\": \"https://accounts.example.com\",\n \"sub\": \"example-subject\",\n \"email\": \"user@example.com\"\n }\n }\n " } } )", &expected_payload, google::protobuf::util::JsonParseOptions{}); + // When an exchanged token is not in the intended header, the token + // is treated as a normal token with its claims extracted. EXPECT_TRUE(authenticator_.validateJwt(jwt_, payload_)); EXPECT_TRUE(MessageDifferencer::Equals(expected_payload, *payload_)); } diff --git a/src/envoy/http/authn/authn_utils.cc b/src/envoy/http/authn/authn_utils.cc index 952114b9810..de7a6128921 100644 --- a/src/envoy/http/authn/authn_utils.cc +++ b/src/envoy/http/authn/authn_utils.cc @@ -13,8 +13,12 @@ * limitations under the License. */ +#include + +#include "absl/strings/match.h" #include "authn_utils.h" #include "common/json/json_loader.h" +#include "google/protobuf/struct.pb.h" #include "src/envoy/http/jwt_auth/jwt.h" namespace Envoy { @@ -24,52 +28,28 @@ namespace AuthN { namespace { // The JWT audience key name static const std::string kJwtAudienceKey = "aud"; +// The JWT issuer key name +static const std::string kJwtIssuerKey = "iss"; +// The key name for the original claims in an exchanged token +static const std::string kExchangedTokenOriginalPayload = "original_claims"; -// The JWT groups key name -static const std::string kJwtGroupsKey = "groups"; - -// Extract JWT audience into the JwtPayload. -// This function should to be called after the claims are extracted. -void ExtractJwtAudience( - const Envoy::Json::Object& obj, - const ::google::protobuf::Map< ::std::string, ::std::string>& claims, - istio::authn::JwtPayload* payload) { - const std::string& key = kJwtAudienceKey; - // "aud" can be either string array or string. +// Extract JWT claim as a string list. +// This function only extracts string and string list claims. +// A string claim is extracted as a string list of 1 item. +void ExtractStringList(const std::string& key, const Envoy::Json::Object& obj, + std::vector* list) { // First, try as string - if (claims.count(key) > 0) { - payload->add_audiences(claims.at(key)); - return; - } - // Next, try as string array try { - std::vector aud_vector = obj.getStringArray(key); - for (const std::string aud : aud_vector) { - payload->add_audiences(aud); - } + // Try as string, will throw execption if object type is not string. + list->push_back(obj.getString(key)); } catch (Json::Exception& e) { - // Not convertable to string array - } -} - -// Extract JWT groups into the JwtPayload. -// This function should to be called after the claims are extracted. -void ExtractJwtGroups( - const Envoy::Json::Object& obj, - const ::google::protobuf::Map< ::std::string, ::std::string>& claims, - istio::authn::JwtPayload* payload) { - const std::string& key = kJwtGroupsKey; - // "groups" can be either string array or string. - // First, try as string - if (claims.count(key) > 0) { - payload->add_groups(claims.at(key)); - return; + // Not convertable to string } // Next, try as string array try { - std::vector group_vector = obj.getStringArray(key); - for (const std::string group : group_vector) { - payload->add_groups(group); + std::vector vector = obj.getStringArray(key); + for (const std::string v : vector) { + list->push_back(v); } } catch (Json::Exception& e) { // Not convertable to string array @@ -89,41 +69,139 @@ bool AuthnUtils::ProcessJwtPayload(const std::string& payload_str, } *payload->mutable_raw_claims() = payload_str; - ::google::protobuf::Map< ::std::string, ::std::string>* claims = - payload->mutable_claims(); - - // Extract claims - json_obj->iterate( - [payload](const std::string& key, const Json::Object& obj) -> bool { - ::google::protobuf::Map< ::std::string, ::std::string>* claims = - payload->mutable_claims(); - // In current implementation, only string objects are extracted into - // claims. If call obj.asJsonString(), will get "panic: not reached" - // from json_loader.cc. - try { - // Try as string, will throw execption if object type is not string. - (*claims)[key] = obj.asString(); - } catch (Json::Exception& e) { - } - return true; - }); - // Extract audience - // ExtractJwtAudience() should be called after claims are extracted. - ExtractJwtAudience(*json_obj, payload->claims(), payload); - // ExtractJwtGroups() should be called after claims are extracted. - ExtractJwtGroups(*json_obj, payload->claims(), payload); + + auto claims = payload->mutable_claims()->mutable_fields(); + // Extract claims as string lists + json_obj->iterate([json_obj, claims](const std::string& key, + const Json::Object&) -> bool { + // In current implementation, only string/string list objects are extracted + std::vector list; + ExtractStringList(key, *json_obj, &list); + for (auto s : list) { + (*claims)[key].mutable_list_value()->add_values()->set_string_value(s); + } + return true; + }); + // Copy audience to the audience in context.proto + if (claims->find(kJwtAudienceKey) != claims->end()) { + for (const auto& v : (*claims)[kJwtAudienceKey].list_value().values()) { + payload->add_audiences(v.string_value()); + } + } + // Build user - if (claims->count("iss") > 0 && claims->count("sub") > 0) { - payload->set_user((*claims)["iss"] + "/" + (*claims)["sub"]); + if (claims->find("iss") != claims->end() && + claims->find("sub") != claims->end()) { + payload->set_user( + (*claims)["iss"].list_value().values().Get(0).string_value() + "/" + + (*claims)["sub"].list_value().values().Get(0).string_value()); } // Build authorized presenter (azp) - if (claims->count("azp") > 0) { - payload->set_presenter((*claims)["azp"]); + if (claims->find("azp") != claims->end()) { + payload->set_presenter( + (*claims)["azp"].list_value().values().Get(0).string_value()); } + return true; } +bool AuthnUtils::ExtractOriginalPayload(const std::string& token, + std::string* original_payload) { + Envoy::Json::ObjectSharedPtr json_obj; + try { + json_obj = Json::Factory::loadFromString(token); + } catch (...) { + return false; + } + + if (json_obj->hasObject(kExchangedTokenOriginalPayload) == false) { + return false; + } + + Envoy::Json::ObjectSharedPtr original_payload_obj; + try { + auto original_payload_obj = + json_obj->getObject(kExchangedTokenOriginalPayload); + *original_payload = original_payload_obj->asJsonString(); + ENVOY_LOG(debug, "{}: the original payload in exchanged token is {}", + __FUNCTION__, *original_payload); + } catch (...) { + ENVOY_LOG(debug, + "{}: original_payload in exchanged token is of invalid format.", + __FUNCTION__); + return false; + } + + return true; +} + +bool AuthnUtils::MatchString(const char* const str, + const iaapi::StringMatch& match) { + if (str == nullptr) { + return false; + } + switch (match.match_type_case()) { + case iaapi::StringMatch::kExact: { + return match.exact().compare(str) == 0; + } + case iaapi::StringMatch::kPrefix: { + return absl::StartsWith(str, match.prefix()); + } + case iaapi::StringMatch::kSuffix: { + return absl::EndsWith(str, match.suffix()); + } + case iaapi::StringMatch::kRegex: { + return std::regex_match(str, std::regex(match.regex())); + } + default: + return false; + } +} + +static bool matchRule(const char* const path, + const iaapi::Jwt_TriggerRule& rule) { + for (const auto& excluded : rule.excluded_paths()) { + if (AuthnUtils::MatchString(path, excluded)) { + // The rule is not matched if any of excluded_paths matched. + return false; + } + } + + if (rule.included_paths_size() > 0) { + for (const auto& included : rule.included_paths()) { + if (AuthnUtils::MatchString(path, included)) { + // The rule is matched if any of included_paths matched. + return true; + } + } + + // The rule is not matched if included_paths is not empty and none of them + // matched. + return false; + } + + // The rule is matched if none of excluded_paths matched and included_paths is + // empty. + return true; +} + +bool AuthnUtils::ShouldValidateJwtPerPath(const char* const path, + const iaapi::Jwt& jwt) { + // If the path is nullptr which shouldn't happen for a HTTP request or if + // there are no trigger rules at all, then simply return true as if there're + // no per-path jwt support. + if (path == nullptr || jwt.trigger_rules_size() == 0) { + return true; + } + for (const auto& rule : jwt.trigger_rules()) { + if (matchRule(path, rule)) { + return true; + } + } + return false; +} + } // namespace AuthN } // namespace Istio } // namespace Http -} // namespace Envoy +} // namespace Envoy \ No newline at end of file diff --git a/src/envoy/http/authn/authn_utils.h b/src/envoy/http/authn/authn_utils.h index 023b03e8f10..38f81913326 100644 --- a/src/envoy/http/authn/authn_utils.h +++ b/src/envoy/http/authn/authn_utils.h @@ -15,11 +15,15 @@ #pragma once +#include "authentication/v1alpha1/policy.pb.h" #include "common/common/logger.h" +#include "common/common/utility.h" #include "envoy/http/header_map.h" #include "envoy/json/json_object.h" #include "src/istio/authn/context.pb.h" +namespace iaapi = istio::authentication::v1alpha1; + namespace Envoy { namespace Http { namespace Istio { @@ -33,9 +37,24 @@ class AuthnUtils : public Logger::Loggable { // successfully. Otherwise, return false. static bool ProcessJwtPayload(const std::string& jwt_payload_str, istio::authn::JwtPayload* payload); + + // Parses the original_payload in an exchanged JWT. + // Returns true if original_payload can be + // parsed successfully. Otherwise, returns false. + static bool ExtractOriginalPayload(const std::string& token, + std::string* original_payload); + + // Returns true if str is matched to match. + static bool MatchString(const char* const str, + const iaapi::StringMatch& match); + + // Returns true if the jwt should be validated. It will check if the request + // path is matched to the trigger rule in the jwt. + static bool ShouldValidateJwtPerPath(const char* const path, + const iaapi::Jwt& jwt); }; } // namespace AuthN } // namespace Istio } // namespace Http -} // namespace Envoy +} // namespace Envoy \ No newline at end of file diff --git a/src/envoy/http/authn/authn_utils_test.cc b/src/envoy/http/authn/authn_utils_test.cc index 710a30e5bda..49e299b82b2 100644 --- a/src/envoy/http/authn/authn_utils_test.cc +++ b/src/envoy/http/authn/authn_utils_test.cc @@ -63,21 +63,47 @@ TEST(AuthnUtilsTest, GetJwtPayloadFromHeaderTest) { R"( user: "issuer@foo.com/sub@foo.com" audiences: ["aud1"] - claims { - key: "aud" - value: "aud1" - } - claims { - key: "iss" - value: "issuer@foo.com" - } - claims { - key: "sub" - value: "sub@foo.com" - } - claims { - key: "some-other-string-claims" - value: "some-claims-kept" + claims: { + fields: { + key: "aud" + value: { + list_value: { + values: { + string_value: "aud1" + } + } + } + } + fields: { + key: "iss" + value: { + list_value: { + values: { + string_value: "issuer@foo.com" + } + } + } + } + fields: { + key: "sub" + value: { + list_value: { + values: { + string_value: "sub@foo.com" + } + } + } + } + fields: { + key: "some-other-string-claims" + value: { + list_value: { + values: { + string_value: "some-claims-kept" + } + } + } + } } raw_claims: ")" + StringUtil::escape(kSecIstioAuthUserinfoHeaderValue) + R"(")", @@ -95,17 +121,37 @@ TEST(AuthnUtilsTest, ProcessJwtPayloadWithNoAudTest) { ASSERT_TRUE(Protobuf::TextFormat::ParseFromString( R"( user: "issuer@foo.com/sub@foo.com" - claims { - key: "iss" - value: "issuer@foo.com" - } - claims { - key: "sub" - value: "sub@foo.com" - } - claims { - key: "some-other-string-claims" - value: "some-claims-kept" + claims: { + fields: { + key: "iss" + value: { + list_value: { + values: { + string_value: "issuer@foo.com" + } + } + } + } + fields: { + key: "sub" + value: { + list_value: { + values: { + string_value: "sub@foo.com" + } + } + } + } + fields: { + key: "some-other-string-claims" + value: { + list_value: { + values: { + string_value: "some-claims-kept" + } + } + } + } } raw_claims: ")" + StringUtil::escape(kSecIstioAuthUserInfoHeaderWithNoAudValue) + @@ -127,23 +173,55 @@ TEST(AuthnUtilsTest, ProcessJwtPayloadWithTwoAudTest) { user: "issuer@foo.com/sub@foo.com" audiences: "aud1" audiences: "aud2" - claims { - key: "iss" - value: "issuer@foo.com" - } - claims { - key: "sub" - value: "sub@foo.com" - } - claims { - key: "some-other-string-claims" - value: "some-claims-kept" + claims: { + fields: { + key: "aud" + value: { + list_value: { + values: { + string_value: "aud1" + } + values: { + string_value: "aud2" + } + } + } + } + fields: { + key: "iss" + value: { + list_value: { + values: { + string_value: "issuer@foo.com" + } + } + } + } + fields: { + key: "sub" + value: { + list_value: { + values: { + string_value: "sub@foo.com" + } + } + } + } + fields: { + key: "some-other-string-claims" + value: { + list_value: { + values: { + string_value: "some-claims-kept" + } + } + } + } } raw_claims: ")" + StringUtil::escape(kSecIstioAuthUserInfoHeaderWithTwoAudValue) + R"(")", &expected_payload)); - // The payload returned from ProcessJwtPayload() should be the same as // the expected. When the aud is a string array, the aud is not saved in the // claims. @@ -153,6 +231,101 @@ TEST(AuthnUtilsTest, ProcessJwtPayloadWithTwoAudTest) { EXPECT_TRUE(MessageDifferencer::Equals(expected_payload, payload)); } +TEST(AuthnUtilsTest, MatchString) { + iaapi::StringMatch match; + EXPECT_FALSE(AuthnUtils::MatchString(nullptr, match)); + EXPECT_FALSE(AuthnUtils::MatchString("", match)); + + match.set_exact("exact"); + EXPECT_TRUE(AuthnUtils::MatchString("exact", match)); + EXPECT_FALSE(AuthnUtils::MatchString("exac", match)); + EXPECT_FALSE(AuthnUtils::MatchString("exacy", match)); + + match.set_prefix("prefix"); + EXPECT_TRUE(AuthnUtils::MatchString("prefix-1", match)); + EXPECT_TRUE(AuthnUtils::MatchString("prefix", match)); + EXPECT_FALSE(AuthnUtils::MatchString("prefi", match)); + EXPECT_FALSE(AuthnUtils::MatchString("prefiy", match)); + + match.set_suffix("suffix"); + EXPECT_TRUE(AuthnUtils::MatchString("1-suffix", match)); + EXPECT_TRUE(AuthnUtils::MatchString("suffix", match)); + EXPECT_FALSE(AuthnUtils::MatchString("suffi", match)); + EXPECT_FALSE(AuthnUtils::MatchString("suffiy", match)); + + match.set_regex(".+abc.+"); + EXPECT_TRUE(AuthnUtils::MatchString("1-abc-1", match)); + EXPECT_FALSE(AuthnUtils::MatchString("1-abc", match)); + EXPECT_FALSE(AuthnUtils::MatchString("abc-1", match)); + EXPECT_FALSE(AuthnUtils::MatchString("1-ac-1", match)); +} + +TEST(AuthnUtilsTest, ShouldValidateJwtPerPathExcluded) { + iaapi::Jwt jwt; + + // Create a rule that triggers on everything except /good-x and /allow-x. + auto* rule = jwt.add_trigger_rules(); + rule->add_excluded_paths()->set_exact("/good-x"); + rule->add_excluded_paths()->set_exact("/allow-x"); + EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/good-x", jwt)); + EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/allow-x", jwt)); + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/good-1", jwt)); + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/allow-1", jwt)); + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/other", jwt)); + + // Change the rule to only triggers on prefix /good and /allow. + rule->add_included_paths()->set_prefix("/good"); + rule->add_included_paths()->set_prefix("/allow"); + EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/good-x", jwt)); + EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/allow-x", jwt)); + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/good-1", jwt)); + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/allow-1", jwt)); + EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/other", jwt)); +} + +TEST(AuthnUtilsTest, ShouldValidateJwtPerPathIncluded) { + iaapi::Jwt jwt; + + // Create a rule that triggers on everything with prefix /good and /allow. + auto* rule = jwt.add_trigger_rules(); + rule->add_included_paths()->set_prefix("/good"); + rule->add_included_paths()->set_prefix("/allow"); + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/good-x", jwt)); + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/allow-x", jwt)); + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/good-2", jwt)); + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/allow-1", jwt)); + EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/other", jwt)); + + // Change the rule to also exclude /allow-x and /good-x. + rule->add_excluded_paths()->set_exact("/good-x"); + rule->add_excluded_paths()->set_exact("/allow-x"); + EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/good-x", jwt)); + EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/allow-x", jwt)); + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/good-2", jwt)); + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/allow-1", jwt)); + EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/other", jwt)); +} + +TEST(AuthnUtilsTest, ShouldValidateJwtPerPathDefault) { + iaapi::Jwt jwt; + + // Always trigger when path is unavailable. + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath(nullptr, jwt)); + + // Always trigger when there is no rules in jwt. + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/test", jwt)); + + // Add a rule that triggers on everything except /hello. + jwt.add_trigger_rules()->add_excluded_paths()->set_exact("/hello"); + EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/hello", jwt)); + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/other", jwt)); + + // Add another rule that triggers on path /hello. + jwt.add_trigger_rules()->add_included_paths()->set_exact("/hello"); + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/hello", jwt)); + EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/other", jwt)); +} + } // namespace } // namespace AuthN } // namespace Istio diff --git a/src/envoy/http/authn/filter_context.h b/src/envoy/http/authn/filter_context.h index 0b5e060f98e..6190ce7d076 100644 --- a/src/envoy/http/authn/filter_context.h +++ b/src/envoy/http/authn/filter_context.h @@ -19,6 +19,7 @@ #include "common/common/logger.h" #include "envoy/api/v2/core/base.pb.h" #include "envoy/config/filter/http/authn/v2alpha1/config.pb.h" +#include "envoy/http/filter.h" #include "envoy/network/connection.h" #include "src/istio/authn/context.pb.h" @@ -33,10 +34,11 @@ class FilterContext : public Logger::Loggable { public: FilterContext( const envoy::api::v2::core::Metadata& dynamic_metadata, - const Network::Connection* connection, + const HeaderMap& header_map, const Network::Connection* connection, const istio::envoy::config::filter::http::authn::v2alpha1::FilterConfig& filter_config) : dynamic_metadata_(dynamic_metadata), + header_map_(header_map), connection_(connection), filter_config_(filter_config) {} virtual ~FilterContext() {} @@ -70,11 +72,17 @@ class FilterContext : public Logger::Loggable { // returns false. bool getJwtPayload(const std::string& issuer, std::string* payload) const; + const HeaderMap& headerMap() const { return header_map_; } + private: // Const reference to request info dynamic metadata. This provides data that // output from other filters, e.g JWT. const envoy::api::v2::core::Metadata& dynamic_metadata_; + // Const reference to header map of the request. This provides request path + // that could be used to decide if a JWT should be used for validation. + const HeaderMap& header_map_; + // Pointer to network connection of the request. const Network::Connection* connection_; diff --git a/src/envoy/http/authn/filter_context_test.cc b/src/envoy/http/authn/filter_context_test.cc index 6fb89866e15..96689e84897 100644 --- a/src/envoy/http/authn/filter_context_test.cc +++ b/src/envoy/http/authn/filter_context_test.cc @@ -34,8 +34,9 @@ class FilterContextTest : public testing::Test { virtual ~FilterContextTest() {} envoy::api::v2::core::Metadata metadata_; + Envoy::Http::TestHeaderMapImpl header_{}; // This test suit does not use connection, so ok to use null for it. - FilterContext filter_context_{metadata_, nullptr, + FilterContext filter_context_{metadata_, header_, nullptr, istio::envoy::config::filter::http::authn:: v2alpha1::FilterConfig::default_instance()}; diff --git a/src/envoy/http/authn/http_filter.cc b/src/envoy/http/authn/http_filter.cc index f1f372fa960..5503b6d7fc7 100644 --- a/src/envoy/http/authn/http_filter.cc +++ b/src/envoy/http/authn/http_filter.cc @@ -42,26 +42,27 @@ void AuthenticationFilter::onDestroy() { ENVOY_LOG(debug, "Called AuthenticationFilter : {}", __func__); } -FilterHeadersStatus AuthenticationFilter::decodeHeaders(HeaderMap&, bool) { +FilterHeadersStatus AuthenticationFilter::decodeHeaders(HeaderMap& headers, + bool) { ENVOY_LOG(debug, "AuthenticationFilter::decodeHeaders with config\n{}", filter_config_.DebugString()); state_ = State::PROCESSING; filter_context_.reset(new Istio::AuthN::FilterContext( - decoder_callbacks_->requestInfo().dynamicMetadata(), + decoder_callbacks_->streamInfo().dynamicMetadata(), headers, decoder_callbacks_->connection(), filter_config_)); Payload payload; - if (!filter_config_.policy().peer_is_optional() && - !createPeerAuthenticator(filter_context_.get())->run(&payload)) { + if (!createPeerAuthenticator(filter_context_.get())->run(&payload) && + !filter_config_.policy().peer_is_optional()) { rejectRequest("Peer authentication failed."); return FilterHeadersStatus::StopIteration; } bool success = - filter_config_.policy().origin_is_optional() || - createOriginAuthenticator(filter_context_.get())->run(&payload); + createOriginAuthenticator(filter_context_.get())->run(&payload) || + filter_config_.policy().origin_is_optional(); if (!success) { rejectRequest("Origin authentication failed."); @@ -75,7 +76,7 @@ FilterHeadersStatus AuthenticationFilter::decodeHeaders(HeaderMap&, bool) { ProtobufWkt::Struct data; Utils::Authentication::SaveAuthAttributesToStruct( filter_context_->authenticationResult(), data); - decoder_callbacks_->requestInfo().setDynamicMetadata( + decoder_callbacks_->streamInfo().setDynamicMetadata( Utils::IstioFilterName::kAuthentication, data); ENVOY_LOG(debug, "Saved Dynamic Metadata:\n{}", data.DebugString()); } @@ -107,8 +108,8 @@ void AuthenticationFilter::rejectRequest(const std::string& message) { return; } state_ = State::REJECTED; - decoder_callbacks_->sendLocalReply(Http::Code::Unauthorized, message, - nullptr); + decoder_callbacks_->sendLocalReply(Http::Code::Unauthorized, message, nullptr, + absl::nullopt); } std::unique_ptr @@ -128,4 +129,4 @@ AuthenticationFilter::createOriginAuthenticator( } // namespace AuthN } // namespace Istio } // namespace Http -} // namespace Envoy +} // namespace Envoy \ No newline at end of file diff --git a/src/envoy/http/authn/http_filter_test.cc b/src/envoy/http/authn/http_filter_test.cc index b80de882208..7196384fae7 100644 --- a/src/envoy/http/authn/http_filter_test.cc +++ b/src/envoy/http/authn/http_filter_test.cc @@ -16,7 +16,7 @@ #include "src/envoy/http/authn/http_filter.h" #include "common/common/base64.h" #include "common/http/header_map_impl.h" -#include "common/request_info/request_info_impl.h" +#include "common/stream_info/stream_info_impl.h" #include "envoy/config/filter/http/authn/v2alpha1/config.pb.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -73,7 +73,8 @@ std::unique_ptr createAlwaysPassAuthenticator( _local(FilterContext *filter_context) : AuthenticatorBase(filter_context) {} bool run(Payload *) override { // Set some data to verify authentication result later. - auto payload = TestUtilities::CreateX509Payload("foo"); + auto payload = TestUtilities::CreateX509Payload( + "cluster.local/sa/test_user/ns/test_ns/"); filter_context()->setPeerResult(&payload); return true; } @@ -121,11 +122,11 @@ TEST_F(AuthenticationFilterTest, PeerFail) { .Times(1) .WillOnce(Invoke(createAlwaysFailAuthenticator)); DangerousDeprecatedTestTime test_time; - RequestInfo::RequestInfoImpl request_info(Http::Protocol::Http2, - test_time.timeSystem()); - EXPECT_CALL(decoder_callbacks_, requestInfo()) + StreamInfo::StreamInfoImpl stream_info(Http::Protocol::Http2, + test_time.timeSystem()); + EXPECT_CALL(decoder_callbacks_, streamInfo()) .Times(AtLeast(1)) - .WillRepeatedly(ReturnRef(request_info)); + .WillRepeatedly(ReturnRef(stream_info)); EXPECT_CALL(decoder_callbacks_, encodeHeaders_(_, _)) .Times(1) .WillOnce(testing::Invoke([](Http::HeaderMap &headers, bool) { @@ -134,7 +135,7 @@ TEST_F(AuthenticationFilterTest, PeerFail) { EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_.decodeHeaders(request_headers_, true)); EXPECT_FALSE(Utils::Authentication::GetResultFromMetadata( - request_info.dynamicMetadata())); + stream_info.dynamicMetadata())); } TEST_F(AuthenticationFilterTest, PeerPassOriginFail) { @@ -147,11 +148,11 @@ TEST_F(AuthenticationFilterTest, PeerPassOriginFail) { .Times(1) .WillOnce(Invoke(createAlwaysFailAuthenticator)); DangerousDeprecatedTestTime test_time; - RequestInfo::RequestInfoImpl request_info(Http::Protocol::Http2, - test_time.timeSystem()); - EXPECT_CALL(decoder_callbacks_, requestInfo()) + StreamInfo::StreamInfoImpl stream_info(Http::Protocol::Http2, + test_time.timeSystem()); + EXPECT_CALL(decoder_callbacks_, streamInfo()) .Times(AtLeast(1)) - .WillRepeatedly(ReturnRef(request_info)); + .WillRepeatedly(ReturnRef(stream_info)); EXPECT_CALL(decoder_callbacks_, encodeHeaders_(_, _)) .Times(1) .WillOnce(testing::Invoke([](Http::HeaderMap &headers, bool) { @@ -160,7 +161,7 @@ TEST_F(AuthenticationFilterTest, PeerPassOriginFail) { EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_.decodeHeaders(request_headers_, true)); EXPECT_FALSE(Utils::Authentication::GetResultFromMetadata( - request_info.dynamicMetadata())); + stream_info.dynamicMetadata())); } TEST_F(AuthenticationFilterTest, AllPass) { @@ -171,32 +172,38 @@ TEST_F(AuthenticationFilterTest, AllPass) { .Times(1) .WillOnce(Invoke(createAlwaysPassAuthenticator)); DangerousDeprecatedTestTime test_time; - RequestInfo::RequestInfoImpl request_info(Http::Protocol::Http2, - test_time.timeSystem()); - EXPECT_CALL(decoder_callbacks_, requestInfo()) + StreamInfo::StreamInfoImpl stream_info(Http::Protocol::Http2, + test_time.timeSystem()); + EXPECT_CALL(decoder_callbacks_, streamInfo()) .Times(AtLeast(1)) - .WillRepeatedly(ReturnRef(request_info)); + .WillRepeatedly(ReturnRef(stream_info)); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers_, true)); - EXPECT_EQ(1, request_info.dynamicMetadata().filter_metadata_size()); + EXPECT_EQ(1, stream_info.dynamicMetadata().filter_metadata_size()); const auto *data = Utils::Authentication::GetResultFromMetadata( - request_info.dynamicMetadata()); + stream_info.dynamicMetadata()); ASSERT_TRUE(data); ProtobufWkt::Struct expected_data; ASSERT_TRUE(Protobuf::TextFormat::ParseFromString(R"( + fields { + key: "source.namespace" + value { + string_value: "test_ns" + } + } fields { key: "source.principal" value { - string_value: "foo" + string_value: "cluster.local/sa/test_user/ns/test_ns/" } } fields { key: "source.user" value { - string_value: "foo" + string_value: "cluster.local/sa/test_user/ns/test_ns/" } })", &expected_data)); @@ -210,10 +217,70 @@ TEST_F(AuthenticationFilterTest, IgnoreBothFail) { *filter_config_.mutable_policy() = policy_; StrictMock filter(filter_config_); filter.setDecoderFilterCallbacks(decoder_callbacks_); + + EXPECT_CALL(filter, createPeerAuthenticator(_)) + .Times(1) + .WillOnce(Invoke(createAlwaysFailAuthenticator)); + EXPECT_CALL(filter, createOriginAuthenticator(_)) + .Times(1) + .WillOnce(Invoke(createAlwaysFailAuthenticator)); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter.decodeHeaders(request_headers_, true)); } +TEST_F(AuthenticationFilterTest, IgnoreBothPass) { + iaapi::Policy policy_; + ASSERT_TRUE( + Protobuf::TextFormat::ParseFromString(ingoreBothPolicy, &policy_)); + *filter_config_.mutable_policy() = policy_; + StrictMock filter(filter_config_); + filter.setDecoderFilterCallbacks(decoder_callbacks_); + + EXPECT_CALL(filter, createPeerAuthenticator(_)) + .Times(1) + .WillOnce(Invoke(createAlwaysPassAuthenticator)); + EXPECT_CALL(filter, createOriginAuthenticator(_)) + .Times(1) + .WillOnce(Invoke(createAlwaysPassAuthenticator)); + DangerousDeprecatedTestTime test_time; + StreamInfo::StreamInfoImpl stream_info(Http::Protocol::Http2, + test_time.timeSystem()); + EXPECT_CALL(decoder_callbacks_, streamInfo()) + .Times(AtLeast(1)) + .WillRepeatedly(ReturnRef(stream_info)); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, + filter.decodeHeaders(request_headers_, true)); + + EXPECT_EQ(1, stream_info.dynamicMetadata().filter_metadata_size()); + const auto *data = Utils::Authentication::GetResultFromMetadata( + stream_info.dynamicMetadata()); + ASSERT_TRUE(data); + + ProtobufWkt::Struct expected_data; + ASSERT_TRUE(Protobuf::TextFormat::ParseFromString(R"( + fields { + key: "source.namespace" + value { + string_value: "test_ns" + } + } + fields { + key: "source.principal" + value { + string_value: "cluster.local/sa/test_user/ns/test_ns/" + } + } + fields { + key: "source.user" + value { + string_value: "cluster.local/sa/test_user/ns/test_ns/" + } + })", + &expected_data)); + EXPECT_TRUE(TestUtility::protoEqual(expected_data, *data)); +} + } // namespace } // namespace AuthN } // namespace Istio diff --git a/src/envoy/http/authn/origin_authenticator.cc b/src/envoy/http/authn/origin_authenticator.cc index dca6d48af69..6d517fa1459 100644 --- a/src/envoy/http/authn/origin_authenticator.cc +++ b/src/envoy/http/authn/origin_authenticator.cc @@ -15,6 +15,7 @@ #include "src/envoy/http/authn/origin_authenticator.h" #include "authentication/v1alpha1/policy.pb.h" +#include "src/envoy/http/authn/authn_utils.h" using istio::authn::Payload; @@ -30,46 +31,57 @@ OriginAuthenticator::OriginAuthenticator(FilterContext* filter_context, : AuthenticatorBase(filter_context), policy_(policy) {} bool OriginAuthenticator::run(Payload* payload) { - bool success = false; + if (policy_.origins_size() == 0 && + policy_.principal_binding() == iaapi::PrincipalBinding::USE_ORIGIN) { + // Validation should reject policy that have rule to USE_ORIGIN but + // does not provide any origin method so this code should + // never reach. However, it's ok to treat it as authentication + // fails. + ENVOY_LOG(warn, + "Principal is binded to origin, but no method specified in " + "policy {}", + policy_.DebugString()); + return false; + } - if (policy_.origins_size() == 0) { - switch (policy_.principal_binding()) { - case iaapi::PrincipalBinding::USE_ORIGIN: - // Validation should reject policy that have rule to USE_ORIGIN but - // does not provide any origin method so this code should - // never reach. However, it's ok to treat it as authentication - // fails. - ENVOY_LOG(warn, - "Principal is binded to origin, but no method specified in " - "policy {}", - policy_.DebugString()); - break; - case iaapi::PrincipalBinding::USE_PEER: - // On the other hand, it's ok to have no (origin) methods if - // rule USE_SOURCE - success = true; - break; - default: - // Should never come here. - ENVOY_LOG(error, "Invalid binding value for policy {}", - policy_.DebugString()); - break; - } + const char* request_path = nullptr; + if (filter_context()->headerMap().Path() != nullptr) { + request_path = filter_context()->headerMap().Path()->value().c_str(); + ENVOY_LOG(debug, "Got request path {}", request_path); + } else { + ENVOY_LOG(error, + "Failed to get request path, JWT will always be used for " + "validation"); } + bool triggered = false; + bool triggered_success = false; for (const auto& method : policy_.origins()) { - if (validateJwt(method.jwt(), payload)) { - success = true; - break; + const auto& jwt = method.jwt(); + + if (AuthnUtils::ShouldValidateJwtPerPath(request_path, jwt)) { + ENVOY_LOG(debug, "Validating request path {} for jwt {}", request_path, + jwt.DebugString()); + // set triggered to true if any of the jwt trigger rule matched. + triggered = true; + if (validateJwt(jwt, payload)) { + ENVOY_LOG(debug, "JWT validation succeeded"); + triggered_success = true; + break; + } } } - if (success) { + // returns true if no jwt was triggered, or triggered and success. + if (!triggered || triggered_success) { filter_context()->setOriginResult(payload); filter_context()->setPrincipal(policy_.principal_binding()); + ENVOY_LOG(debug, "Origin authenticator succeeded"); + return true; } - return success; + ENVOY_LOG(debug, "Origin authenticator failed"); + return false; } } // namespace AuthN diff --git a/src/envoy/http/authn/origin_authenticator_test.cc b/src/envoy/http/authn/origin_authenticator_test.cc index a2a2cd7e858..7ed9b04eee0 100644 --- a/src/envoy/http/authn/origin_authenticator_test.cc +++ b/src/envoy/http/authn/origin_authenticator_test.cc @@ -41,6 +41,14 @@ namespace Istio { namespace AuthN { namespace { +const char kZeroOriginMethodPolicyBindPeer[] = R"( + principal_binding: USE_PEER +)"; + +const char kZeroOriginMethodPolicyBindOrigin[] = R"( + principal_binding: USE_ORIGIN +)"; + const char kSingleOriginMethodPolicy[] = R"( principal_binding: USE_ORIGIN origins { @@ -78,6 +86,54 @@ const char kPeerBinding[] = R"( } )"; +const char kSingleOriginMethodWithTriggerRulePolicy[] = R"( + principal_binding: USE_ORIGIN + origins { + jwt { + issuer: "abc.xyz" + trigger_rules: { + included_paths: { + exact: "/allow" + } + } + } + } +)"; + +const char kMultipleOriginMethodWithTriggerRulePolicy[] = R"( + principal_binding: USE_ORIGIN + origins { + jwt { + issuer: "one" + trigger_rules: { + excluded_paths: { + exact: "/bad" + } + } + } + } + origins { + jwt { + issuer: "two" + trigger_rules: { + included_paths: { + exact: "/two" + } + } + } + } + origins { + jwt { + issuer: "three" + trigger_rules: { + included_paths: { + exact: "/allow" + } + } + } + } +)"; + class MockOriginAuthenticator : public OriginAuthenticator { public: MockOriginAuthenticator(FilterContext* filter_context, @@ -121,8 +177,9 @@ class OriginAuthenticatorTest : public testing::TestWithParam { protected: std::unique_ptr> authenticator_; // envoy::api::v2::core::Metadata metadata_; + Envoy::Http::TestHeaderMapImpl header_{}; FilterContext filter_context_{ - envoy::api::v2::core::Metadata::default_instance(), nullptr, + envoy::api::v2::core::Metadata::default_instance(), header_, nullptr, istio::envoy::config::filter::http::authn::v2alpha1::FilterConfig:: default_instance()}; iaapi::Policy policy_; @@ -143,6 +200,11 @@ class OriginAuthenticatorTest : public testing::TestWithParam { // Indicates peer is set in the authN result before running. This is set from // test GetParam() bool set_peer_; + + void setPath(const std::string& path) { + header_.removePath(); + header_.addCopy(":path", path); + } }; TEST_P(OriginAuthenticatorTest, Empty) { @@ -155,6 +217,36 @@ TEST_P(OriginAuthenticatorTest, Empty) { filter_context_.authenticationResult())); } +// It should fail if the binding is USE_ORIGIN but origin methods are empty. +TEST_P(OriginAuthenticatorTest, ZeroMethodFail) { + ASSERT_TRUE(Protobuf::TextFormat::ParseFromString( + kZeroOriginMethodPolicyBindOrigin, &policy_)); + createAuthenticator(); + EXPECT_FALSE(authenticator_->run(payload_)); +} + +// It should pass if the binding is USE_PEER and origin methods are empty. +TEST_P(OriginAuthenticatorTest, ZeroMethodPass) { + ASSERT_TRUE(Protobuf::TextFormat::ParseFromString( + kZeroOriginMethodPolicyBindPeer, &policy_)); + createAuthenticator(); + + Result expected_result = TestUtilities::AuthNResultFromString(R"( + origin { + user: "bar" + presenter: "istio.io" + } + )"); + if (set_peer_) { + expected_result.set_principal("bar"); + expected_result.set_peer_user("bar"); + } + + EXPECT_TRUE(authenticator_->run(&jwt_extra_payload_)); + EXPECT_TRUE(TestUtility::protoEqual(expected_result, + filter_context_.authenticationResult())); +} + TEST_P(OriginAuthenticatorTest, SingleMethodPass) { ASSERT_TRUE(Protobuf::TextFormat::ParseFromString(kSingleOriginMethodPolicy, &policy_)); @@ -185,6 +277,51 @@ TEST_P(OriginAuthenticatorTest, SingleMethodFail) { filter_context_.authenticationResult())); } +TEST_P(OriginAuthenticatorTest, TriggeredWithNullPath) { + ASSERT_TRUE(Protobuf::TextFormat::ParseFromString( + kSingleOriginMethodWithTriggerRulePolicy, &policy_)); + + createAuthenticator(); + + EXPECT_CALL(*authenticator_, validateJwt(_, _)) + .Times(1) + .WillOnce(DoAll(SetArgPointee<1>(jwt_payload_), Return(true))); + + EXPECT_TRUE(authenticator_->run(payload_)); + EXPECT_TRUE(TestUtility::protoEqual(expected_result_when_pass_, + filter_context_.authenticationResult())); +} + +TEST_P(OriginAuthenticatorTest, SingleRuleTriggered) { + ASSERT_TRUE(Protobuf::TextFormat::ParseFromString( + kSingleOriginMethodWithTriggerRulePolicy, &policy_)); + + createAuthenticator(); + + EXPECT_CALL(*authenticator_, validateJwt(_, _)) + .Times(1) + .WillOnce(DoAll(SetArgPointee<1>(jwt_payload_), Return(true))); + + setPath("/allow"); + EXPECT_TRUE(authenticator_->run(payload_)); + EXPECT_TRUE(TestUtility::protoEqual(expected_result_when_pass_, + filter_context_.authenticationResult())); +} + +TEST_P(OriginAuthenticatorTest, SingleRuleNotTriggered) { + ASSERT_TRUE(Protobuf::TextFormat::ParseFromString( + kSingleOriginMethodWithTriggerRulePolicy, &policy_)); + + createAuthenticator(); + + EXPECT_CALL(*authenticator_, validateJwt(_, _)).Times(0); + + setPath("/bad"); + EXPECT_TRUE(authenticator_->run(payload_)); + EXPECT_TRUE(TestUtility::protoEqual(initial_result_, + filter_context_.authenticationResult())); +} + TEST_P(OriginAuthenticatorTest, Multiple) { ASSERT_TRUE(Protobuf::TextFormat::ParseFromString( kMultipleOriginMethodsPolicy, &policy_)); @@ -219,6 +356,54 @@ TEST_P(OriginAuthenticatorTest, MultipleFail) { filter_context_.authenticationResult())); } +TEST_P(OriginAuthenticatorTest, MultipleRuleTriggeredValidationSucceeded) { + ASSERT_TRUE(Protobuf::TextFormat::ParseFromString( + kMultipleOriginMethodWithTriggerRulePolicy, &policy_)); + + createAuthenticator(); + // First method triggered but failed, second method not triggered, third + // method triggered and succeeded. + EXPECT_CALL(*authenticator_, validateJwt(_, _)) + .Times(2) + .WillOnce(DoAll(SetArgPointee<1>(jwt_extra_payload_), Return(false))) + .WillOnce(DoAll(SetArgPointee<1>(jwt_payload_), Return(true))); + + setPath("/allow"); + EXPECT_TRUE(authenticator_->run(payload_)); + EXPECT_TRUE(TestUtility::protoEqual(expected_result_when_pass_, + filter_context_.authenticationResult())); +} + +TEST_P(OriginAuthenticatorTest, MultipleRuleTriggeredValidationFailed) { + ASSERT_TRUE(Protobuf::TextFormat::ParseFromString( + kMultipleOriginMethodWithTriggerRulePolicy, &policy_)); + + createAuthenticator(); + // Triggered on first and second method but all failed. + EXPECT_CALL(*authenticator_, validateJwt(_, _)) + .Times(2) + .WillRepeatedly( + DoAll(SetArgPointee<1>(jwt_extra_payload_), Return(false))); + + setPath("/two"); + EXPECT_FALSE(authenticator_->run(payload_)); + EXPECT_TRUE(TestUtility::protoEqual(initial_result_, + filter_context_.authenticationResult())); +} + +TEST_P(OriginAuthenticatorTest, MultipleRuleNotTriggered) { + ASSERT_TRUE(Protobuf::TextFormat::ParseFromString( + kMultipleOriginMethodWithTriggerRulePolicy, &policy_)); + + createAuthenticator(); + EXPECT_CALL(*authenticator_, validateJwt(_, _)).Times(0); + + setPath("/bad"); + EXPECT_TRUE(authenticator_->run(payload_)); + EXPECT_TRUE(TestUtility::protoEqual(initial_result_, + filter_context_.authenticationResult())); +} + TEST_P(OriginAuthenticatorTest, PeerBindingPass) { ASSERT_TRUE(Protobuf::TextFormat::ParseFromString(kPeerBinding, &policy_)); // Expected principal is from peer_user. diff --git a/src/envoy/http/authn/peer_authenticator_test.cc b/src/envoy/http/authn/peer_authenticator_test.cc index cce09dbdc99..ffda3a6f749 100644 --- a/src/envoy/http/authn/peer_authenticator_test.cc +++ b/src/envoy/http/authn/peer_authenticator_test.cc @@ -66,8 +66,9 @@ class PeerAuthenticatorTest : public testing::Test { protected: std::unique_ptr> authenticator_; + Envoy::Http::TestHeaderMapImpl header_; FilterContext filter_context_{ - envoy::api::v2::core::Metadata::default_instance(), nullptr, + envoy::api::v2::core::Metadata::default_instance(), header_, nullptr, istio::envoy::config::filter::http::authn::v2alpha1::FilterConfig:: default_instance()}; diff --git a/src/envoy/http/authn/sample/APToken/APToken-example1.jwt b/src/envoy/http/authn/sample/APToken/APToken-example1.jwt new file mode 100644 index 00000000000..82f1e6ab448 --- /dev/null +++ b/src/envoy/http/authn/sample/APToken/APToken-example1.jwt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJleGFtcGxlLWF1ZGllbmNlIiwiZW1haWwiOiJmb29AZ29vZ2xlLmNvbSIsImV4cCI6NDY5ODM2MTUwOCwiaWF0IjoxNTQ0NzYxNTA4LCJpc3MiOiJodHRwczovL2V4YW1wbGUudG9rZW5fc2VydmljZS5jb20iLCJpc3Rpb19hdHRyaWJ1dGVzIjpbeyJzb3VyY2UuaXAiOiIxMjcuMC4wLjEifV0sImtleTEiOlsidmFsMiIsInZhbDMiXSwib3JpZ2luYWxfY2xhaW1zIjp7ImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZXhhbXBsZS5jb20iLCJzdWIiOiJleGFtcGxlLXN1YmplY3QifSwic3ViIjoiaHR0cHM6Ly9hY2NvdW50cy5leGFtcGxlLmNvbS8xMjM0NTU2Nzg5MCJ9.mLm9Gmcd748anwybiPxGPEuYgJBChqoHkVOvRhQN-H9jMqVKyF-7ynud1CJp5n72VeMB1FzvKAV0ErzSyWQc0iofQywG6whYXP6zL-Oc0igUrLDvzb6PuBDkbWOcZrvHkHM4tIYAkF4j880GqMWEP3gGrykziIEY9g4povquCFSdkLjjyol2-Ge_6MFdayYoeWLLOaMP7tHiPTm_ajioQ4jcz5whBWu3DZWx4IuU5UIBYlHG_miJZv5zmwwQ60T1_p_sW7zkABJgDhCvu6cHh6g-hZdQvZbATFwMfN8VDzttTjRG8wuLlkQ1TTOCx5PDv-_gHfQfRWt8Z94HrIJPuQ \ No newline at end of file diff --git a/src/envoy/http/authn/sample/APToken/aptoken-envoy.conf b/src/envoy/http/authn/sample/APToken/aptoken-envoy.conf new file mode 100644 index 00000000000..a5905812f7c --- /dev/null +++ b/src/envoy/http/authn/sample/APToken/aptoken-envoy.conf @@ -0,0 +1,118 @@ +{ + "admin": { + "access_log_path": "/dev/stdout", + "address": { + "socket_address": { + "address": "0.0.0.0", + "port_value": 9001 + } + } + }, + "static_resources": { + "clusters": [ + { + "name": "service1", + "connect_timeout": "5s", + "type": "STATIC", + "hosts": [ + { + "socket_address": { + "address": "0.0.0.0", + "port_value": 8080 + } + } + ] + } + ], + "listeners": [ + { + "name": "server", + "address": { + "socket_address": { + "address": "0.0.0.0", + "port_value": 9090 + } + }, + "filter_chains": [ + { + "filters": [ + { + "name": "envoy.http_connection_manager", + "config": { + "codec_type": "AUTO", + "stat_prefix": "inbound_http", + "access_log": [ + { + "name": "envoy.file_access_log", + "config": { + "path": "/tmp/envoy-access.log" + } + } + ], + "http_filters": [ + { + "name": "jwt-auth", + "config": { + "rules": [ + { + "issuer": "https://example.token_service.com", + "local_jwks": { + "inline_string": "{ \"keys\":[ {\"e\":\"AQAB\",\"kid\":\"DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ\",\"kty\":\"RSA\",\"n\":\"xAE7eB6qugXyCAG3yhh7pkDkT65pHymX-P7KfIupjf59vsdo91bSP9C8H07pSAGQO1MV_xFj9VswgsCg4R6otmg5PV2He95lZdHtOcU5DXIg_pbhLdKXbi66GlVeK6ABZOUW3WYtnNHD-91gVuoeJT_DwtGGcp4ignkgXfkiEm4sw-4sfb4qdt5oLbyVpmW6x9cfa7vs2WTfURiCrBoUqgBo_-4WTiULmmHSGZHOjzwa8WtrtOQGsAFjIbno85jp6MnGGGZPYZbDAa_b3y5u-YpW7ypZrvD8BgtKVjgtQgZhLAGezMt0ua3DRrWnKqTZ0BJ_EyxOGuHJrLsn00fnMQ\"}]}", + }, + "from_headers": [{"name": "ingress-authorization"}], + "forward_payload_header": "test-jwt-payload-output" + } + ] + } + }, + { + "name":"istio_authn", + "config":{ + "policy":{ + "origins":[ + { + "jwt":{ + "issuer":"https://example.token_service.com", + "jwt_headers":["ingress-authorization"] + } + } + ], + "principal_binding":1 + } + } + }, + { + "name": "envoy.router" + } + ], + "route_config": { + "name": "backend", + "virtual_hosts": [ + { + "name": "backend", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "service1", + "timeout": "0s" + } + } + ] + } + ] + } + } + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/src/envoy/http/authn/sample/APToken/guide.txt b/src/envoy/http/authn/sample/APToken/guide.txt new file mode 100644 index 00000000000..7fdf21c9d6f --- /dev/null +++ b/src/envoy/http/authn/sample/APToken/guide.txt @@ -0,0 +1,18 @@ +This is a guide of sending an example exchanged token to +the jwt-authn filter and the Istio authn filter, and observing +that the example backend echoes back the request when +the authentication succeeds. + +1. Open a terminal, go to the root directory of the istio-proxy repository. +Start the example backend: + go run test/backend/echo/echo.go + +2. Build the Istio proxy and run the proxy with the config for authenticating +an example exchanged token. + bazel build //src/envoy:envoy + bazel-bin/src/envoy/envoy -l debug -c src/envoy/http/jwt_auth/sample/APToken/aptoken-envoy.conf + +3. Open a terminal, go to the root directory of the istio-proxy repository. +Send a request with the example exchanged token. + export token=$(cat src/envoy/http/jwt_auth/sample/APToken/APToken-example1.jwt) + curl --header "ingress-authorization:$token" http://localhost:9090/echo -d "hello world" diff --git a/src/envoy/http/jwt_auth/README.md b/src/envoy/http/jwt_auth/README.md index 899e15996cc..50fa0cbfe10 100644 --- a/src/envoy/http/jwt_auth/README.md +++ b/src/envoy/http/jwt_auth/README.md @@ -50,12 +50,6 @@ bazel-bin/src/envoy/envoy -c src/envoy/http/jwt_auth/sample/envoy.conf go run test/backend/echo/echo.go ``` -* Start (fake) issuer server. - -``` -go run src/envoy/http/jwt_auth/sample/fake_issuer.go src/envoy/http/jwt_auth/sample/pubkey.jwk -``` - * Then issue HTTP request to proxy. With valid JWT: diff --git a/src/envoy/http/jwt_auth/auth_store.h b/src/envoy/http/jwt_auth/auth_store.h index 2410929e4eb..da80c27958e 100644 --- a/src/envoy/http/jwt_auth/auth_store.h +++ b/src/envoy/http/jwt_auth/auth_store.h @@ -27,21 +27,24 @@ namespace Envoy { namespace Http { namespace JwtAuth { +typedef std::shared_ptr + JwtAuthenticationConstSharedPtr; + // The JWT auth store object to store config and caches. // It only has pubkey_cache for now. In the future it will have token cache. // It is per-thread and stored in thread local. class JwtAuthStore : public ThreadLocal::ThreadLocalObject { public: // Load the config from envoy config. - JwtAuthStore(const ::istio::envoy::config::filter::http::jwt_auth::v2alpha1:: - JwtAuthentication& config) - : config_(config), pubkey_cache_(config_), token_extractor_(config_) {} + JwtAuthStore(JwtAuthenticationConstSharedPtr config) + : config_(config), pubkey_cache_(*config_), token_extractor_(*config_) {} // Get the Config. const ::istio::envoy::config::filter::http::jwt_auth::v2alpha1:: JwtAuthentication& config() const { - return config_; + return *config_; } // Get the pubkey cache. @@ -52,8 +55,7 @@ class JwtAuthStore : public ThreadLocal::ThreadLocalObject { private: // Store the config. - const ::istio::envoy::config::filter::http::jwt_auth::v2alpha1:: - JwtAuthentication& config_; + JwtAuthenticationConstSharedPtr config_; // The public key cache, indexed by issuer. PubkeyCache pubkey_cache_; // The object to extract token. @@ -66,13 +68,16 @@ class JwtAuthStoreFactory : public Logger::Loggable { JwtAuthStoreFactory(const ::istio::envoy::config::filter::http::jwt_auth:: v2alpha1::JwtAuthentication& config, Server::Configuration::FactoryContext& context) - : config_(config), tls_(context.threadLocal().allocateSlot()) { - tls_->set( - [this](Event::Dispatcher&) -> ThreadLocal::ThreadLocalObjectSharedPtr { - return std::make_shared(config_); - }); + : config_(std::make_shared( + config)), + tls_(context.threadLocal().allocateSlot()) { + tls_->set([config = this->config_](Event::Dispatcher&) + -> ThreadLocal::ThreadLocalObjectSharedPtr { + return std::make_shared(config); + }); ENVOY_LOG(debug, "Loaded JwtAuthConfig: {}", - MessageUtil::getJsonStringFromMessage(config_, true)); + MessageUtil::getJsonStringFromMessage(*config_, true)); } // Get per-thread auth store object. @@ -80,8 +85,7 @@ class JwtAuthStoreFactory : public Logger::Loggable { private: // The auth config. - ::istio::envoy::config::filter::http::jwt_auth::v2alpha1::JwtAuthentication - config_; + JwtAuthenticationConstSharedPtr config_; // Thread local slot to store per-thread auth store ThreadLocal::SlotPtr tls_; }; diff --git a/src/envoy/http/jwt_auth/http_filter.cc b/src/envoy/http/jwt_auth/http_filter.cc index 40a50e9ffb5..c882a558cb3 100644 --- a/src/envoy/http/jwt_auth/http_filter.cc +++ b/src/envoy/http/jwt_auth/http_filter.cc @@ -63,7 +63,7 @@ void JwtVerificationFilter::onDone(const JwtAuth::Status& status) { Code code = Code(401); // Unauthorized // return failure reason as message body decoder_callbacks_->sendLocalReply(code, JwtAuth::StatusToString(status), - nullptr); + nullptr, absl::nullopt); return; } @@ -75,7 +75,7 @@ void JwtVerificationFilter::onDone(const JwtAuth::Status& status) { void JwtVerificationFilter::savePayload(const std::string& key, const std::string& payload) { - decoder_callbacks_->requestInfo().setDynamicMetadata( + decoder_callbacks_->streamInfo().setDynamicMetadata( Utils::IstioFilterName::kJwt, MessageUtil::keyValueStruct(key, payload)); } diff --git a/src/envoy/http/jwt_auth/integration_test/envoy.conf b/src/envoy/http/jwt_auth/integration_test/envoy.conf index 225c4db7023..763be2a7a97 100644 --- a/src/envoy/http/jwt_auth/integration_test/envoy.conf +++ b/src/envoy/http/jwt_auth/integration_test/envoy.conf @@ -1,65 +1,85 @@ { - "listeners": [ - { - "address": "tcp://{{ ip_loopback_address }}:0", - "bind_to_port": true, - "filters": [ - { - "type": "read", - "name": "http_connection_manager", - "config": { - "codec_type": "auto", - "stat_prefix": "ingress_http", - "route_config": { - "virtual_hosts": [ - { - "name": "backend", - "domains": ["*"], - "routes": [ + "admin": { + "access_log_path": "/dev/null", + "address": { + "socket_address": { + "address": "{{ ntop_ip_loopback_address }}", + "port_value": 0 + } + } + }, + "static_resources": { + "listeners": [ + { + "address": { + "socket_address": { + "address": "{{ ntop_ip_loopback_address }}", + "port_value": 0 + } + }, + "bind_to_port": true, + "filter_chains": [ + { + "filters": [ + { + "type": "read", + "name": "envoy.http_connection_manager", + "config": { + "codec_type": "auto", + "stat_prefix": "ingress_http", + "route_config": { + "virtual_hosts": [ + { + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "service1" + } + } + ] + } + ] + }, + "access_log": [ + { + "name": "envoy.file_access_log", + "config": { + "path": "/dev/null" + } + } + ], + "http_filters": [ { - "prefix": "/", - "cluster": "service1" + "name": "jwt-auth", + "config": {} + }, + { + "name": "envoy.router", + "config": {} } ] } - ] - }, - "access_log": [ - { - "path": "/dev/stdout" - } - ], - "filters": [ - { - "type": "decoder", - "name": "jwt-auth", - "config": {} - }, - { - "type": "decoder", - "name": "router", - "config": {} } ] } - } - ] - } - ], - "admin": { - "access_log_path": "/dev/stdout", - "address": "tcp://{{ ip_loopback_address }}:0" - }, - "cluster_manager": { + ] + } + ], "clusters": [ { "name": "service1", - "connect_timeout_ms": 5000, + "connect_timeout": "5s", "type": "static", - "lb_type": "round_robin", + "lb_policy": "round_robin", "hosts": [ { - "url": "tcp://{{ ip_loopback_address }}:{{ upstream_0 }}" + "socket_address": { + "address": "{{ ntop_ip_loopback_address }}", + "port_value": "{{ upstream_0 }}" + } } ] } diff --git a/src/envoy/http/jwt_auth/integration_test/envoy.conf.jwk b/src/envoy/http/jwt_auth/integration_test/envoy.conf.jwk index 0262d21357e..8a547c855a0 100644 --- a/src/envoy/http/jwt_auth/integration_test/envoy.conf.jwk +++ b/src/envoy/http/jwt_auth/integration_test/envoy.conf.jwk @@ -1,98 +1,123 @@ { - "listeners": [ - { - "address": "tcp://{{ ip_loopback_address }}:0", - "bind_to_port": true, - "filters": [ - { - "type": "read", - "name": "http_connection_manager", - "config": { - "codec_type": "auto", - "stat_prefix": "ingress_http", - "route_config": { - "virtual_hosts": [ - { - "name": "backend", - "domains": ["*"], - "routes": [ - { - "prefix": "/", - "cluster": "service1" - } - ] - } - ] - }, - "access_log": [ - { - "path": "/dev/null" - } - ], + "admin": { + "access_log_path": "/dev/null", + "address": { + "socket_address": { + "address": "{{ ntop_ip_loopback_address }}", + "port_value": 0 + } + } + }, + "static_resources": { + "listeners": [ + { + "address": { + "socket_address": { + "address": "{{ ntop_ip_loopback_address }}", + "port_value": 0 + } + }, + "filter_chains": [ + { "filters": [ { - "type": "decoder", - "name": "jwt-auth", + "name": "envoy.http_connection_manager", "config": { - "rules": [ - { - "issuer": "https://example.com", - "audiences": [ - "example_service" - ], - "remote_jwks": { - "http_uri": { - "uri": "http://example.com/foobar_cert", - "cluster": "example_issuer" + "codec_type": "auto", + "stat_prefix": "ingress_http", + "route_config": { + "virtual_hosts": [ + { + "name": "backend", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "service1" + } } - }, - "forward_payload_header": "test-jwt-payload-output" - } - ] - } - }, - { - "type": "decoder", - "name": "router", - "config": {} + ] + } + ] + }, + "access_log": [ + { + "name": "envoy.file_access_log", + "config": { + "path": "/dev/null" + } + } + ], + "http_filters": [ + { + "name": "jwt-auth", + "config": { + "rules": [ + { + "issuer": "https://example.com", + "audiences": [ + "example_service" + ], + "remote_jwks": { + "http_uri": { + "uri": "http://example.com/foobar_cert", + "cluster": "example_issuer" + } + }, + "forward_payload_header": "test-jwt-payload-output" + } + ] + } + }, + { + "name": "envoy.router", + "config": {} + } + ] + } } ] } - } - ] - } - ], - "admin": { - "access_log_path": "/dev/null", - "address": "tcp://{{ ip_loopback_address }}:0" - }, - "cluster_manager": { + ] + } + ], "clusters": [ { "name": "service1", - "connect_timeout_ms": 5000, + "connect_timeout": "5s", "type": "static", - "lb_type": "round_robin", + "lb_policy": "round_robin", "hosts": [ { - "url": "tcp://{{ ip_loopback_address }}:{{ upstream_0 }}" + "socket_address": { + "address": "{{ ntop_ip_loopback_address }}", + "port_value": "{{ upstream_0 }}" + } } ] }, { "name": "example_issuer", - "connect_timeout_ms": 5000, + "connect_timeout": "5s", "type": "static", - "circuit_breakers": { - "default": { - "max_pending_requests": 10000, - "max_requests": 10000 - } - }, - "lb_type": "round_robin", + "circuit_breakers": { + "thresholds": { + "max_pending_requests": 10000, + "max_requests": 10000 + } + }, + "lb_policy": "round_robin", "hosts": [ { - "url": "tcp://{{ ip_loopback_address }}:{{ upstream_1 }}" + "socket_address": { + "address": "{{ ntop_ip_loopback_address }}", + "port_value": "{{ upstream_1 }}" + } } ] } diff --git a/src/envoy/http/jwt_auth/integration_test/envoy_allow_missing_or_failed_jwt.conf.jwk b/src/envoy/http/jwt_auth/integration_test/envoy_allow_missing_or_failed_jwt.conf.jwk index 172fa3f7a2c..9f49cc80f5c 100644 --- a/src/envoy/http/jwt_auth/integration_test/envoy_allow_missing_or_failed_jwt.conf.jwk +++ b/src/envoy/http/jwt_auth/integration_test/envoy_allow_missing_or_failed_jwt.conf.jwk @@ -1,99 +1,124 @@ { - "listeners": [ - { - "address": "tcp://{{ ip_loopback_address }}:0", - "bind_to_port": true, - "filters": [ - { - "type": "read", - "name": "http_connection_manager", - "config": { - "codec_type": "auto", - "stat_prefix": "ingress_http", - "route_config": { - "virtual_hosts": [ - { - "name": "backend", - "domains": ["*"], - "routes": [ - { - "prefix": "/", - "cluster": "service1" - } - ] - } - ] - }, - "access_log": [ - { - "path": "/dev/null" - } - ], + "admin": { + "access_log_path": "/dev/null", + "address": { + "socket_address": { + "address": "{{ ntop_ip_loopback_address }}", + "port_value": 0 + } + } + }, + "static_resources": { + "listeners": [ + { + "address": { + "socket_address": { + "address": "{{ ntop_ip_loopback_address }}", + "port_value": 0 + } + }, + "filter_chains": [ + { "filters": [ { - "type": "decoder", - "name": "jwt-auth", + "name": "envoy.http_connection_manager", "config": { - "rules": [ - { - "issuer": "https://example.com", - "audiences": [ - "example_service" - ], - "remote_jwks": { - "http_uri": { - "uri": "http://example.com/foobar_cert", - "cluster": "example_issuer" - } - }, - "forward_payload_header": "test-jwt-payload-output" + "codec_type": "auto", + "stat_prefix": "ingress_http", + "route_config": { + "virtual_hosts": [ + { + "name": "backend", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "service1" + } + } + ] } - ], - "allow_missing_or_failed": true - } - }, - { - "type": "decoder", - "name": "router", - "config": {} + ] + }, + "access_log": [ + { + "name": "envoy.file_access_log", + "config": { + "path": "/dev/null" + } + } + ], + "http_filters": [ + { + "name": "jwt-auth", + "config": { + "rules": [ + { + "issuer": "https://example.com", + "audiences": [ + "example_service" + ], + "remote_jwks": { + "http_uri": { + "uri": "http://example.com/foobar_cert", + "cluster": "example_issuer" + } + }, + "forward_payload_header": "test-jwt-payload-output" + } + ], + "allow_missing_or_failed": true + } + }, + { + "name": "envoy.router", + "config": {} + } + ] + } } ] } - } - ] - } - ], - "admin": { - "access_log_path": "/dev/null", - "address": "tcp://{{ ip_loopback_address }}:0" - }, - "cluster_manager": { + ] + } + ], "clusters": [ { "name": "service1", - "connect_timeout_ms": 5000, + "connect_timeout": "5s", "type": "static", - "lb_type": "round_robin", + "lb_policy": "round_robin", "hosts": [ { - "url": "tcp://{{ ip_loopback_address }}:{{ upstream_0 }}" + "socket_address": { + "address": "{{ ntop_ip_loopback_address }}", + "port_value": "{{ upstream_0 }}" + } } ] }, { "name": "example_issuer", - "connect_timeout_ms": 5000, + "connect_timeout": "5s", "type": "static", - "circuit_breakers": { - "default": { - "max_pending_requests": 10000, - "max_requests": 10000 - } - }, - "lb_type": "round_robin", + "circuit_breakers": { + "thresholds": { + "max_pending_requests": 10000, + "max_requests": 10000 + } + }, + "lb_policy": "round_robin", "hosts": [ { - "url": "tcp://{{ ip_loopback_address }}:{{ upstream_1 }}" + "socket_address": { + "address": "{{ ntop_ip_loopback_address }}", + "port_value": "{{ upstream_1 }}" + } } ] } diff --git a/src/envoy/http/jwt_auth/integration_test/http_filter_integration_test.cc b/src/envoy/http/jwt_auth/integration_test/http_filter_integration_test.cc index 55bd663b95c..ad7c26b2555 100644 --- a/src/envoy/http/jwt_auth/integration_test/http_filter_integration_test.cc +++ b/src/envoy/http/jwt_auth/integration_test/http_filter_integration_test.cc @@ -36,18 +36,19 @@ class JwtVerificationFilterIntegrationTest public testing::TestWithParam { public: JwtVerificationFilterIntegrationTest() - : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, GetParam()) {} + : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, GetParam(), + realTime()) {} virtual ~JwtVerificationFilterIntegrationTest() {} /** * Initializer for an individual integration test. */ void SetUp() override { - fake_upstreams_.emplace_back( - new FakeUpstream(0, FakeHttpConnection::Type::HTTP1, version_)); + fake_upstreams_.emplace_back(new FakeUpstream( + 0, FakeHttpConnection::Type::HTTP1, version_, timeSystem())); registerPort("upstream_0", fake_upstreams_.back()->localAddress()->ip()->port()); - fake_upstreams_.emplace_back( - new FakeUpstream(0, FakeHttpConnection::Type::HTTP1, version_)); + fake_upstreams_.emplace_back(new FakeUpstream( + 0, FakeHttpConnection::Type::HTTP1, version_, timeSystem())); registerPort("upstream_1", fake_upstreams_.back()->localAddress()->ip()->port()); createTestServer(ConfigPath(), {"http"}); diff --git a/src/envoy/http/jwt_auth/jwt_authenticator.cc b/src/envoy/http/jwt_auth/jwt_authenticator.cc index c47b8b6d036..b309b6b169a 100644 --- a/src/envoy/http/jwt_auth/jwt_authenticator.cc +++ b/src/envoy/http/jwt_auth/jwt_authenticator.cc @@ -138,7 +138,7 @@ void JwtAuthenticator::FetchPubkey(PubkeyCacheItem* issuer) { ENVOY_LOG(debug, "fetch pubkey from [uri = {}]: start", uri_); request_ = cm_.httpAsyncClientForCluster(cluster).send( - std::move(message), *this, absl::optional()); + std::move(message), *this, Http::AsyncClient::RequestOptions()); } void JwtAuthenticator::onSuccess(MessagePtr&& response) { diff --git a/src/envoy/http/jwt_auth/jwt_authenticator_test.cc b/src/envoy/http/jwt_auth/jwt_authenticator_test.cc index fd35224e7fe..05bf3ad2fc3 100644 --- a/src/envoy/http/jwt_auth/jwt_authenticator_test.cc +++ b/src/envoy/http/jwt_auth/jwt_authenticator_test.cc @@ -292,11 +292,13 @@ class JwtAuthenticatorTest : public ::testing::Test { google::protobuf::util::Status status = ::google::protobuf::util::JsonStringToMessage(json_str, &config_); ASSERT_TRUE(status.ok()); - store_.reset(new JwtAuthStore(config_)); + config_ptr_ = std::make_shared(config_); + store_.reset(new JwtAuthStore(config_ptr_)); auth_.reset(new JwtAuthenticator(mock_cm_, *store_)); } JwtAuthentication config_; + JwtAuthenticationConstSharedPtr config_ptr_; std::unique_ptr store_; std::unique_ptr auth_; NiceMock mock_cm_; @@ -310,18 +312,16 @@ class MockUpstream { const std::string &response_body) : request_(&mock_cm.async_client_), response_body_(response_body) { ON_CALL(mock_cm.async_client_, send_(_, _, _)) - .WillByDefault( - Invoke([this](MessagePtr &, AsyncClient::Callbacks &cb, - const absl::optional &) - -> AsyncClient::Request * { - Http::MessagePtr response_message(new ResponseMessageImpl( - HeaderMapPtr{new TestHeaderMapImpl{{":status", "200"}}})); - response_message->body().reset( - new Buffer::OwnedImpl(response_body_)); - cb.onSuccess(std::move(response_message)); - called_count_++; - return &request_; - })); + .WillByDefault(Invoke([this](MessagePtr &, AsyncClient::Callbacks &cb, + const Http::AsyncClient::RequestOptions &) + -> AsyncClient::Request * { + Http::MessagePtr response_message(new ResponseMessageImpl( + HeaderMapPtr{new TestHeaderMapImpl{{":status", "200"}}})); + response_message->body().reset(new Buffer::OwnedImpl(response_body_)); + cb.onSuccess(std::move(response_message)); + called_count_++; + return &request_; + })); } int called_count() const { return called_count_; } @@ -484,7 +484,8 @@ TEST_F(JwtAuthenticatorTest, TestForwardJwt) { // Confit forward_jwt flag config_.mutable_rules(0)->set_forward(true); // Re-create store and auth objects. - store_.reset(new JwtAuthStore(config_)); + config_ptr_ = std::make_shared(config_); + store_.reset(new JwtAuthStore(config_ptr_)); auth_.reset(new JwtAuthenticator(mock_cm_, *store_)); MockUpstream mock_pubkey(mock_cm_, kPublicKey); @@ -629,7 +630,7 @@ TEST_F(JwtAuthenticatorTest, TestPubkeyFetchFail) { AsyncClient::Callbacks *callbacks; EXPECT_CALL(async_client, send_(_, _, _)) .WillOnce(Invoke([&](MessagePtr &message, AsyncClient::Callbacks &cb, - const absl::optional &) + const Http::AsyncClient::RequestOptions &) -> AsyncClient::Request * { EXPECT_EQ((TestHeaderMapImpl{ {":method", "GET"}, @@ -665,7 +666,7 @@ TEST_F(JwtAuthenticatorTest, TestInvalidPubkey) { AsyncClient::Callbacks *callbacks; EXPECT_CALL(async_client, send_(_, _, _)) .WillOnce(Invoke([&](MessagePtr &message, AsyncClient::Callbacks &cb, - const absl::optional &) + const Http::AsyncClient::RequestOptions &) -> AsyncClient::Request * { EXPECT_EQ((TestHeaderMapImpl{ {":method", "GET"}, @@ -702,7 +703,7 @@ TEST_F(JwtAuthenticatorTest, TestOnDestroy) { AsyncClient::Callbacks *callbacks; EXPECT_CALL(async_client, send_(_, _, _)) .WillOnce(Invoke([&](MessagePtr &message, AsyncClient::Callbacks &cb, - const absl::optional &) + const Http::AsyncClient::RequestOptions &) -> AsyncClient::Request * { EXPECT_EQ((TestHeaderMapImpl{ {":method", "GET"}, @@ -754,7 +755,8 @@ TEST_F(JwtAuthenticatorTest, TestInlineJwks) { local_jwks->set_inline_string(kPublicKey); // recreate store and auth with modified config. - store_.reset(new JwtAuthStore(config_)); + config_ptr_ = std::make_shared(config_); + store_.reset(new JwtAuthStore(config_ptr_)); auth_.reset(new JwtAuthenticator(mock_cm_, *store_)); MockUpstream mock_pubkey(mock_cm_, ""); diff --git a/src/envoy/http/jwt_auth/sample/envoy.conf b/src/envoy/http/jwt_auth/sample/envoy.conf index 2b6bf28381b..5322a2fa81a 100644 --- a/src/envoy/http/jwt_auth/sample/envoy.conf +++ b/src/envoy/http/jwt_auth/sample/envoy.conf @@ -1,97 +1,101 @@ { - "listeners": [ - { - "address": "tcp://0.0.0.0:9090", - "bind_to_port": true, - "filters": [ - { - "type": "read", - "name": "http_connection_manager", - "config": { - "codec_type": "auto", - "stat_prefix": "ingress_http", - "route_config": { - "virtual_hosts": [ - { - "name": "backend", - "domains": ["*"], - "routes": [ - { - "prefix": "/", - "cluster": "service1" - } - ] - } - ] - }, - "access_log": [ - { - "path": "/dev/stdout" - } - ], - "filters": [ - { - "type": "decoder", - "name": "jwt-auth", - "config": { - "rules": [ - { - "issuer": "628645741881-noabiu23f5a8m8ovd8ucv698lj78vv0l@developer.gserviceaccount.com", - "remote_jwks": { - "http_uri":{ - "uri": "http://localhost:8081/", - "cluster": "example_issuer" - } - } - } - ] - } - }, - { - "type": "decoder", - "name": "router", - "config": {} - } - ] - } - } - ] - } - ], "admin": { "access_log_path": "/dev/stdout", - "address": "tcp://0.0.0.0:9001" + "address": { + "socket_address": { + "address": "0.0.0.0", + "port_value": 9001 + } + } }, - "cluster_manager": { + "static_resources": { "clusters": [ { "name": "service1", - "connect_timeout_ms": 5000, - "type": "strict_dns", - "lb_type": "round_robin", + "connect_timeout": "5s", + "type": "STATIC", "hosts": [ { - "url": "tcp://0.0.0.0:8080" + "socket_address": { + "address": "0.0.0.0", + "port_value": 8080 + } } ] - }, + } + ], + "listeners": [ { - "name": "example_issuer", - "connect_timeout_ms": 5000, - "type": "strict_dns", - "circuit_breakers": { - "default": { - "max_pending_requests": 10000, - "max_requests": 10000 - } - }, - "lb_type": "round_robin", - "hosts": [ + "name": "server", + "address": { + "socket_address": { + "address": "0.0.0.0", + "port_value": 9090 + } + }, + "filter_chains": [ { - "url": "tcp://localhost:8081" + "filters": [ + { + "name": "envoy.http_connection_manager", + "config": { + "codec_type": "AUTO", + "stat_prefix": "inbound_http", + "access_log": [ + { + "name": "envoy.file_access_log", + "config": { + "path": "/tmp/envoy-access.log" + } + } + ], + "http_filters": [ + { + "name": "jwt-auth", + "config": { + "rules": [ + { + "issuer": "628645741881-noabiu23f5a8m8ovd8ucv698lj78vv0l@developer.gserviceaccount.com", + "local_jwks": { + "inline_string": "{ \"keys\" : [ {\"e\": \"AQAB\", \"kid\": \"b3319a147514df7ee5e4bcdee51350cc890cc89e\", \"kty\": \"RSA\",\"n\": \"qDi7Tx4DhNvPQsl1ofxxc2ePQFcs-L0mXYo6TGS64CY_2WmOtvYlcLNZjhuddZVV2X88m0MfwaSA16wE-RiKM9hqo5EY8BPXj57CMiYAyiHuQPp1yayjMgoE1P2jvp4eqF-BTillGJt5W5RuXti9uqfMtCQdagB8EC3MNRuU_KdeLgBy3lS3oo4LOYd-74kRBVZbk2wnmmb7IhP9OoLc1-7-9qU1uhpDxmE6JwBau0mDSwMnYDS4G_ML17dC-ZDtLd1i24STUw39KH0pcSdfFbL2NtEZdNeam1DDdk0iUtJSPZliUHJBI_pj8M-2Mn_oA8jBuI8YKwBqYkZCN1I95Q\"}]}" + }, + "forward_payload_header": "test-jwt-payload-output" + } + ] + } + }, + { + "name": "envoy.router" + } + ], + "route_config": { + "name": "backend", + "virtual_hosts": [ + { + "name": "backend", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "service1", + "timeout": "0s" + } + } + ] + } + ] + } + } + } + ] } ] } ] } -} +} \ No newline at end of file diff --git a/src/envoy/http/jwt_auth/token_extractor.cc b/src/envoy/http/jwt_auth/token_extractor.cc index fda9af9dfd8..0ca77ce5858 100644 --- a/src/envoy/http/jwt_auth/token_extractor.cc +++ b/src/envoy/http/jwt_auth/token_extractor.cc @@ -14,6 +14,7 @@ */ #include "src/envoy/http/jwt_auth/token_extractor.h" +#include "absl/strings/match.h" #include "common/common/utility.h" #include "common/http/utility.h" @@ -33,20 +34,20 @@ const std::string kParamAccessToken = "access_token"; } // namespace -JwtTokenExtractor::JwtTokenExtractor(const JwtAuthentication& config) { - for (const auto& jwt : config.rules()) { +JwtTokenExtractor::JwtTokenExtractor(const JwtAuthentication &config) { + for (const auto &jwt : config.rules()) { bool use_default = true; if (jwt.from_headers_size() > 0) { use_default = false; - for (const auto& header : jwt.from_headers()) { - auto& issuers = header_maps_[LowerCaseString(header.name())]; + for (const auto &header : jwt.from_headers()) { + auto &issuers = header_maps_[LowerCaseString(header.name())]; issuers.insert(jwt.issuer()); } } if (jwt.from_params_size() > 0) { use_default = false; - for (const std::string& param : jwt.from_params()) { - auto& issuers = param_maps_[param]; + for (const std::string ¶m : jwt.from_params()) { + auto &issuers = param_maps_[param]; issuers.insert(jwt.issuer()); } } @@ -55,21 +56,21 @@ JwtTokenExtractor::JwtTokenExtractor(const JwtAuthentication& config) { if (use_default) { authorization_issuers_.insert(jwt.issuer()); - auto& param_issuers = param_maps_[kParamAccessToken]; + auto ¶m_issuers = param_maps_[kParamAccessToken]; param_issuers.insert(jwt.issuer()); } } } void JwtTokenExtractor::Extract( - const HeaderMap& headers, - std::vector>* tokens) const { + const HeaderMap &headers, + std::vector> *tokens) const { if (!authorization_issuers_.empty()) { - const HeaderEntry* entry = headers.Authorization(); + const HeaderEntry *entry = headers.Authorization(); if (entry) { // Extract token from header. - const HeaderString& value = entry->value(); - if (StringUtil::startsWith(value.c_str(), kBearerPrefix, true)) { + const HeaderString &value = entry->value(); + if (absl::StartsWith(value.getStringView(), kBearerPrefix)) { tokens->emplace_back(new Token(value.c_str() + kBearerPrefix.length(), authorization_issuers_, true, nullptr)); // Only take the first one. @@ -79,12 +80,21 @@ void JwtTokenExtractor::Extract( } // Check header first - for (const auto& header_it : header_maps_) { - const HeaderEntry* entry = headers.get(header_it.first); + for (const auto &header_it : header_maps_) { + const HeaderEntry *entry = headers.get(header_it.first); if (entry) { + std::string token; + absl::string_view val = entry->value().getStringView(); + size_t pos = val.find(' '); + if (pos != absl::string_view::npos) { + // If the header value has prefix, trim the prefix. + token = entry->value().c_str() + pos + 1; + } else { + token = std::string(entry->value().c_str(), entry->value().size()); + } + tokens->emplace_back( - new Token(std::string(entry->value().c_str(), entry->value().size()), - header_it.second, false, &header_it.first)); + new Token(token, header_it.second, false, &header_it.first)); // Only take the first one. return; } @@ -94,10 +104,10 @@ void JwtTokenExtractor::Extract( return; } - const auto& params = Utility::parseQueryString(std::string( + const auto ¶ms = Utility::parseQueryString(std::string( headers.Path()->value().c_str(), headers.Path()->value().size())); - for (const auto& param_it : param_maps_) { - const auto& it = params.find(param_it.first); + for (const auto ¶m_it : param_maps_) { + const auto &it = params.find(param_it.first); if (it != params.end()) { tokens->emplace_back( new Token(it->second, param_it.second, false, nullptr)); diff --git a/src/envoy/http/jwt_auth/token_extractor_test.cc b/src/envoy/http/jwt_auth/token_extractor_test.cc index 8c5dac0d69f..d9873f13a05 100644 --- a/src/envoy/http/jwt_auth/token_extractor_test.cc +++ b/src/envoy/http/jwt_auth/token_extractor_test.cc @@ -136,22 +136,26 @@ TEST_F(JwtTokenExtractorTest, TestDefaultParamLocation) { } TEST_F(JwtTokenExtractorTest, TestCustomHeaderToken) { - auto headers = TestHeaderMapImpl{{"token-header", "jwt_token"}}; - std::vector> tokens; - extractor_->Extract(headers, &tokens); - EXPECT_EQ(tokens.size(), 1); + std::vector headerVals = {"jwt_token", "istio jwt_token"}; - EXPECT_EQ(tokens[0]->token(), "jwt_token"); + for (const auto& v : headerVals) { + auto headers = TestHeaderMapImpl{{"token-header", v}}; + std::vector> tokens; + extractor_->Extract(headers, &tokens); + EXPECT_EQ(tokens.size(), 1); - EXPECT_FALSE(tokens[0]->IsIssuerAllowed("issuer1")); - EXPECT_TRUE(tokens[0]->IsIssuerAllowed("issuer2")); - EXPECT_FALSE(tokens[0]->IsIssuerAllowed("issuer3")); - EXPECT_TRUE(tokens[0]->IsIssuerAllowed("issuer4")); - EXPECT_FALSE(tokens[0]->IsIssuerAllowed("unknown_issuer")); + EXPECT_EQ(tokens[0]->token(), "jwt_token"); - // Test token remove - tokens[0]->Remove(&headers); - EXPECT_FALSE(headers.get(LowerCaseString("token-header"))); + EXPECT_FALSE(tokens[0]->IsIssuerAllowed("issuer1")); + EXPECT_TRUE(tokens[0]->IsIssuerAllowed("issuer2")); + EXPECT_FALSE(tokens[0]->IsIssuerAllowed("issuer3")); + EXPECT_TRUE(tokens[0]->IsIssuerAllowed("issuer4")); + EXPECT_FALSE(tokens[0]->IsIssuerAllowed("unknown_issuer")); + + // Test token remove + tokens[0]->Remove(&headers); + EXPECT_FALSE(headers.get(LowerCaseString("token-header"))); + } } TEST_F(JwtTokenExtractorTest, TestCustomParamToken) { diff --git a/src/envoy/http/mixer/BUILD b/src/envoy/http/mixer/BUILD index 34644658768..ca1d40d25fa 100644 --- a/src/envoy/http/mixer/BUILD +++ b/src/envoy/http/mixer/BUILD @@ -42,6 +42,7 @@ envoy_cc_library( "//src/envoy/utils:authn_lib", "//src/envoy/utils:utils_lib", "//src/istio/control/http:control_lib", + "//src/istio/utils:utils_lib", "@envoy//source/exe:envoy_common_lib", ], ) diff --git a/src/envoy/http/mixer/control.cc b/src/envoy/http/mixer/control.cc index 5a092f0dfb8..32b7a384ab8 100644 --- a/src/envoy/http/mixer/control.cc +++ b/src/envoy/http/mixer/control.cc @@ -14,29 +14,46 @@ */ #include "src/envoy/http/mixer/control.h" +#include "include/istio/utils/local_attributes.h" + +using ::istio::mixer::v1::Attributes; +using ::istio::utils::LocalNode; namespace Envoy { namespace Http { namespace Mixer { -Control::Control(const Config& config, Upstream::ClusterManager& cm, - Event::Dispatcher& dispatcher, +Control::Control(ControlDataSharedPtr control_data, + Upstream::ClusterManager& cm, Event::Dispatcher& dispatcher, Runtime::RandomGenerator& random, Stats::Scope& scope, - Utils::MixerFilterStats& stats) - : config_(config), + const LocalInfo::LocalInfo& local_info) + : control_data_(control_data), check_client_factory_(Utils::GrpcClientFactoryForCluster( - config_.check_cluster(), cm, scope, dispatcher.timeSystem())), + control_data_->config().check_cluster(), cm, scope, + dispatcher.timeSystem())), report_client_factory_(Utils::GrpcClientFactoryForCluster( - config_.report_cluster(), cm, scope, dispatcher.timeSystem())), - stats_obj_(dispatcher, stats, - config_.config_pb().transport().stats_update_interval(), + control_data_->config().report_cluster(), cm, scope, + dispatcher.timeSystem())), + stats_obj_(dispatcher, control_data_->stats(), + control_data_->config() + .config_pb() + .transport() + .stats_update_interval(), [this](::istio::mixerclient::Statistics* stat) -> bool { return GetStats(stat); }) { - Utils::SerializeForwardedAttributes(config_.config_pb().transport(), - &serialized_forward_attributes_); + auto& logger = Logger::Registry::getLog(Logger::Id::config); + LocalNode local_node; + if (!Utils::ExtractNodeInfo(local_info.node(), &local_node)) { + ENVOY_LOG_TO_LOGGER( + logger, warn, + "Missing required node metadata: NODE_UID, NODE_NAMESPACE"); + } + ::istio::utils::SerializeForwardedAttributes(local_node, + &serialized_forward_attributes_); - ::istio::control::http::Controller::Options options(config_.config_pb()); + ::istio::control::http::Controller::Options options( + control_data_->config().config_pb(), local_node); Utils::CreateEnvironment(dispatcher, random, *check_client_factory_, *report_client_factory_, diff --git a/src/envoy/http/mixer/control.h b/src/envoy/http/mixer/control.h index bcb69bff702..e4c7c7edbed 100644 --- a/src/envoy/http/mixer/control.h +++ b/src/envoy/http/mixer/control.h @@ -15,11 +15,14 @@ #pragma once +#include "common/common/logger.h" #include "envoy/event/dispatcher.h" +#include "envoy/local_info/local_info.h" #include "envoy/runtime/runtime.h" #include "envoy/thread_local/thread_local.h" #include "envoy/upstream/cluster_manager.h" #include "include/istio/control/http/controller.h" +#include "include/istio/utils/local_attributes.h" #include "src/envoy/http/mixer/config.h" #include "src/envoy/utils/grpc_transport.h" #include "src/envoy/utils/mixer_control.h" @@ -29,13 +32,28 @@ namespace Envoy { namespace Http { namespace Mixer { +class ControlData { + public: + ControlData(std::unique_ptr config, Utils::MixerFilterStats stats) + : config_(std::move(config)), stats_(stats) {} + + const Config& config() { return *config_; } + Utils::MixerFilterStats& stats() { return stats_; } + + private: + std::unique_ptr config_; + Utils::MixerFilterStats stats_; +}; + +typedef std::shared_ptr ControlDataSharedPtr; + // The control object created per-thread. class Control final : public ThreadLocal::ThreadLocalObject { public: // The constructor. - Control(const Config& config, Upstream::ClusterManager& cm, + Control(ControlDataSharedPtr control_data, Upstream::ClusterManager& cm, Event::Dispatcher& dispatcher, Runtime::RandomGenerator& random, - Stats::Scope& scope, Utils::MixerFilterStats& stats); + Stats::Scope& scope, const LocalInfo::LocalInfo& local_info); // Get low-level controller object. ::istio::control::http::Controller* controller() { return controller_.get(); } @@ -47,8 +65,8 @@ class Control final : public ThreadLocal::ThreadLocalObject { // Call controller to get statistics. bool GetStats(::istio::mixerclient::Statistics* stat); - // The mixer config. - const Config& config_; + // The control data. + ControlDataSharedPtr control_data_; // Pre-serialized attributes_for_mixer_proxy. std::string serialized_forward_attributes_; // async client factories diff --git a/src/envoy/http/mixer/control_factory.h b/src/envoy/http/mixer/control_factory.h index e6765cad102..2dc21057b98 100644 --- a/src/envoy/http/mixer/control_factory.h +++ b/src/envoy/http/mixer/control_factory.h @@ -16,6 +16,7 @@ #pragma once #include "common/common/logger.h" +#include "envoy/local_info/local_info.h" #include "src/envoy/http/mixer/control.h" #include "src/envoy/utils/stats.h" @@ -35,29 +36,36 @@ class ControlFactory : public Logger::Loggable { public: ControlFactory(std::unique_ptr config, Server::Configuration::FactoryContext& context) - : config_(std::move(config)), - tls_(context.threadLocal().allocateSlot()), - stats_{ALL_MIXER_FILTER_STATS( - POOL_COUNTER_PREFIX(context.scope(), kHttpStatsPrefix))} { + : control_data_(std::make_shared( + std::move(config), + generateStats(kHttpStatsPrefix, context.scope()))), + tls_(context.threadLocal().allocateSlot()) { Upstream::ClusterManager& cm = context.clusterManager(); Runtime::RandomGenerator& random = context.random(); Stats::Scope& scope = context.scope(); - tls_->set([this, &cm, &random, &scope](Event::Dispatcher& dispatcher) + const LocalInfo::LocalInfo& local_info = context.localInfo(); + + tls_->set([control_data = this->control_data_, &cm, &random, &scope, + &local_info](Event::Dispatcher& dispatcher) -> ThreadLocal::ThreadLocalObjectSharedPtr { - return std::make_shared(*config_, cm, dispatcher, random, scope, - stats_); + return std::make_shared(control_data, cm, dispatcher, random, + scope, local_info); }); } Control& control() { return tls_->getTyped(); } private: - // Own the config object. - std::unique_ptr config_; + // Generates stats struct. + static Utils::MixerFilterStats generateStats(const std::string& name, + Stats::Scope& scope) { + return {ALL_MIXER_FILTER_STATS(POOL_COUNTER_PREFIX(scope, name))}; + } + + // The control data object + ControlDataSharedPtr control_data_; // Thread local slot. ThreadLocal::SlotPtr tls_; - // This stats object. - Utils::MixerFilterStats stats_; }; } // namespace Mixer diff --git a/src/envoy/http/mixer/filter.cc b/src/envoy/http/mixer/filter.cc index 8c55fd4552a..bf716d0ff3c 100644 --- a/src/envoy/http/mixer/filter.cc +++ b/src/envoy/http/mixer/filter.cc @@ -24,33 +24,11 @@ #include "src/envoy/utils/header_update.h" using ::google::protobuf::util::Status; -using ::istio::mixer::v1::config::client::ServiceConfig; using ::istio::mixerclient::CheckResponseInfo; namespace Envoy { namespace Http { namespace Mixer { -namespace { - -// Per route opaque data for "destination.service". -const std::string kPerRouteDestinationService("destination.service"); -// Per route opaque data name "mixer" is base64(JSON(ServiceConfig)) -const std::string kPerRouteMixer("mixer"); -// Per route opaque data name "mixer_sha" is SHA(JSON(ServiceConfig)) -const std::string kPerRouteMixerSha("mixer_sha"); - -// Read a string value from a string map. -bool ReadStringMap(const std::multimap& string_map, - const std::string& name, std::string* value) { - auto it = string_map.find(name); - if (it != string_map.end()) { - *value = it->second; - return true; - } - return false; -} - -} // namespace Filter::Filter(Control& control) : control_(control), @@ -77,45 +55,6 @@ void Filter::ReadPerRouteConfig( config->service_config_id = route_cfg->hash; return; } - - const auto& string_map = entry->opaqueConfig(); - ReadStringMap(string_map, kPerRouteDestinationService, - &config->destination_service); - - if (!ReadStringMap(string_map, kPerRouteMixerSha, - &config->service_config_id) || - config->service_config_id.empty()) { - return; - } - - if (control_.controller()->LookupServiceConfig(config->service_config_id)) { - return; - } - - std::string config_base64; - if (!ReadStringMap(string_map, kPerRouteMixer, &config_base64)) { - ENVOY_LOG(warn, "Service {} missing [mixer] per-route attribute", - config->destination_service); - return; - } - std::string config_json = Base64::decode(config_base64); - if (config_json.empty()) { - ENVOY_LOG(warn, "Service {} invalid base64 config data", - config->destination_service); - return; - } - ServiceConfig config_pb; - auto status = Utils::ParseJsonMessage(config_json, &config_pb); - if (!status.ok()) { - ENVOY_LOG(warn, - "Service {} failed to convert JSON config to protobuf, error: {}", - config->destination_service, status.ToString()); - return; - } - control_.controller()->AddServiceConfig(config->service_config_id, config_pb); - ENVOY_LOG(info, "Service {}, config_id {}, config: {}", - config->destination_service, config->service_config_id, - MessageUtil::getJsonStringFromMessage(config_pb, true)); } FilterHeadersStatus Filter::decodeHeaders(HeaderMap& headers, bool) { @@ -132,7 +71,7 @@ FilterHeadersStatus Filter::decodeHeaders(HeaderMap& headers, bool) { state_ = Calling; initiating_call_ = true; CheckData check_data(headers, - decoder_callbacks_->requestInfo().dynamicMetadata(), + decoder_callbacks_->streamInfo().dynamicMetadata(), decoder_callbacks_->connection()); Utils::HeaderUpdate header_update(&headers); headers_ = &headers; @@ -216,33 +155,43 @@ void Filter::completeCheck(const CheckResponseInfo& info) { return; } - if (!status.ok()) { - state_ = Responded; - int status_code = ::istio::utils::StatusHttpCode(status.error_code()); - decoder_callbacks_->sendLocalReply(Code(status_code), status.ToString(), - nullptr); - return; - } - - state_ = Complete; route_directive_ = info.route_directive; + Utils::CheckResponseInfoToStreamInfo(info, decoder_callbacks_->streamInfo()); + // handle direct response from the route directive - if (status.ok() && route_directive_.direct_response_code() != 0) { - ENVOY_LOG(debug, "Mixer::Filter direct response"); + if (route_directive_.direct_response_code() != 0) { + int status_code = route_directive_.direct_response_code(); + ENVOY_LOG(debug, "Mixer::Filter direct response {}", status_code); state_ = Responded; decoder_callbacks_->sendLocalReply( - Code(route_directive_.direct_response_code()), - route_directive_.direct_response_body(), [this](HeaderMap& headers) { + Code(status_code), route_directive_.direct_response_body(), + [this](HeaderMap& headers) { UpdateHeaders(headers, route_directive_.response_header_operations()); - }); + }, + absl::nullopt); return; } + // create a local reply for status not OK even if there is no direct response + if (!status.ok()) { + state_ = Responded; + + int status_code = ::istio::utils::StatusHttpCode(status.error_code()); + decoder_callbacks_->sendLocalReply(Code(status_code), status.ToString(), + nullptr, absl::nullopt); + return; + } + + state_ = Complete; + // handle request header operations if (nullptr != headers_) { UpdateHeaders(*headers_, route_directive_.request_header_operations()); headers_ = nullptr; + if (route_directive_.request_header_operations().size() > 0) { + decoder_callbacks_->clearRouteCache(); + } } if (!initiating_call_) { @@ -266,7 +215,7 @@ void Filter::onDestroy() { void Filter::log(const HeaderMap* request_headers, const HeaderMap* response_headers, const HeaderMap* response_trailers, - const RequestInfo::RequestInfo& request_info) { + const StreamInfo::StreamInfo& stream_info) { ENVOY_LOG(debug, "Called Mixer::Filter : {}", __func__); if (!handler_) { if (request_headers == nullptr) { @@ -275,19 +224,19 @@ void Filter::log(const HeaderMap* request_headers, // Here Request is rejected by other filters, Mixer filter is not called. ::istio::control::http::Controller::PerRouteConfig config; - ReadPerRouteConfig(request_info.routeEntry(), &config); + ReadPerRouteConfig(stream_info.routeEntry(), &config); handler_ = control_.controller()->CreateRequestHandler(config); - - CheckData check_data(*request_headers, request_info.dynamicMetadata(), - nullptr); - handler_->ExtractRequestAttributes(&check_data); } + + // If check is NOT called, check attributes are not extracted. + CheckData check_data(*request_headers, stream_info.dynamicMetadata(), + decoder_callbacks_->connection()); // response trailer header is not counted to response total size. - ReportData report_data(response_headers, response_trailers, request_info, + ReportData report_data(response_headers, response_trailers, stream_info, request_total_size_); - handler_->Report(&report_data); + handler_->Report(&check_data, &report_data); } } // namespace Mixer } // namespace Http -} // namespace Envoy +} // namespace Envoy \ No newline at end of file diff --git a/src/envoy/http/mixer/filter.h b/src/envoy/http/mixer/filter.h index d1e73e56255..326c60acac5 100644 --- a/src/envoy/http/mixer/filter.h +++ b/src/envoy/http/mixer/filter.h @@ -60,6 +60,9 @@ class Filter : public StreamFilter, FilterTrailersStatus encodeTrailers(HeaderMap&) override { return FilterTrailersStatus::Continue; } + Http::FilterMetadataStatus encodeMetadata(MetadataMap&) override { + return FilterMetadataStatus::Continue; + } void setEncoderFilterCallbacks(StreamEncoderFilterCallbacks&) override {} // This is the callback function when Check is done. @@ -69,7 +72,7 @@ class Filter : public StreamFilter, virtual void log(const HeaderMap* request_headers, const HeaderMap* response_headers, const HeaderMap* response_trailers, - const RequestInfo::RequestInfo& request_info) override; + const StreamInfo::StreamInfo& stream_info) override; private: // Read per-route config. diff --git a/src/envoy/http/mixer/report_data.h b/src/envoy/http/mixer/report_data.h index a843b6b4729..33f4c6b0ed2 100644 --- a/src/envoy/http/mixer/report_data.h +++ b/src/envoy/http/mixer/report_data.h @@ -16,10 +16,11 @@ #pragma once #include "common/common/logger.h" -#include "common/request_info/utility.h" +#include "common/stream_info/utility.h" #include "envoy/http/header_map.h" -#include "envoy/request_info/request_info.h" +#include "envoy/stream_info/stream_info.h" #include "extensions/filters/http/well_known_names.h" +#include "google/protobuf/struct.pb.h" #include "include/istio/control/http/controller.h" #include "src/envoy/utils/utils.h" @@ -27,8 +28,8 @@ namespace Envoy { namespace Http { namespace Mixer { namespace { -const std::string kRbacPermissivePolicyIDField = "shadow_effective_policyID"; -const std::string kRbacPermissiveRespCodeField = "shadow_response_code"; +const std::string kRbacPermissivePolicyIDField = "shadow_effective_policy_id"; +const std::string kRbacPermissiveEngineResultField = "shadow_engine_result"; // Set of headers excluded from response.headers attribute. const std::set ResponseHeaderExclusives = {}; @@ -53,13 +54,13 @@ class ReportData : public ::istio::control::http::ReportData, public Logger::Loggable { const HeaderMap *headers_; const HeaderMap *trailers_; - const RequestInfo::RequestInfo &info_; + const StreamInfo::StreamInfo &info_; uint64_t response_total_size_; uint64_t request_total_size_; public: ReportData(const HeaderMap *headers, const HeaderMap *response_trailers, - const RequestInfo::RequestInfo &info, uint64_t request_total_size) + const StreamInfo::StreamInfo &info, uint64_t request_total_size) : headers_(headers), trailers_(response_trailers), info_(info), @@ -96,7 +97,7 @@ class ReportData : public ::istio::control::http::ReportData, // is rejected by Envoy. Set the response code for such requests as 500. data->response_code = info_.responseCode().value_or(500); - data->response_flags = RequestInfo::ResponseFlagUtils::toShortString(info_); + data->response_flags = StreamInfo::ResponseFlagUtils::toShortString(info_); } bool GetDestinationIpPort(std::string *str_ip, int *port) const override { @@ -109,7 +110,7 @@ class ReportData : public ::istio::control::http::ReportData, bool GetDestinationUID(std::string *uid) const override { if (info_.upstreamHost()) { - return Utils::GetDestinationUID(info_.upstreamHost()->metadata(), uid); + return Utils::GetDestinationUID(*info_.upstreamHost()->metadata(), uid); } return false; } @@ -134,12 +135,12 @@ class ReportData : public ::istio::control::http::ReportData, const auto &data_struct = filter_it->second; const auto resp_code_it = - data_struct.fields().find(kRbacPermissiveRespCodeField); + data_struct.fields().find(kRbacPermissiveEngineResultField); if (resp_code_it != data_struct.fields().end()) { report_info->permissive_resp_code = resp_code_it->second.string_value(); } else { ENVOY_LOG(debug, "No {} field found in filter {} dynamic_metadata", - kRbacPermissiveRespCodeField, + kRbacPermissiveEngineResultField, Extensions::HttpFilters::HttpFilterNames::get().Rbac); } @@ -156,6 +157,12 @@ class ReportData : public ::istio::control::http::ReportData, return !report_info->permissive_resp_code.empty() || !report_info->permissive_policy_id.empty(); } + + // Get attributes generated by http filters + const ::google::protobuf::Map + &GetDynamicFilterState() const override { + return info_.dynamicMetadata().filter_metadata(); + } }; } // namespace Mixer diff --git a/src/envoy/tcp/forward_downstream_sni/BUILD b/src/envoy/tcp/forward_downstream_sni/BUILD new file mode 100644 index 00000000000..f2bcacbc0dc --- /dev/null +++ b/src/envoy/tcp/forward_downstream_sni/BUILD @@ -0,0 +1,56 @@ +# Copyright 2018 Istio Authors. All Rights Reserved. +# +# Licensed 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. +# +################################################################################ +# + +load( + "@envoy//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_cc_test", +) + +envoy_cc_library( + name = "config_lib", + srcs = ["config.cc"], + hdrs = ["config.h"], + repository = "@envoy", + visibility = ["//visibility:public"], + deps = [ + ":forward_downstream_sni_lib", + "@envoy//source/exe:envoy_common_lib", + ], +) +envoy_cc_library( + name = "forward_downstream_sni_lib", + srcs = ["forward_downstream_sni.cc"], + hdrs = ["forward_downstream_sni.h"], + repository = "@envoy", + deps = [ + "@envoy//source/exe:envoy_common_lib", + ], +) + +envoy_cc_test( + name = "forward_downstream_sni_test", + srcs = ["forward_downstream_sni_test.cc"], + repository = "@envoy", + deps = [ + ":forward_downstream_sni_lib", + ":config_lib", + "@envoy//test/mocks/network:network_mocks", + "@envoy//test/mocks/server:server_mocks", + "@envoy//test/mocks/stream_info:stream_info_mocks", + ], +) diff --git a/src/envoy/tcp/forward_downstream_sni/config.cc b/src/envoy/tcp/forward_downstream_sni/config.cc new file mode 100644 index 00000000000..6b62e1a3a35 --- /dev/null +++ b/src/envoy/tcp/forward_downstream_sni/config.cc @@ -0,0 +1,59 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "src/envoy/tcp/forward_downstream_sni/config.h" + +#include "envoy/registry/registry.h" +#include "envoy/server/filter_config.h" + +#include "src/envoy/tcp/forward_downstream_sni/forward_downstream_sni.h" + +namespace Envoy { +namespace Tcp { +namespace ForwardDownstreamSni { + +Network::FilterFactoryCb +ForwardDownstreamSniNetworkFilterConfigFactory::createFilterFactory( + const Json::Object&, Server::Configuration::FactoryContext&) { + // Only used in v1 filters. + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; +} + +Network::FilterFactoryCb +ForwardDownstreamSniNetworkFilterConfigFactory::createFilterFactoryFromProto( + const Protobuf::Message&, Server::Configuration::FactoryContext&) { + return [](Network::FilterManager& filter_manager) -> void { + filter_manager.addReadFilter( + std::make_shared()); + }; +} + +ProtobufTypes::MessagePtr +ForwardDownstreamSniNetworkFilterConfigFactory::createEmptyConfigProto() { + return std::make_unique(); +} + +/** + * Static registration for the forward_original_sni filter. @see + * RegisterFactory. + */ +static Registry::RegisterFactory< + ForwardDownstreamSniNetworkFilterConfigFactory, + Server::Configuration::NamedNetworkFilterConfigFactory> + registered_; + +} // namespace ForwardDownstreamSni +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/tcp/forward_downstream_sni/config.h b/src/envoy/tcp/forward_downstream_sni/config.h new file mode 100644 index 00000000000..06b6419f4ec --- /dev/null +++ b/src/envoy/tcp/forward_downstream_sni/config.h @@ -0,0 +1,43 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "envoy/server/filter_config.h" + +namespace Envoy { +namespace Tcp { +namespace ForwardDownstreamSni { + +/** + * Config registration for the forward_downstream_sni filter. @see + * NamedNetworkFilterConfigFactory. + */ +class ForwardDownstreamSniNetworkFilterConfigFactory + : public Server::Configuration::NamedNetworkFilterConfigFactory { + public: + // NamedNetworkFilterConfigFactory + Network::FilterFactoryCb createFilterFactory( + const Json::Object&, Server::Configuration::FactoryContext&) override; + Network::FilterFactoryCb createFilterFactoryFromProto( + const Protobuf::Message&, + Server::Configuration::FactoryContext&) override; + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + std::string name() override { return "forward_downstream_sni"; } +}; + +} // namespace ForwardDownstreamSni +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/tcp/forward_downstream_sni/forward_downstream_sni.cc b/src/envoy/tcp/forward_downstream_sni/forward_downstream_sni.cc new file mode 100644 index 00000000000..bb10ffc998f --- /dev/null +++ b/src/envoy/tcp/forward_downstream_sni/forward_downstream_sni.cc @@ -0,0 +1,41 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "envoy/network/connection.h" + +#include "common/network/upstream_server_name.h" +#include "src/envoy/tcp/forward_downstream_sni/forward_downstream_sni.h" + +namespace Envoy { +namespace Tcp { +namespace ForwardDownstreamSni { + +using ::Envoy::Network::UpstreamServerName; + +Network::FilterStatus ForwardDownstreamSniFilter::onNewConnection() { + absl::string_view sni = read_callbacks_->connection().requestedServerName(); + + if (!sni.empty()) { + read_callbacks_->connection().streamInfo().filterState().setData( + UpstreamServerName::key(), std::make_unique(sni), + StreamInfo::FilterState::StateType::ReadOnly); + } + + return Network::FilterStatus::Continue; +} + +} // namespace ForwardDownstreamSni +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/tcp/forward_downstream_sni/forward_downstream_sni.h b/src/envoy/tcp/forward_downstream_sni/forward_downstream_sni.h new file mode 100644 index 00000000000..25132713867 --- /dev/null +++ b/src/envoy/tcp/forward_downstream_sni/forward_downstream_sni.h @@ -0,0 +1,46 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "envoy/network/filter.h" + +namespace Envoy { +namespace Tcp { +namespace ForwardDownstreamSni { + +/** + * Implementation of the forward_downstream_sni filter that sets the original + * requested server name from the SNI field in the TLS connection. + */ +class ForwardDownstreamSniFilter : public Network::ReadFilter { + public: + // Network::ReadFilter + Network::FilterStatus onData(Buffer::Instance&, bool) override { + return Network::FilterStatus::Continue; + } + Network::FilterStatus onNewConnection() override; + void initializeReadFilterCallbacks( + Network::ReadFilterCallbacks& callbacks) override { + read_callbacks_ = &callbacks; + } + + private: + Network::ReadFilterCallbacks* read_callbacks_{}; +}; + +} // namespace ForwardDownstreamSni +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/tcp/forward_downstream_sni/forward_downstream_sni_test.cc b/src/envoy/tcp/forward_downstream_sni/forward_downstream_sni_test.cc new file mode 100644 index 00000000000..21c74160a44 --- /dev/null +++ b/src/envoy/tcp/forward_downstream_sni/forward_downstream_sni_test.cc @@ -0,0 +1,93 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "test/mocks/network/mocks.h" +#include "test/mocks/server/mocks.h" +#include "test/mocks/stream_info/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "common/network/upstream_server_name.h" + +#include "src/envoy/tcp/forward_downstream_sni/config.h" +#include "src/envoy/tcp/forward_downstream_sni/forward_downstream_sni.h" + +using testing::_; +using testing::Matcher; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Tcp { +namespace ForwardDownstreamSni { + +using ::Envoy::Network::UpstreamServerName; + +// Test that a ForwardDownstreamSni filter config works. +TEST(ForwardDownstreamSni, ConfigTest) { + NiceMock context; + ForwardDownstreamSniNetworkFilterConfigFactory factory; + + Network::FilterFactoryCb cb = factory.createFilterFactoryFromProto( + *factory.createEmptyConfigProto(), context); + Network::MockConnection connection; + EXPECT_CALL(connection, addReadFilter(_)); + cb(connection); +} + +// Test that forward requested server name is set if SNI is available +TEST(ForwardDownstreamSni, SetUpstreamServerNameOnlyIfSniIsPresent) { + NiceMock filter_callbacks; + + NiceMock stream_info; + ON_CALL(filter_callbacks.connection_, streamInfo()) + .WillByDefault(ReturnRef(stream_info)); + ON_CALL(Const(filter_callbacks.connection_), streamInfo()) + .WillByDefault(ReturnRef(stream_info)); + + ForwardDownstreamSniFilter filter; + filter.initializeReadFilterCallbacks(filter_callbacks); + + // no sni + { + ON_CALL(filter_callbacks.connection_, requestedServerName()) + .WillByDefault(Return(EMPTY_STRING)); + filter.onNewConnection(); + + EXPECT_FALSE(stream_info.filterState().hasData( + UpstreamServerName::key())); + } + + // with sni + { + ON_CALL(filter_callbacks.connection_, requestedServerName()) + .WillByDefault(Return("www.example.com")); + filter.onNewConnection(); + + EXPECT_TRUE(stream_info.filterState().hasData( + UpstreamServerName::key())); + + auto forward_requested_server_name = + stream_info.filterState().getDataReadOnly( + UpstreamServerName::key()); + EXPECT_EQ(forward_requested_server_name.value(), "www.example.com"); + } +} + +} // namespace ForwardDownstreamSni +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/tcp/mixer/BUILD b/src/envoy/tcp/mixer/BUILD index f9cba6d787e..ecf05489ce1 100644 --- a/src/envoy/tcp/mixer/BUILD +++ b/src/envoy/tcp/mixer/BUILD @@ -36,6 +36,7 @@ envoy_cc_library( deps = [ "//src/envoy/utils:utils_lib", "//src/istio/control/tcp:control_lib", + "//src/istio/utils:utils_lib", "@envoy//source/exe:envoy_common_lib", ], ) diff --git a/src/envoy/tcp/mixer/control.cc b/src/envoy/tcp/mixer/control.cc index 543943e8c2c..8b25c2fad1f 100644 --- a/src/envoy/tcp/mixer/control.cc +++ b/src/envoy/tcp/mixer/control.cc @@ -14,32 +14,47 @@ */ #include "src/envoy/tcp/mixer/control.h" +#include "include/istio/utils/local_attributes.h" #include "src/envoy/utils/mixer_control.h" +using ::istio::mixer::v1::Attributes; using ::istio::mixerclient::Statistics; +using ::istio::utils::LocalNode; namespace Envoy { namespace Tcp { namespace Mixer { -Control::Control(const Config& config, Upstream::ClusterManager& cm, - Event::Dispatcher& dispatcher, +Control::Control(ControlDataSharedPtr control_data, + Upstream::ClusterManager& cm, Event::Dispatcher& dispatcher, Runtime::RandomGenerator& random, Stats::Scope& scope, - Utils::MixerFilterStats& stats, const std::string& uuid) - : config_(config), + const LocalInfo::LocalInfo& local_info) + : control_data_(control_data), dispatcher_(dispatcher), check_client_factory_(Utils::GrpcClientFactoryForCluster( - config_.check_cluster(), cm, scope, dispatcher.timeSystem())), + control_data_->config().check_cluster(), cm, scope, + dispatcher.timeSystem())), report_client_factory_(Utils::GrpcClientFactoryForCluster( - config_.report_cluster(), cm, scope, dispatcher.timeSystem())), - stats_obj_(dispatcher, stats, - config_.config_pb().transport().stats_update_interval(), - [this](Statistics* stat) -> bool { return GetStats(stat); }), - uuid_(uuid) { - Utils::SerializeForwardedAttributes(config_.config_pb().transport(), - &serialized_forward_attributes_); - - ::istio::control::tcp::Controller::Options options(config_.config_pb()); + control_data_->config().report_cluster(), cm, scope, + dispatcher.timeSystem())), + stats_obj_(dispatcher, control_data_->stats(), + control_data_->config() + .config_pb() + .transport() + .stats_update_interval(), + [this](Statistics* stat) -> bool { return GetStats(stat); }) { + auto& logger = Logger::Registry::getLog(Logger::Id::config); + LocalNode local_node; + if (!Utils::ExtractNodeInfo(local_info.node(), &local_node)) { + ENVOY_LOG_TO_LOGGER( + logger, warn, + "Missing required node metadata: NODE_UID, NODE_NAMESPACE"); + } + ::istio::utils::SerializeForwardedAttributes(local_node, + &serialized_forward_attributes_); + + ::istio::control::tcp::Controller::Options options( + control_data_->config().config_pb(), local_node); Utils::CreateEnvironment(dispatcher, random, *check_client_factory_, *report_client_factory_, diff --git a/src/envoy/tcp/mixer/control.h b/src/envoy/tcp/mixer/control.h index 203858619ad..42efb785bb0 100644 --- a/src/envoy/tcp/mixer/control.h +++ b/src/envoy/tcp/mixer/control.h @@ -15,11 +15,14 @@ #pragma once +#include "common/common/logger.h" #include "envoy/event/dispatcher.h" +#include "envoy/local_info/local_info.h" #include "envoy/runtime/runtime.h" #include "envoy/thread_local/thread_local.h" #include "envoy/upstream/cluster_manager.h" #include "include/istio/control/tcp/controller.h" +#include "include/istio/utils/local_attributes.h" #include "src/envoy/tcp/mixer/config.h" #include "src/envoy/utils/stats.h" @@ -27,28 +30,46 @@ namespace Envoy { namespace Tcp { namespace Mixer { +class ControlData { + public: + ControlData(std::unique_ptr config, Utils::MixerFilterStats stats, + const std::string& uuid) + : config_(std::move(config)), stats_(stats), uuid_(uuid) {} + + const Config& config() { return *config_; } + Utils::MixerFilterStats& stats() { return stats_; } + const std::string& uuid() { return uuid_; } + + private: + std::unique_ptr config_; + Utils::MixerFilterStats stats_; + // UUID of the Envoy TCP mixer filter. + const std::string uuid_; +}; + +typedef std::shared_ptr ControlDataSharedPtr; + class Control final : public ThreadLocal::ThreadLocalObject { public: // The constructor. - Control(const Config& config, Upstream::ClusterManager& cm, + Control(ControlDataSharedPtr control_data, Upstream::ClusterManager& cm, Event::Dispatcher& dispatcher, Runtime::RandomGenerator& random, - Stats::Scope& scope, Utils::MixerFilterStats& stats, - const std::string& uuid); + Stats::Scope& scope, const LocalInfo::LocalInfo& local_info); ::istio::control::tcp::Controller* controller() { return controller_.get(); } Event::Dispatcher& dispatcher() { return dispatcher_; } - const std::string& uuid() const { return uuid_; } + const std::string& uuid() const { return control_data_->uuid(); } - const Config& config() const { return config_; } + const Config& config() const { return control_data_->config(); } private: // Call controller to get statistics. bool GetStats(::istio::mixerclient::Statistics* stat); - // The mixer config. - const Config& config_; + // The control data. + ControlDataSharedPtr control_data_; // dispatcher. Event::Dispatcher& dispatcher_; @@ -62,8 +83,7 @@ class Control final : public ThreadLocal::ThreadLocalObject { // statistics Utils::MixerStatsObject stats_obj_; - // UUID of the Envoy TCP mixer filter. - const std::string& uuid_; + // The mixer control std::unique_ptr<::istio::control::tcp::Controller> controller_; }; diff --git a/src/envoy/tcp/mixer/control_factory.h b/src/envoy/tcp/mixer/control_factory.h index 763d0e65cfe..ef131cf9f12 100644 --- a/src/envoy/tcp/mixer/control_factory.h +++ b/src/envoy/tcp/mixer/control_factory.h @@ -15,6 +15,7 @@ #pragma once +#include "envoy/local_info/local_info.h" #include "src/envoy/tcp/mixer/control.h" namespace Envoy { @@ -31,17 +32,20 @@ class ControlFactory : public Logger::Loggable { public: ControlFactory(std::unique_ptr config, Server::Configuration::FactoryContext& context) - : config_(std::move(config)), - cm_(context.clusterManager()), - tls_(context.threadLocal().allocateSlot()), - stats_(generateStats(kTcpStatsPrefix, context.scope())), - uuid_(context.random().uuid()) { + : control_data_(std::make_shared( + std::move(config), generateStats(kTcpStatsPrefix, context.scope()), + context.random().uuid())), + tls_(context.threadLocal().allocateSlot()) { Runtime::RandomGenerator& random = context.random(); Stats::Scope& scope = context.scope(); - tls_->set([this, &random, &scope](Event::Dispatcher& dispatcher) + const LocalInfo::LocalInfo& local_info = context.localInfo(); + + tls_->set([control_data = this->control_data_, + &cm = context.clusterManager(), &random, &scope, + &local_info](Event::Dispatcher& dispatcher) -> ThreadLocal::ThreadLocalObjectSharedPtr { return ThreadLocal::ThreadLocalObjectSharedPtr( - new Control(*config_, cm_, dispatcher, random, scope, stats_, uuid_)); + new Control(control_data, cm, dispatcher, random, scope, local_info)); }); } @@ -55,16 +59,10 @@ class ControlFactory : public Logger::Loggable { return {ALL_MIXER_FILTER_STATS(POOL_COUNTER_PREFIX(scope, name))}; } - // The config object - std::unique_ptr config_; - // The cluster manager - Upstream::ClusterManager& cm_; + // The control data object + ControlDataSharedPtr control_data_; // the thread local slots ThreadLocal::SlotPtr tls_; - // The statistics struct. - Utils::MixerFilterStats stats_; - // UUID of the Envoy TCP mixer filter. - const std::string uuid_; }; } // namespace Mixer diff --git a/src/envoy/tcp/mixer/filter.cc b/src/envoy/tcp/mixer/filter.cc index 3081c31f2d5..6da8d92de4c 100644 --- a/src/envoy/tcp/mixer/filter.cc +++ b/src/envoy/tcp/mixer/filter.cc @@ -15,6 +15,7 @@ #include "src/envoy/tcp/mixer/filter.h" #include "common/common/enum_to_int.h" +#include "extensions/filters/network/well_known_names.h" #include "src/envoy/utils/utils.h" using ::google::protobuf::util::Status; @@ -24,7 +25,7 @@ namespace Envoy { namespace Tcp { namespace Mixer { -Filter::Filter(Control& control) : control_(control) { +Filter::Filter(Control &control) : control_(control) { ENVOY_LOG(debug, "Called tcp filter: {}", __func__); } @@ -34,7 +35,7 @@ Filter::~Filter() { } void Filter::initializeReadFilterCallbacks( - Network::ReadFilterCallbacks& callbacks) { + Network::ReadFilterCallbacks &callbacks) { ENVOY_LOG(debug, "Called tcp filter: {}", __func__); filter_callbacks_ = &callbacks; filter_callbacks_->connection().addConnectionCallbacks(*this); @@ -60,14 +61,38 @@ void Filter::callCheck() { state_ = State::Calling; filter_callbacks_->connection().readDisable(true); calling_check_ = true; - cancel_check_ = handler_->Check(this, [this](const CheckResponseInfo& info) { - completeCheck(info.response_status); - }); + cancel_check_ = handler_->Check( + this, [this](const CheckResponseInfo &info) { completeCheck(info); }); calling_check_ = false; } +// TODO(venilnoronha): rewrite this to deep-clone dynamic metadata for all +// filters. +void Filter::cacheFilterMetadata( + const ::google::protobuf::Map + &filter_metadata) { + for (auto &filter_pair : filter_metadata) { + if (filter_pair.first == + Extensions::NetworkFilters::NetworkFilterNames::get().MongoProxy) { + if (cached_filter_metadata_.count(filter_pair.first) == 0) { + ProtobufWkt::Struct dynamic_metadata; + cached_filter_metadata_[filter_pair.first] = dynamic_metadata; + } + + auto &cached_fields = + *cached_filter_metadata_[filter_pair.first].mutable_fields(); + for (const auto &message_pair : filter_pair.second.fields()) { + cached_fields[message_pair.first].mutable_list_value()->CopyFrom( + message_pair.second.list_value()); + } + } + } +} + +void Filter::clearCachedFilterMetadata() { cached_filter_metadata_.clear(); } + // Network::ReadFilter -Network::FilterStatus Filter::onData(Buffer::Instance& data, bool) { +Network::FilterStatus Filter::onData(Buffer::Instance &data, bool) { if (state_ == State::NotStarted) { // By waiting to invoke the callCheck() at onData(), the call to Mixer // will have sufficient SSL information to fill the check Request. @@ -78,12 +103,23 @@ Network::FilterStatus Filter::onData(Buffer::Instance& data, bool) { filter_callbacks_->connection(), data.length()); received_bytes_ += data.length(); + // Envoy filters like the mongo_proxy filter clear previously set dynamic + // metadata on each onData call. Since the Mixer filter sends metadata based + // on a timer event, it's possible that the previously set metadata is cleared + // off by the time the event is fired. Therefore, we append metadata from each + // onData call to a local cache and send it all at once when the timer event + // occurs. The local cache is cleared after reporting it on the timer event. + cacheFilterMetadata(filter_callbacks_->connection() + .streamInfo() + .dynamicMetadata() + .filter_metadata()); + return state_ == State::Calling ? Network::FilterStatus::StopIteration : Network::FilterStatus::Continue; } // Network::WriteFilter -Network::FilterStatus Filter::onWrite(Buffer::Instance& data, bool) { +Network::FilterStatus Filter::onWrite(Buffer::Instance &data, bool) { ENVOY_CONN_LOG(debug, "Called tcp filter onWrite bytes: {}", filter_callbacks_->connection(), data.length()); send_bytes_ += data.length(); @@ -101,13 +137,18 @@ Network::FilterStatus Filter::onNewConnection() { return Network::FilterStatus::Continue; } -void Filter::completeCheck(const Status& status) { +void Filter::completeCheck(const CheckResponseInfo &info) { + const auto &status = info.response_status; ENVOY_LOG(debug, "Called tcp filter completeCheck: {}", status.ToString()); cancel_check_ = nullptr; if (state_ == State::Closed) { return; } state_ = State::Completed; + + Utils::CheckResponseInfoToStreamInfo( + info, filter_callbacks_->connection().streamInfo()); + filter_callbacks_->connection().readDisable(false); if (!status.ok()) { @@ -146,11 +187,12 @@ void Filter::onEvent(Network::ConnectionEvent event) { } } -bool Filter::GetSourceIpPort(std::string* str_ip, int* port) const { +bool Filter::GetSourceIpPort(std::string *str_ip, int *port) const { return Utils::GetIpPort(filter_callbacks_->connection().remoteAddress()->ip(), str_ip, port); } -bool Filter::GetPrincipal(bool peer, std::string* user) const { + +bool Filter::GetPrincipal(bool peer, std::string *user) const { return Utils::GetPrincipal(&filter_callbacks_->connection(), peer, user); } @@ -158,11 +200,11 @@ bool Filter::IsMutualTLS() const { return Utils::IsMutualTLS(&filter_callbacks_->connection()); } -bool Filter::GetRequestedServerName(std::string* name) const { +bool Filter::GetRequestedServerName(std::string *name) const { return Utils::GetRequestedServerName(&filter_callbacks_->connection(), name); } -bool Filter::GetDestinationIpPort(std::string* str_ip, int* port) const { +bool Filter::GetDestinationIpPort(std::string *str_ip, int *port) const { if (filter_callbacks_->upstreamHost() && filter_callbacks_->upstreamHost()->address()) { return Utils::GetIpPort(filter_callbacks_->upstreamHost()->address()->ip(), @@ -170,15 +212,22 @@ bool Filter::GetDestinationIpPort(std::string* str_ip, int* port) const { } return false; } -bool Filter::GetDestinationUID(std::string* uid) const { + +bool Filter::GetDestinationUID(std::string *uid) const { if (filter_callbacks_->upstreamHost()) { return Utils::GetDestinationUID( - filter_callbacks_->upstreamHost()->metadata(), uid); + *filter_callbacks_->upstreamHost()->metadata(), uid); } return false; } + +const ::google::protobuf::Map + &Filter::GetDynamicFilterState() const { + return cached_filter_metadata_; +} + void Filter::GetReportInfo( - ::istio::control::tcp::ReportData::ReportInfo* data) const { + ::istio::control::tcp::ReportData::ReportInfo *data) const { data->received_bytes = received_bytes_; data->send_bytes = send_bytes_; data->duration = std::chrono::duration_cast( @@ -195,9 +244,10 @@ std::string Filter::GetConnectionId() const { void Filter::OnReportTimer() { handler_->Report(this, ConnectionEvent::CONTINUE); + clearCachedFilterMetadata(); report_timer_->enableTimer(control_.config().report_interval_ms()); } } // namespace Mixer } // namespace Tcp -} // namespace Envoy +} // namespace Envoy \ No newline at end of file diff --git a/src/envoy/tcp/mixer/filter.h b/src/envoy/tcp/mixer/filter.h index a06daa9f852..b9bfb97369c 100644 --- a/src/envoy/tcp/mixer/filter.h +++ b/src/envoy/tcp/mixer/filter.h @@ -18,6 +18,8 @@ #include "common/common/logger.h" #include "envoy/network/connection.h" #include "envoy/network/filter.h" +#include "google/protobuf/struct.pb.h" +#include "include/istio/mixerclient/check_response.h" #include "src/envoy/tcp/mixer/control.h" namespace Envoy { @@ -30,17 +32,17 @@ class Filter : public Network::Filter, public ::istio::control::tcp::ReportData, public Logger::Loggable { public: - Filter(Control& control); + Filter(Control &control); ~Filter(); void initializeReadFilterCallbacks( - Network::ReadFilterCallbacks& callbacks) override; + Network::ReadFilterCallbacks &callbacks) override; // Network::ReadFilter - Network::FilterStatus onData(Buffer::Instance& data, bool) override; + Network::FilterStatus onData(Buffer::Instance &data, bool) override; // Network::WriteFilter - Network::FilterStatus onWrite(Buffer::Instance& data, bool) override; + Network::FilterStatus onWrite(Buffer::Instance &data, bool) override; Network::FilterStatus onNewConnection() override; // Network::ConnectionCallbacks @@ -50,18 +52,25 @@ class Filter : public Network::Filter, void onBelowWriteBufferLowWatermark() override {} // CheckData virtual functions. - bool GetSourceIpPort(std::string* str_ip, int* port) const override; - bool GetPrincipal(bool peer, std::string* user) const override; + bool GetSourceIpPort(std::string *str_ip, int *port) const override; + bool GetPrincipal(bool peer, std::string *user) const override; bool IsMutualTLS() const override; - bool GetRequestedServerName(std::string* name) const override; + bool GetRequestedServerName(std::string *name) const override; // ReportData virtual functions. - bool GetDestinationIpPort(std::string* str_ip, int* port) const override; - bool GetDestinationUID(std::string* uid) const override; + bool GetDestinationIpPort(std::string *str_ip, int *port) const override; + bool GetDestinationUID(std::string *uid) const override; + const ::google::protobuf::Map + &GetDynamicFilterState() const override; void GetReportInfo( - ::istio::control::tcp::ReportData::ReportInfo* data) const override; + ::istio::control::tcp::ReportData::ReportInfo *data) const override; std::string GetConnectionId() const override; + void cacheFilterMetadata( + const ::google::protobuf::Map + &filter_metadata); + void clearCachedFilterMetadata(); + private: enum class State { NotStarted, Calling, Completed, Closed }; // This function is invoked when timer event fires. @@ -72,7 +81,7 @@ class Filter : public Network::Filter, void callCheck(); // Called when Check is done. - void completeCheck(const ::google::protobuf::util::Status& status); + void completeCheck(const ::istio::mixerclient::CheckResponseInfo &info); // Cancel the pending Check call. void cancelCheck(); @@ -80,11 +89,11 @@ class Filter : public Network::Filter, // the cancel check istio::mixerclient::CancelFunc cancel_check_; // the control object. - Control& control_; + Control &control_; // pre-request handler std::unique_ptr<::istio::control::tcp::RequestHandler> handler_; // filter callback - Network::ReadFilterCallbacks* filter_callbacks_{}; + Network::ReadFilterCallbacks *filter_callbacks_{}; // state State state_{State::NotStarted}; // calling_check @@ -93,6 +102,9 @@ class Filter : public Network::Filter, uint64_t received_bytes_{}; // send bytes uint64_t send_bytes_{}; + // cached filter metadata + ::google::protobuf::Map + cached_filter_metadata_{}; // Timer that periodically sends reports. Event::TimerPtr report_timer_; diff --git a/src/envoy/tcp/sni_verifier/BUILD b/src/envoy/tcp/sni_verifier/BUILD new file mode 100644 index 00000000000..c1708ba9d7e --- /dev/null +++ b/src/envoy/tcp/sni_verifier/BUILD @@ -0,0 +1,58 @@ +# Copyright 2018 Istio Authors. All Rights Reserved. +# +# Licensed 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. +# +################################################################################ +# + +load( + "@envoy//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_cc_test", +) + +envoy_cc_library( + name = "config_lib", + srcs = ["config.cc"], + hdrs = ["config.h"], + repository = "@envoy", + visibility = ["//visibility:public"], + deps = [ + ":sni_verifier_lib", + "@envoy//source/exe:envoy_common_lib", + ], +) + +envoy_cc_library( + name = "sni_verifier_lib", + srcs = [ "sni_verifier.cc"], + hdrs = [ "sni_verifier.h"], + repository = "@envoy", + external_deps = ["ssl"], + deps = [ + "@envoy//source/exe:envoy_common_lib", + ], +) + +envoy_cc_test( + name = "sni_verifier_test", + srcs = ["sni_verifier_test.cc"], + repository = "@envoy", + deps = [ + ":sni_verifier_lib", + ":config_lib", + "@envoy//test/mocks/network:network_mocks", + "@envoy//test/mocks/server:server_mocks", + "@envoy//test/extensions/filters/listener/tls_inspector:tls_utility_lib", + ], +) diff --git a/src/envoy/tcp/sni_verifier/config.cc b/src/envoy/tcp/sni_verifier/config.cc new file mode 100644 index 00000000000..8bcaa43133a --- /dev/null +++ b/src/envoy/tcp/sni_verifier/config.cc @@ -0,0 +1,57 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "src/envoy/tcp/sni_verifier/config.h" +#include "envoy/registry/registry.h" +#include "src/envoy/tcp/sni_verifier/sni_verifier.h" + +namespace Envoy { +namespace Tcp { +namespace SniVerifier { + +Network::FilterFactoryCb SniVerifierConfigFactory::createFilterFactory( + const Json::Object&, Server::Configuration::FactoryContext& context) { + return createFilterFactoryFromContext(context); +} + +Network::FilterFactoryCb SniVerifierConfigFactory::createFilterFactoryFromProto( + const Protobuf::Message&, Server::Configuration::FactoryContext& context) { + return createFilterFactoryFromContext(context); +} + +ProtobufTypes::MessagePtr SniVerifierConfigFactory::createEmptyConfigProto() { + return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Empty()}; +} + +Network::FilterFactoryCb +SniVerifierConfigFactory::createFilterFactoryFromContext( + Server::Configuration::FactoryContext& context) { + ConfigSharedPtr filter_config(new Config(context.scope())); + return [filter_config](Network::FilterManager& filter_manager) -> void { + filter_manager.addReadFilter(std::make_shared(filter_config)); + }; +} + +/** + * Static registration for the echo filter. @see RegisterFactory. + */ +static Registry::RegisterFactory< + SniVerifierConfigFactory, + Server::Configuration::NamedNetworkFilterConfigFactory> + registered_; + +} // namespace SniVerifier +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/tcp/sni_verifier/config.h b/src/envoy/tcp/sni_verifier/config.h new file mode 100644 index 00000000000..5ef1baf5aa9 --- /dev/null +++ b/src/envoy/tcp/sni_verifier/config.h @@ -0,0 +1,49 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "envoy/server/filter_config.h" + +namespace Envoy { +namespace Tcp { +namespace SniVerifier { + +/** + * Config registration for the SNI verifier filter. @see + * NamedNetworkFilterConfigFactory. + */ +class SniVerifierConfigFactory + : public Server::Configuration::NamedNetworkFilterConfigFactory { + public: + // NamedNetworkFilterConfigFactory + Network::FilterFactoryCb createFilterFactory( + const Json::Object&, + Server::Configuration::FactoryContext& context) override; + + Network::FilterFactoryCb createFilterFactoryFromProto( + const Protobuf::Message&, + Server::Configuration::FactoryContext& context) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + + std::string name() override { return "sni_verifier"; } + + private: + Network::FilterFactoryCb createFilterFactoryFromContext( + Server::Configuration::FactoryContext& context); +}; + +} // namespace SniVerifier +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/tcp/sni_verifier/sni_verifier.cc b/src/envoy/tcp/sni_verifier/sni_verifier.cc new file mode 100644 index 00000000000..5210b1648c0 --- /dev/null +++ b/src/envoy/tcp/sni_verifier/sni_verifier.cc @@ -0,0 +1,187 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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. + */ + +// the implementation (extracting the SNI) is based on the TLS inspector +// listener filter of Envoy + +#include "src/envoy/tcp/sni_verifier/sni_verifier.h" + +#include "envoy/buffer/buffer.h" +#include "envoy/common/exception.h" +#include "envoy/network/connection.h" +#include "envoy/stats/scope.h" + +#include "common/common/assert.h" + +#include "openssl/bytestring.h" +#include "openssl/err.h" +#include "openssl/ssl.h" + +namespace Envoy { +namespace Tcp { +namespace SniVerifier { + +Config::Config(Stats::Scope& scope, size_t max_client_hello_size) + : stats_{SNI_VERIFIER_STATS(POOL_COUNTER_PREFIX(scope, "sni_verifier."))}, + ssl_ctx_(SSL_CTX_new(TLS_with_buffers_method())), + max_client_hello_size_(max_client_hello_size) { + if (max_client_hello_size_ > TLS_MAX_CLIENT_HELLO) { + throw EnvoyException(fmt::format( + "max_client_hello_size of {} is greater than maximum of {}.", + max_client_hello_size_, size_t(TLS_MAX_CLIENT_HELLO))); + } + + SSL_CTX_set_options(ssl_ctx_.get(), SSL_OP_NO_TICKET); + SSL_CTX_set_session_cache_mode(ssl_ctx_.get(), SSL_SESS_CACHE_OFF); + SSL_CTX_set_tlsext_servername_callback( + ssl_ctx_.get(), [](SSL* ssl, int* out_alert, void*) -> int { + Filter* filter = static_cast(SSL_get_app_data(ssl)); + + if (filter != nullptr) { + filter->onServername( + SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name)); + } + + // Return an error to stop the handshake; we have what we wanted + // already. + *out_alert = SSL_AD_USER_CANCELLED; + return SSL_TLSEXT_ERR_ALERT_FATAL; + }); +} + +bssl::UniquePtr Config::newSsl() { + return bssl::UniquePtr{SSL_new(ssl_ctx_.get())}; +} + +Filter::Filter(const ConfigSharedPtr config) + : config_(config), + ssl_(config_->newSsl()), + buf_(std::make_unique(config_->maxClientHelloSize())) { + SSL_set_accept_state(ssl_.get()); +} + +Network::FilterStatus Filter::onData(Buffer::Instance& data, bool) { + ENVOY_CONN_LOG(trace, "SniVerifier: got {} bytes", + read_callbacks_->connection(), data.length()); + if (done_) { + return is_match_ ? Network::FilterStatus::Continue + : Network::FilterStatus::StopIteration; + } + + size_t left_space_in_buf = config_->maxClientHelloSize() - read_; + size_t data_to_read = + (data.length() < left_space_in_buf) ? data.length() : left_space_in_buf; + data.copyOut(0, data_to_read, buf_.get() + read_); + + auto start_handshake_data = + restart_handshake_ ? buf_.get() : buf_.get() + read_; + auto handshake_size = + restart_handshake_ ? read_ + data_to_read : data_to_read; + + read_ += data_to_read; + parseClientHello(start_handshake_data, handshake_size); + + return is_match_ ? Network::FilterStatus::Continue + : Network::FilterStatus::StopIteration; +} + +void Filter::onServername(absl::string_view servername) { + if (!servername.empty()) { + config_->stats().inner_sni_found_.inc(); + absl::string_view outer_sni = + read_callbacks_->connection().requestedServerName(); + + is_match_ = (servername == outer_sni); + if (!is_match_) { + config_->stats().snis_do_not_match_.inc(); + } + ENVOY_LOG( + debug, + "sni_verifier:onServerName(), inner SNI: {}, outer SNI: {}, match: {}", + servername, outer_sni, is_match_); + } else { + config_->stats().inner_sni_not_found_.inc(); + } + clienthello_success_ = true; +} + +void Filter::done(bool success) { + ENVOY_LOG(trace, "sni_verifier: done: {}", success); + done_ = true; + if (success) { + read_callbacks_->continueReading(); + } +} + +void Filter::parseClientHello(const void* data, size_t len) { + // Ownership is passed to ssl_ in SSL_set_bio() + bssl::UniquePtr bio(BIO_new_mem_buf(data, len)); + + // Make the mem-BIO return that there is more data + // available beyond it's end + BIO_set_mem_eof_return(bio.get(), -1); + + SSL_set_bio(ssl_.get(), bio.get(), bio.get()); + bio.release(); + + restart_handshake_ = false; + SSL_set_app_data(ssl_.get(), this); + int ret = SSL_do_handshake(ssl_.get()); + + // reset the app data + SSL_set_app_data(ssl_.get(), nullptr); + + // This should never succeed because an error is always returned from the SNI + // callback. + ASSERT(ret <= 0); + switch (SSL_get_error(ssl_.get(), ret)) { + case SSL_ERROR_WANT_READ: + if (read_ == config_->maxClientHelloSize()) { + // We've hit the specified size limit. This is an unreasonably large + // ClientHello; indicate failure. + config_->stats().client_hello_too_large_.inc(); + done(false); + } + break; // do nothing until more data arrives + case SSL_ERROR_SSL: + if (clienthello_success_) { + config_->stats().tls_found_.inc(); + done(true); + } else { + if (read_ >= config_->maxClientHelloSize()) { + // give up on client hello parsing at this point + config_->stats().tls_not_found_.inc(); + done(false); + } else { // clean the SSL object to allow another handshake once we get + // more data + SSL_shutdown(ssl_.get()); + SSL_clear(ssl_.get()); + // once we get more data - restart the hanshake with the data from the + // beginning + restart_handshake_ = true; + } + } + break; + default: + done(false); + break; + } + + ERR_clear_error(); +} + +} // namespace SniVerifier +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/tcp/sni_verifier/sni_verifier.h b/src/envoy/tcp/sni_verifier/sni_verifier.h new file mode 100644 index 00000000000..f62374c68e8 --- /dev/null +++ b/src/envoy/tcp/sni_verifier/sni_verifier.h @@ -0,0 +1,109 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "envoy/network/filter.h" +#include "envoy/stats/scope.h" + +#include "common/common/logger.h" + +#include "openssl/bytestring.h" +#include "openssl/ssl.h" + +namespace Envoy { +namespace Tcp { +namespace SniVerifier { + +/** + * All stats for the SNI verifier. @see stats_macros.h + */ +#define SNI_VERIFIER_STATS(COUNTER) \ + COUNTER(client_hello_too_large) \ + COUNTER(tls_found) \ + COUNTER(tls_not_found) \ + COUNTER(inner_sni_found) \ + COUNTER(inner_sni_not_found) \ + COUNTER(snis_do_not_match) + +/** + * Definition of all stats for the SNI verifier. @see stats_macros.h + */ +struct SniVerifierStats { + SNI_VERIFIER_STATS(GENERATE_COUNTER_STRUCT) +}; + +/** + * Global configuration for SNI verifier. + */ +class Config { + public: + Config(Stats::Scope& scope, + size_t max_client_hello_size = TLS_MAX_CLIENT_HELLO); + + const SniVerifierStats& stats() const { return stats_; } + bssl::UniquePtr newSsl(); + size_t maxClientHelloSize() const { return max_client_hello_size_; } + + static constexpr size_t TLS_MAX_CLIENT_HELLO = 64 * 1024; + + private: + SniVerifierStats stats_; + bssl::UniquePtr ssl_ctx_; + const size_t max_client_hello_size_; +}; + +typedef std::shared_ptr ConfigSharedPtr; + +class Filter : public Network::ReadFilter, + Logger::Loggable { + public: + Filter(const ConfigSharedPtr config); + + // Network::ReadFilter + Network::FilterStatus onData(Buffer::Instance& data, + bool end_stream) override; + Network::FilterStatus onNewConnection() override { + return Network::FilterStatus::Continue; + } + void initializeReadFilterCallbacks( + Network::ReadFilterCallbacks& callbacks) override { + read_callbacks_ = &callbacks; + } + + private: + void parseClientHello(const void* data, size_t len); + void done(bool success); + void onServername(absl::string_view name); + + ConfigSharedPtr config_; + Network::ReadFilterCallbacks* read_callbacks_{}; + + bssl::UniquePtr ssl_; + uint64_t read_{0}; + bool clienthello_success_{false}; + bool done_{false}; + bool is_match_{false}; + bool restart_handshake_{false}; + + std::unique_ptr buf_; + + // Allows callbacks on the SSL_CTX to set fields in this class. + friend class Config; +}; + +} // namespace SniVerifier +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/tcp/sni_verifier/sni_verifier_test.cc b/src/envoy/tcp/sni_verifier/sni_verifier_test.cc new file mode 100644 index 00000000000..e452ad09c01 --- /dev/null +++ b/src/envoy/tcp/sni_verifier/sni_verifier_test.cc @@ -0,0 +1,236 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 +#include + +#include "src/envoy/tcp/sni_verifier/config.h" +#include "src/envoy/tcp/sni_verifier/sni_verifier.h" + +#include "common/buffer/buffer_impl.h" + +#include "test/extensions/filters/listener/tls_inspector/tls_utility.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/server/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::NiceMock; +using testing::Return; + +namespace Envoy { +namespace Tcp { +namespace SniVerifier { + +// Test that a SniVerifier filter config works. +TEST(SniVerifierTest, ConfigTest) { + NiceMock context; + SniVerifierConfigFactory factory; + + Network::FilterFactoryCb cb = factory.createFilterFactoryFromProto( + *factory.createEmptyConfigProto(), context); + Network::MockConnection connection; + EXPECT_CALL(connection, addReadFilter(_)); + cb(connection); +} + +TEST(SniVerifierTest, MaxClientHelloSize) { + Stats::IsolatedStoreImpl store; + EXPECT_THROW_WITH_MESSAGE( + Config(store, Config::TLS_MAX_CLIENT_HELLO + 1), EnvoyException, + "max_client_hello_size of 65537 is greater than maximum of 65536."); +} + +class SniVerifierFilterTest : public testing::Test { + protected: + static constexpr size_t TLS_MAX_CLIENT_HELLO = 200; + + void SetUp() override { + store_ = std::make_unique(); + cfg_ = std::make_shared(*store_, TLS_MAX_CLIENT_HELLO); + filter_ = std::make_unique(cfg_); + } + + void TearDown() override { + filter_ = nullptr; + cfg_ = nullptr; + store_ = nullptr; + } + + void runTestForClientHello(std::string outer_sni, std::string inner_sni, + Network::FilterStatus expected_status, + size_t data_installment_size = UINT_MAX) { + auto client_hello = Tls::Test::generateClientHello(inner_sni, ""); + runTestForData(outer_sni, client_hello, expected_status, + data_installment_size); + } + + void runTestForData(std::string outer_sni, std::vector& data, + Network::FilterStatus expected_status, + size_t data_installment_size = UINT_MAX) { + NiceMock filter_callbacks; + + ON_CALL(filter_callbacks.connection_, requestedServerName()) + .WillByDefault(Return(outer_sni)); + + filter_->initializeReadFilterCallbacks(filter_callbacks); + filter_->onNewConnection(); + + size_t sent_data = 0; + size_t remaining_data_to_send = data.size(); + auto status = Network::FilterStatus::StopIteration; + + while (remaining_data_to_send > 0) { + size_t data_to_send_size = data_installment_size < remaining_data_to_send + ? data_installment_size + : remaining_data_to_send; + Buffer::OwnedImpl buf; + buf.add(data.data() + sent_data, data_to_send_size); + status = filter_->onData(buf, true); + sent_data += data_to_send_size; + remaining_data_to_send -= data_to_send_size; + if (remaining_data_to_send > 0) { + // expect that until the whole hello message is parsed, the status is + // stop iteration + EXPECT_EQ(Network::FilterStatus::StopIteration, status); + } + } + + EXPECT_EQ(expected_status, status); + } + + ConfigSharedPtr cfg_; + + private: + std::unique_ptr filter_; + std::unique_ptr store_; +}; + +constexpr size_t SniVerifierFilterTest::TLS_MAX_CLIENT_HELLO; // definition + +TEST_F(SniVerifierFilterTest, SnisMatch) { + runTestForClientHello("example.com", "example.com", + Network::FilterStatus::Continue); + EXPECT_EQ(0, cfg_->stats().client_hello_too_large_.value()); + EXPECT_EQ(1, cfg_->stats().tls_found_.value()); + EXPECT_EQ(0, cfg_->stats().tls_not_found_.value()); + EXPECT_EQ(1, cfg_->stats().inner_sni_found_.value()); + EXPECT_EQ(0, cfg_->stats().inner_sni_not_found_.value()); + EXPECT_EQ(0, cfg_->stats().snis_do_not_match_.value()); +} + +TEST_F(SniVerifierFilterTest, SnisDoNotMatch) { + runTestForClientHello("example.com", "istio.io", + Network::FilterStatus::StopIteration); + EXPECT_EQ(0, cfg_->stats().client_hello_too_large_.value()); + EXPECT_EQ(1, cfg_->stats().tls_found_.value()); + EXPECT_EQ(0, cfg_->stats().tls_not_found_.value()); + EXPECT_EQ(1, cfg_->stats().inner_sni_found_.value()); + EXPECT_EQ(0, cfg_->stats().inner_sni_not_found_.value()); + EXPECT_EQ(1, cfg_->stats().snis_do_not_match_.value()); +} + +TEST_F(SniVerifierFilterTest, EmptyOuterSni) { + runTestForClientHello("", "istio.io", Network::FilterStatus::StopIteration); + EXPECT_EQ(0, cfg_->stats().client_hello_too_large_.value()); + EXPECT_EQ(1, cfg_->stats().tls_found_.value()); + EXPECT_EQ(0, cfg_->stats().tls_not_found_.value()); + EXPECT_EQ(1, cfg_->stats().inner_sni_found_.value()); + EXPECT_EQ(0, cfg_->stats().inner_sni_not_found_.value()); + EXPECT_EQ(1, cfg_->stats().snis_do_not_match_.value()); +} + +TEST_F(SniVerifierFilterTest, EmptyInnerSni) { + runTestForClientHello("example.com", "", + Network::FilterStatus::StopIteration); + EXPECT_EQ(0, cfg_->stats().client_hello_too_large_.value()); + EXPECT_EQ(1, cfg_->stats().tls_found_.value()); + EXPECT_EQ(0, cfg_->stats().tls_not_found_.value()); + EXPECT_EQ(0, cfg_->stats().inner_sni_found_.value()); + EXPECT_EQ(1, cfg_->stats().inner_sni_not_found_.value()); + EXPECT_EQ(0, cfg_->stats().snis_do_not_match_.value()); +} + +TEST_F(SniVerifierFilterTest, BothSnisEmpty) { + runTestForClientHello("", "", Network::FilterStatus::StopIteration); + EXPECT_EQ(0, cfg_->stats().client_hello_too_large_.value()); + EXPECT_EQ(1, cfg_->stats().tls_found_.value()); + EXPECT_EQ(0, cfg_->stats().tls_not_found_.value()); + EXPECT_EQ(0, cfg_->stats().inner_sni_found_.value()); + EXPECT_EQ(1, cfg_->stats().inner_sni_not_found_.value()); + EXPECT_EQ(0, cfg_->stats().snis_do_not_match_.value()); +} + +TEST_F(SniVerifierFilterTest, SniTooLarge) { + runTestForClientHello("example.com", std::string(TLS_MAX_CLIENT_HELLO, 'a'), + Network::FilterStatus::StopIteration); + EXPECT_EQ(1, cfg_->stats().client_hello_too_large_.value()); + EXPECT_EQ(0, cfg_->stats().tls_found_.value()); + EXPECT_EQ(0, cfg_->stats().tls_not_found_.value()); + EXPECT_EQ(0, cfg_->stats().inner_sni_found_.value()); + EXPECT_EQ(0, cfg_->stats().inner_sni_not_found_.value()); + EXPECT_EQ(0, cfg_->stats().snis_do_not_match_.value()); +} + +TEST_F(SniVerifierFilterTest, SnisMatchSendDataInChunksOfTen) { + runTestForClientHello("example.com", "example.com", + Network::FilterStatus::Continue, 10); + EXPECT_EQ(0, cfg_->stats().client_hello_too_large_.value()); + EXPECT_EQ(1, cfg_->stats().tls_found_.value()); + EXPECT_EQ(0, cfg_->stats().tls_not_found_.value()); + EXPECT_EQ(1, cfg_->stats().inner_sni_found_.value()); + EXPECT_EQ(0, cfg_->stats().inner_sni_not_found_.value()); + EXPECT_EQ(0, cfg_->stats().snis_do_not_match_.value()); +} + +TEST_F(SniVerifierFilterTest, SnisMatchSendDataInChunksOfFifty) { + runTestForClientHello("example.com", "example.com", + Network::FilterStatus::Continue, 50); + EXPECT_EQ(0, cfg_->stats().client_hello_too_large_.value()); + EXPECT_EQ(1, cfg_->stats().tls_found_.value()); + EXPECT_EQ(0, cfg_->stats().tls_not_found_.value()); + EXPECT_EQ(1, cfg_->stats().inner_sni_found_.value()); + EXPECT_EQ(0, cfg_->stats().inner_sni_not_found_.value()); + EXPECT_EQ(0, cfg_->stats().snis_do_not_match_.value()); +} + +TEST_F(SniVerifierFilterTest, SnisMatchSendDataInChunksOfHundred) { + runTestForClientHello("example.com", "example.com", + Network::FilterStatus::Continue, 100); + EXPECT_EQ(0, cfg_->stats().client_hello_too_large_.value()); + EXPECT_EQ(1, cfg_->stats().tls_found_.value()); + EXPECT_EQ(0, cfg_->stats().tls_not_found_.value()); + EXPECT_EQ(1, cfg_->stats().inner_sni_found_.value()); + EXPECT_EQ(0, cfg_->stats().inner_sni_not_found_.value()); + EXPECT_EQ(0, cfg_->stats().snis_do_not_match_.value()); +} + +TEST_F(SniVerifierFilterTest, NonTLS) { + std::vector nonTLSData(TLS_MAX_CLIENT_HELLO, 7); + runTestForData("example.com", nonTLSData, + Network::FilterStatus::StopIteration); + EXPECT_EQ(0, cfg_->stats().client_hello_too_large_.value()); + EXPECT_EQ(0, cfg_->stats().tls_found_.value()); + EXPECT_EQ(1, cfg_->stats().tls_not_found_.value()); + EXPECT_EQ(0, cfg_->stats().inner_sni_found_.value()); + EXPECT_EQ(0, cfg_->stats().inner_sni_not_found_.value()); + EXPECT_EQ(0, cfg_->stats().snis_do_not_match_.value()); +} + +} // namespace SniVerifier +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/tcp/tcp_cluster_rewrite/BUILD b/src/envoy/tcp/tcp_cluster_rewrite/BUILD new file mode 100644 index 00000000000..a163d3da08f --- /dev/null +++ b/src/envoy/tcp/tcp_cluster_rewrite/BUILD @@ -0,0 +1,72 @@ +# Copyright 2018 Istio Authors. All Rights Reserved. +# +# Licensed 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. +# +################################################################################ +# + +package(default_visibility = ["//visibility:public"]) + +load( + "@envoy//bazel:envoy_build_system.bzl", + "envoy_cc_binary", + "envoy_cc_library", + "envoy_cc_test", +) + +envoy_cc_library( + name = "tcp_cluster_rewrite_lib", + srcs = ["tcp_cluster_rewrite.cc"], + hdrs = ["tcp_cluster_rewrite.h"], + repository = "@envoy", + deps = [ + "//external:tcp_cluster_rewrite_config_cc_proto", + "@envoy//source/exe:envoy_common_lib", + ], +) + +envoy_cc_library( + name = "config_lib", + srcs = ["config.cc"], + hdrs = ["config.h"], + repository = "@envoy", + deps = [ + ":tcp_cluster_rewrite_lib", + "//src/envoy/utils:utils_lib", + "//external:tcp_cluster_rewrite_config_cc_proto", + "@envoy//source/exe:envoy_common_lib", + ], +) + +envoy_cc_test( + name = "tcp_cluster_rewrite_test", + srcs = ["tcp_cluster_rewrite_test.cc"], + repository = "@envoy", + deps = [ + ":tcp_cluster_rewrite_lib", + ":config_lib", + "@envoy//test/mocks/network:network_mocks", + "@envoy//test/mocks/server:server_mocks", + "@envoy//test/mocks/stream_info:stream_info_mocks", + ], +) + +envoy_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + repository = "@envoy", + deps = [ + ":config_lib", + "@envoy//test/mocks/server:server_mocks", + ], +) diff --git a/src/envoy/tcp/tcp_cluster_rewrite/config.cc b/src/envoy/tcp/tcp_cluster_rewrite/config.cc new file mode 100644 index 00000000000..5decbbb38f0 --- /dev/null +++ b/src/envoy/tcp/tcp_cluster_rewrite/config.cc @@ -0,0 +1,72 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "src/envoy/tcp/tcp_cluster_rewrite/config.h" +#include "src/envoy/tcp/tcp_cluster_rewrite/tcp_cluster_rewrite.h" + +#include "envoy/registry/registry.h" +#include "envoy/server/filter_config.h" +#include "src/envoy/utils/config.h" + +using namespace ::istio::envoy::config::filter::network::tcp_cluster_rewrite; + +namespace Envoy { +namespace Tcp { +namespace TcpClusterRewrite { + +Network::FilterFactoryCb +TcpClusterRewriteFilterConfigFactory::createFilterFactory( + const Json::Object& config_json, Server::Configuration::FactoryContext&) { + v2alpha1::TcpClusterRewrite config_pb; + if (!Utils::ReadV2Config(config_json, &config_pb)) { + throw EnvoyException("Failed to parse JSON config"); + } + return createFilterFactory(config_pb); +} + +Network::FilterFactoryCb +TcpClusterRewriteFilterConfigFactory::createFilterFactoryFromProto( + const Protobuf::Message& config, Server::Configuration::FactoryContext&) { + return createFilterFactory( + dynamic_cast(config)); +} + +ProtobufTypes::MessagePtr +TcpClusterRewriteFilterConfigFactory::createEmptyConfigProto() { + return ProtobufTypes::MessagePtr{new v2alpha1::TcpClusterRewrite}; +} + +Network::FilterFactoryCb +TcpClusterRewriteFilterConfigFactory::createFilterFactory( + const v2alpha1::TcpClusterRewrite& config_pb) { + TcpClusterRewriteFilterConfigSharedPtr config( + std::make_shared(config_pb)); + return [config](Network::FilterManager& filter_manager) -> void { + filter_manager.addReadFilter( + std::make_shared(config)); + }; +} + +/** + * Static registration for the TCP cluster rewrite filter. @see RegisterFactory. + */ +static Registry::RegisterFactory< + TcpClusterRewriteFilterConfigFactory, + Server::Configuration::NamedNetworkFilterConfigFactory> + registered_; + +} // namespace TcpClusterRewrite +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/tcp/tcp_cluster_rewrite/config.h b/src/envoy/tcp/tcp_cluster_rewrite/config.h new file mode 100644 index 00000000000..94a6041c177 --- /dev/null +++ b/src/envoy/tcp/tcp_cluster_rewrite/config.h @@ -0,0 +1,58 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "envoy/config/filter/network/tcp_cluster_rewrite/v2alpha1/config.pb.h" + +#include "envoy/network/connection.h" +#include "envoy/network/filter.h" +#include "envoy/registry/registry.h" +#include "envoy/server/filter_config.h" + +using namespace ::istio::envoy::config::filter::network::tcp_cluster_rewrite; + +namespace Envoy { +namespace Tcp { +namespace TcpClusterRewrite { + +/** + * Config registration for the TCP cluster rewrite filter. @see + * NamedNetworkFilterConfigFactory. + */ +class TcpClusterRewriteFilterConfigFactory + : public Server::Configuration::NamedNetworkFilterConfigFactory { + public: + Network::FilterFactoryCb createFilterFactory( + const Json::Object&, Server::Configuration::FactoryContext&) override; + + Network::FilterFactoryCb createFilterFactoryFromProto( + const Protobuf::Message&, + Server::Configuration::FactoryContext&) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + + std::string name() override { + return "envoy.filters.network.tcp_cluster_rewrite"; + } + + private: + Network::FilterFactoryCb createFilterFactory( + const v2alpha1::TcpClusterRewrite& config_pb); +}; + +} // namespace TcpClusterRewrite +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/tcp/tcp_cluster_rewrite/config_test.cc b/src/envoy/tcp/tcp_cluster_rewrite/config_test.cc new file mode 100644 index 00000000000..142cf1e8104 --- /dev/null +++ b/src/envoy/tcp/tcp_cluster_rewrite/config_test.cc @@ -0,0 +1,49 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "src/envoy/tcp/tcp_cluster_rewrite/config.h" + +#include "test/mocks/server/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using namespace ::istio::envoy::config::filter::network::tcp_cluster_rewrite; +using testing::_; + +namespace Envoy { +namespace Tcp { +namespace TcpClusterRewrite { + +TEST(ConfigTest, ConfigTest) { + NiceMock context; + TcpClusterRewriteFilterConfigFactory factory; + v2alpha1::TcpClusterRewrite config = + *dynamic_cast( + factory.createEmptyConfigProto().get()); + + config.set_cluster_pattern("connection\\.sni"); + config.set_cluster_replacement("replacement.sni"); + + Network::FilterFactoryCb cb = + factory.createFilterFactoryFromProto(config, context); + Network::MockConnection connection; + EXPECT_CALL(connection, addReadFilter(_)); + cb(connection); +} + +} // namespace TcpClusterRewrite +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/tcp/tcp_cluster_rewrite/tcp_cluster_rewrite.cc b/src/envoy/tcp/tcp_cluster_rewrite/tcp_cluster_rewrite.cc new file mode 100644 index 00000000000..c1306d67a5e --- /dev/null +++ b/src/envoy/tcp/tcp_cluster_rewrite/tcp_cluster_rewrite.cc @@ -0,0 +1,78 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "src/envoy/tcp/tcp_cluster_rewrite/tcp_cluster_rewrite.h" + +#include "envoy/network/connection.h" + +#include "common/common/assert.h" +#include "common/tcp_proxy/tcp_proxy.h" + +using namespace ::istio::envoy::config::filter::network::tcp_cluster_rewrite; + +namespace Envoy { +namespace Tcp { +namespace TcpClusterRewrite { + +TcpClusterRewriteFilterConfig::TcpClusterRewriteFilterConfig( + const v2alpha1::TcpClusterRewrite& proto_config) { + if (!proto_config.cluster_pattern().empty()) { + should_rewrite_cluster_ = true; + cluster_pattern_ = std::regex(proto_config.cluster_pattern()); + cluster_replacement_ = proto_config.cluster_replacement(); + } else { + should_rewrite_cluster_ = false; + } +} + +Network::FilterStatus TcpClusterRewriteFilter::onNewConnection() { + if (config_->shouldRewriteCluster() && + read_callbacks_->connection() + .streamInfo() + .filterState() + .hasData( + TcpProxy::PerConnectionCluster::key())) { + absl::string_view cluster_name = + read_callbacks_->connection() + .streamInfo() + .filterState() + .getDataReadOnly( + TcpProxy::PerConnectionCluster::key()) + .value(); + ENVOY_CONN_LOG(trace, + "tcp_cluster_rewrite: new connection with server name {}", + read_callbacks_->connection(), cluster_name); + + // Rewrite the cluster name prior to setting the tcp_proxy cluster name. + std::string final_cluster_name(absl::StrCat(cluster_name)); + final_cluster_name = + std::regex_replace(final_cluster_name, config_->clusterPattern(), + config_->clusterReplacement()); + ENVOY_CONN_LOG(trace, + "tcp_cluster_rewrite: final tcp proxy cluster name {}", + read_callbacks_->connection(), final_cluster_name); + + // The data is mutable to allow other filters to change it. + read_callbacks_->connection().streamInfo().filterState().setData( + TcpProxy::PerConnectionCluster::key(), + std::make_unique(final_cluster_name), + StreamInfo::FilterState::StateType::Mutable); + } + return Network::FilterStatus::Continue; +} + +} // namespace TcpClusterRewrite +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/tcp/tcp_cluster_rewrite/tcp_cluster_rewrite.h b/src/envoy/tcp/tcp_cluster_rewrite/tcp_cluster_rewrite.h new file mode 100644 index 00000000000..1a96eada547 --- /dev/null +++ b/src/envoy/tcp/tcp_cluster_rewrite/tcp_cluster_rewrite.h @@ -0,0 +1,81 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 + +#include "envoy/config/filter/network/tcp_cluster_rewrite/v2alpha1/config.pb.h" +#include "envoy/network/filter.h" + +#include "common/common/logger.h" + +using namespace ::istio::envoy::config::filter::network::tcp_cluster_rewrite; + +namespace Envoy { +namespace Tcp { +namespace TcpClusterRewrite { + +/** + * Configuration for the TCP cluster rewrite filter. + */ +class TcpClusterRewriteFilterConfig { + public: + TcpClusterRewriteFilterConfig( + const v2alpha1::TcpClusterRewrite& proto_config); + + bool shouldRewriteCluster() const { return should_rewrite_cluster_; } + std::regex clusterPattern() const { return cluster_pattern_; } + std::string clusterReplacement() const { return cluster_replacement_; } + + private: + bool should_rewrite_cluster_; + std::regex cluster_pattern_; + std::string cluster_replacement_; +}; + +typedef std::shared_ptr + TcpClusterRewriteFilterConfigSharedPtr; + +/** + * Implementation of the TCP cluster rewrite filter that sets the upstream + * cluster name from the SNI field in the TLS connection. + */ +class TcpClusterRewriteFilter : public Network::ReadFilter, + Logger::Loggable { + public: + TcpClusterRewriteFilter(TcpClusterRewriteFilterConfigSharedPtr config) + : config_(config) {} + + // Network::ReadFilter + Network::FilterStatus onData(Buffer::Instance&, bool) override { + return Network::FilterStatus::Continue; + } + + Network::FilterStatus onNewConnection() override; + + void initializeReadFilterCallbacks( + Network::ReadFilterCallbacks& callbacks) override { + read_callbacks_ = &callbacks; + } + + private: + TcpClusterRewriteFilterConfigSharedPtr config_; + Network::ReadFilterCallbacks* read_callbacks_{}; +}; + +} // namespace TcpClusterRewrite +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/tcp/tcp_cluster_rewrite/tcp_cluster_rewrite_test.cc b/src/envoy/tcp/tcp_cluster_rewrite/tcp_cluster_rewrite_test.cc new file mode 100644 index 00000000000..c42aa58cc33 --- /dev/null +++ b/src/envoy/tcp/tcp_cluster_rewrite/tcp_cluster_rewrite_test.cc @@ -0,0 +1,133 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "common/tcp_proxy/tcp_proxy.h" + +#include "src/envoy/tcp/tcp_cluster_rewrite/config.h" +#include "src/envoy/tcp/tcp_cluster_rewrite/tcp_cluster_rewrite.h" + +#include "test/mocks/network/mocks.h" +#include "test/mocks/server/mocks.h" +#include "test/mocks/stream_info/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using namespace ::istio::envoy::config::filter::network::tcp_cluster_rewrite; +using testing::_; +using testing::Matcher; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Tcp { +namespace TcpClusterRewrite { + +class TcpClusterRewriteFilterTest : public testing::Test { + public: + TcpClusterRewriteFilterTest() { + ON_CALL(filter_callbacks_.connection_, streamInfo()) + .WillByDefault(ReturnRef(stream_info_)); + ON_CALL(Const(filter_callbacks_.connection_), streamInfo()) + .WillByDefault(ReturnRef(stream_info_)); + configure(v2alpha1::TcpClusterRewrite()); + } + + void configure(v2alpha1::TcpClusterRewrite proto_config) { + config_ = std::make_unique(proto_config); + filter_ = std::make_unique(config_); + filter_->initializeReadFilterCallbacks(filter_callbacks_); + } + + NiceMock filter_callbacks_; + NiceMock stream_info_; + TcpClusterRewriteFilterConfigSharedPtr config_; + std::unique_ptr filter_; +}; + +TEST_F(TcpClusterRewriteFilterTest, ClusterRewrite) { + // no rewrite + { + stream_info_.filterState().setData( + TcpProxy::PerConnectionCluster::key(), + std::make_unique( + "hello.ns1.svc.cluster.local"), + StreamInfo::FilterState::StateType::Mutable); + filter_->onNewConnection(); + + EXPECT_TRUE( + stream_info_.filterState().hasData( + TcpProxy::PerConnectionCluster::key())); + + auto per_connection_cluster = + stream_info_.filterState() + .getDataReadOnly( + TcpProxy::PerConnectionCluster::key()); + EXPECT_EQ(per_connection_cluster.value(), "hello.ns1.svc.cluster.local"); + } + + // with simple rewrite + { + v2alpha1::TcpClusterRewrite proto_config; + proto_config.set_cluster_pattern("\\.global$"); + proto_config.set_cluster_replacement(".svc.cluster.local"); + configure(proto_config); + + stream_info_.filterState().setData( + TcpProxy::PerConnectionCluster::key(), + std::make_unique("hello.ns1.global"), + StreamInfo::FilterState::StateType::Mutable); + filter_->onNewConnection(); + + EXPECT_TRUE( + stream_info_.filterState().hasData( + TcpProxy::PerConnectionCluster::key())); + + auto per_connection_cluster = + stream_info_.filterState() + .getDataReadOnly( + TcpProxy::PerConnectionCluster::key()); + EXPECT_EQ(per_connection_cluster.value(), "hello.ns1.svc.cluster.local"); + } + + // with regex rewrite + { + v2alpha1::TcpClusterRewrite proto_config; + proto_config.set_cluster_pattern("^.*$"); + proto_config.set_cluster_replacement("another.svc.cluster.local"); + configure(proto_config); + + stream_info_.filterState().setData( + TcpProxy::PerConnectionCluster::key(), + std::make_unique("hello.ns1.global"), + StreamInfo::FilterState::StateType::Mutable); + filter_->onNewConnection(); + + EXPECT_TRUE( + stream_info_.filterState().hasData( + TcpProxy::PerConnectionCluster::key())); + + auto per_connection_cluster = + stream_info_.filterState() + .getDataReadOnly( + TcpProxy::PerConnectionCluster::key()); + EXPECT_EQ(per_connection_cluster.value(), "another.svc.cluster.local"); + } +} + +} // namespace TcpClusterRewrite +} // namespace Tcp +} // namespace Envoy diff --git a/src/envoy/utils/BUILD b/src/envoy/utils/BUILD index 2fd540d8b1c..3415ef32633 100644 --- a/src/envoy/utils/BUILD +++ b/src/envoy/utils/BUILD @@ -32,9 +32,11 @@ envoy_cc_library( repository = "@envoy", visibility = ["//visibility:public"], deps = [ + ":utils_lib", "//include/istio/utils:attribute_names_header", "//src/istio/authn:context_proto_cc", "//src/istio/utils:attribute_names_lib", + "//src/istio/utils:utils_lib", ":filter_names_lib", "@envoy//source/exe:envoy_common_lib", ], @@ -87,10 +89,22 @@ envoy_cc_test( repository = "@envoy", deps = [ ":utils_lib", + "@envoy//test/mocks/stream_info:stream_info_mocks", "@envoy//test/test_common:utility_lib", ], ) +envoy_cc_test( + name = "mixer_control_test", + srcs = [ + "mixer_control_test.cc", + ], + repository = "@envoy", + deps = [ + ":utils_lib", + "@envoy//test/test_common:utility_lib", + ], +) cc_library( name = "filter_names_lib", diff --git a/src/envoy/utils/authn.cc b/src/envoy/utils/authn.cc index a28f8ceba8f..8ca2a95bd0c 100644 --- a/src/envoy/utils/authn.cc +++ b/src/envoy/utils/authn.cc @@ -18,6 +18,7 @@ #include "include/istio/utils/attribute_names.h" #include "src/envoy/utils/filter_names.h" #include "src/istio/authn/context.pb.h" +#include "src/istio/utils/utils.h" using istio::authn::Result; @@ -47,6 +48,11 @@ void Authentication::SaveAuthAttributesToStruct( result.peer_user()); setKeyValue(data, istio::utils::AttributeName::kSourcePrincipal, result.peer_user()); + std::string source_ns(""); + if (istio::utils::GetSourceNamespace(result.peer_user(), &source_ns)) { + setKeyValue(data, istio::utils::AttributeName::kSourceNamespace, + source_ns); + } } if (result.has_origin()) { const auto& origin = result.origin(); @@ -70,13 +76,10 @@ void Authentication::SaveAuthAttributesToStruct( setKeyValue(data, istio::utils::AttributeName::kRequestAuthPresenter, origin.presenter()); } - if (!origin.claims().empty()) { - auto s = (*data.mutable_fields()) - [istio::utils::AttributeName::kRequestAuthClaims] - .mutable_struct_value(); - for (const auto& pair : origin.claims()) { - setKeyValue(*s, pair.first, pair.second); - } + if (!origin.claims().fields().empty()) { + *((*data.mutable_fields()) + [istio::utils::AttributeName::kRequestAuthClaims] + .mutable_struct_value()) = origin.claims(); } if (!origin.raw_claims().empty()) { setKeyValue(data, istio::utils::AttributeName::kRequestAuthRawClaims, diff --git a/src/envoy/utils/authn_test.cc b/src/envoy/utils/authn_test.cc index cc97855cda9..a80227a5766 100644 --- a/src/envoy/utils/authn_test.cc +++ b/src/envoy/utils/authn_test.cc @@ -41,16 +41,19 @@ TEST_F(AuthenticationTest, SaveAuthAttributesToStruct) { EXPECT_TRUE(data.mutable_fields()->empty()); result.set_principal("principal"); - result.set_peer_user("peeruser"); + result.set_peer_user("cluster.local/sa/peeruser/ns/abc/"); auto origin = result.mutable_origin(); origin->add_audiences("audiences0"); origin->add_audiences("audiences1"); origin->set_presenter("presenter"); - origin->add_groups("group1"); - origin->add_groups("group2"); - auto claim = origin->mutable_claims(); - (*claim)["key1"] = "value1"; - (*claim)["key2"] = "value2"; + (*origin->mutable_claims()->mutable_fields())["groups"] + .mutable_list_value() + ->add_values() + ->set_string_value("group1"); + (*origin->mutable_claims()->mutable_fields())["groups"] + .mutable_list_value() + ->add_values() + ->set_string_value("group2"); origin->set_raw_claims("rawclaim"); Authentication::SaveAuthAttributesToStruct(result, data); @@ -62,38 +65,40 @@ TEST_F(AuthenticationTest, SaveAuthAttributesToStruct) { "principal"); EXPECT_EQ( data.fields().at(istio::utils::AttributeName::kSourceUser).string_value(), - "peeruser"); + "cluster.local/sa/peeruser/ns/abc/"); EXPECT_EQ(data.fields() .at(istio::utils::AttributeName::kSourcePrincipal) .string_value(), - "peeruser"); + "cluster.local/sa/peeruser/ns/abc/"); + EXPECT_EQ(data.fields() + .at(istio::utils::AttributeName::kSourceNamespace) + .string_value(), + "abc"); EXPECT_EQ(data.fields() .at(istio::utils::AttributeName::kRequestAuthAudiences) .string_value(), "audiences0"); EXPECT_EQ(data.fields() - .at(istio::utils::AttributeName::kRequestAuthGroups) + .at(istio::utils::AttributeName::kRequestAuthPresenter) + .string_value(), + "presenter"); + + auto auth_claims = + data.fields().at(istio::utils::AttributeName::kRequestAuthClaims); + EXPECT_EQ(auth_claims.struct_value() + .fields() + .at("groups") .list_value() .values(0) .string_value(), "group1"); - EXPECT_EQ(data.fields() - .at(istio::utils::AttributeName::kRequestAuthGroups) + EXPECT_EQ(auth_claims.struct_value() + .fields() + .at("groups") .list_value() .values(1) .string_value(), "group2"); - EXPECT_EQ(data.fields() - .at(istio::utils::AttributeName::kRequestAuthPresenter) - .string_value(), - "presenter"); - - auto actual_claim = - data.fields().at(istio::utils::AttributeName::kRequestAuthClaims); - EXPECT_EQ(actual_claim.struct_value().fields().at("key1").string_value(), - "value1"); - EXPECT_EQ(actual_claim.struct_value().fields().at("key2").string_value(), - "value2"); EXPECT_EQ(data.fields() .at(istio::utils::AttributeName::kRequestAuthRawClaims) @@ -102,4 +107,4 @@ TEST_F(AuthenticationTest, SaveAuthAttributesToStruct) { } } // namespace Utils -} // namespace Envoy +} // namespace Envoy \ No newline at end of file diff --git a/src/envoy/utils/grpc_transport.cc b/src/envoy/utils/grpc_transport.cc index 37eebbc0427..17f4591875d 100644 --- a/src/envoy/utils/grpc_transport.cc +++ b/src/envoy/utils/grpc_transport.cc @@ -83,6 +83,7 @@ void GrpcTransport::onFailure( template void GrpcTransport::Cancel() { ENVOY_LOG(debug, "Cancel gRPC request {}", descriptor().name()); + request_->cancel(); delete this; } diff --git a/src/envoy/utils/mixer_control.cc b/src/envoy/utils/mixer_control.cc index 7558f8da09d..c6361a678f8 100644 --- a/src/envoy/utils/mixer_control.cc +++ b/src/envoy/utils/mixer_control.cc @@ -17,9 +17,16 @@ #include "src/envoy/utils/grpc_transport.h" using ::istio::mixerclient::Statistics; +using ::istio::utils::AttributeName; +using ::istio::utils::LocalAttributes; +using ::istio::utils::LocalNode; namespace Envoy { namespace Utils { + +const char kNodeUID[] = "NODE_UID"; +const char kNodeNamespace[] = "NODE_NAMESPACE"; + namespace { // A class to wrap envoy timer for mixer client timer. @@ -55,6 +62,18 @@ class EnvoyGrpcAsyncClientFactory : public Grpc::AsyncClientFactory { TimeSource &time_source_; }; +inline bool ReadProtoMap( + const google::protobuf::Map &meta, + const std::string &key, std::string *val) { + const auto it = meta.find(key); + if (it != meta.end()) { + *val = it->second.string_value(); + return true; + } + + return false; +} + } // namespace // Create all environment functions for mixerclient @@ -103,5 +122,81 @@ Grpc::AsyncClientFactoryPtr GrpcClientFactoryForCluster( time_source); } +// This function is for compatibility with existing node ids. +// "sidecar~10.36.0.15~fortioclient-84469dc8d7-jbbxt.service-graph~service-graph.svc.cluster.local" +// --> {proxy_type}~{ip}~{node_name}.{node_ns}~{node_domain} +bool ExtractInfoCompat(const std::string &nodeid, LocalNode *args) { + auto &logger = Logger::Registry::getLog(Logger::Id::config); + + auto parts = StringUtil::splitToken(nodeid, "~"); + if (parts.size() < 3) { + ENVOY_LOG_TO_LOGGER( + logger, debug, + "ExtractInfoCompat node id {} did not have the correct format:{} ", + nodeid, "{proxy_type}~{ip}~{node_name}.{node_ns}~{node_domain} "); + return false; + } + + auto longname = std::string(parts[2].begin(), parts[2].end()); + auto names = StringUtil::splitToken(longname, "."); + if (names.size() < 2) { + ENVOY_LOG_TO_LOGGER(logger, debug, + "ExtractInfoCompat node_name {} must have two parts: " + "node_name.namespace", + longname); + return false; + } + auto ns = std::string(names[1].begin(), names[1].end()); + + args->ns = ns; + args->uid = "kubernetes://" + longname; + + return true; +} + +// ExtractInfo depends on NODE_UID, NODE_NAMESPACE +bool ExtractInfo(const envoy::api::v2::core::Node &node, LocalNode *args) { + auto &logger = Logger::Registry::getLog(Logger::Id::config); + + const auto meta = node.metadata().fields(); + + if (meta.empty()) { + ENVOY_LOG_TO_LOGGER(logger, debug, "ExtractInfo node metadata empty: {}", + node.DebugString()); + return false; + } + + std::string uid; + if (!ReadProtoMap(meta, kNodeUID, &uid)) { + ENVOY_LOG_TO_LOGGER(logger, debug, + "ExtractInfo node metadata missing:{} {}", kNodeUID, + node.metadata().DebugString()); + return false; + } + + std::string ns; + if (!ReadProtoMap(meta, kNodeNamespace, &ns)) { + ENVOY_LOG_TO_LOGGER(logger, debug, + "ExtractInfo node metadata missing:{} {}", + kNodeNamespace, node.metadata().DebugString()); + return false; + } + + args->ns = ns; + args->uid = uid; + + return true; +} + +bool ExtractNodeInfo(const envoy::api::v2::core::Node &node, LocalNode *args) { + if (ExtractInfo(node, args)) { + return true; + } + if (ExtractInfoCompat(node.id(), args)) { + return true; + } + return false; +} + } // namespace Utils } // namespace Envoy diff --git a/src/envoy/utils/mixer_control.h b/src/envoy/utils/mixer_control.h index 6a4586847b0..f9b34090d0d 100644 --- a/src/envoy/utils/mixer_control.h +++ b/src/envoy/utils/mixer_control.h @@ -16,13 +16,14 @@ #pragma once #include "envoy/event/dispatcher.h" +#include "envoy/local_info/local_info.h" #include "envoy/runtime/runtime.h" #include "envoy/upstream/cluster_manager.h" #include "include/istio/mixerclient/client.h" +#include "include/istio/utils/attribute_names.h" +#include "include/istio/utils/local_attributes.h" #include "src/envoy/utils/config.h" -using ::istio::mixer::v1::Attributes; - namespace Envoy { namespace Utils { @@ -42,5 +43,8 @@ Grpc::AsyncClientFactoryPtr GrpcClientFactoryForCluster( const std::string &cluster_name, Upstream::ClusterManager &cm, Stats::Scope &scope, TimeSource &time_source); +bool ExtractNodeInfo(const envoy::api::v2::core::Node &node, + ::istio::utils::LocalNode *args); + } // namespace Utils } // namespace Envoy diff --git a/src/envoy/utils/mixer_control_test.cc b/src/envoy/utils/mixer_control_test.cc new file mode 100644 index 00000000000..61fcc674a71 --- /dev/null +++ b/src/envoy/utils/mixer_control_test.cc @@ -0,0 +1,129 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "src/envoy/utils/mixer_control.h" +#include "fmt/printf.h" +#include "mixer/v1/config/client/client_config.pb.h" +#include "src/envoy/utils/utils.h" +#include "test/test_common/utility.h" + +using Envoy::Utils::ExtractNodeInfo; +using Envoy::Utils::ParseJsonMessage; +using ::istio::utils::AttributeName; +using ::istio::utils::CreateLocalAttributes; +using ::istio::utils::LocalAttributes; +using ::istio::utils::LocalNode; + +namespace { + +#define ASSERT_LOCAL_NODE(lexp, la) \ + { \ + EXPECT_EQ((lexp).uid, (la).uid); \ + EXPECT_EQ((lexp).ns, (la).ns); \ + }; + +bool ReadAttributeMap( + const google::protobuf::Map< + std::string, ::istio::mixer::v1::Attributes_AttributeValue> &meta, + const std::string &key, std::string *val) { + const auto it = meta.find(key); + if (it != meta.end()) { + *val = it->second.string_value(); + return true; + } + return false; +} + +const std::string kUID = + "kubernetes://fortioclient-84469dc8d7-jbbxt.service-graph"; +const std::string kNS = "service-graph"; +const std::string kNodeID = + "sidecar~10.36.0.15~fortioclient-84469dc8d7-jbbxt.service-graph~service-" + "graph.svc.cluster.local"; + +std::string genNodeConfig(std::string uid, std::string nodeid, std::string ns) { + auto md = R"( + "metadata": { + "ISTIO_VERSION": "1.0.1", + "NODE_UID": "%s", + "NODE_NAMESPACE": "%s", + }, + )"; + std::string meta = ""; + if (!ns.empty()) { + meta = fmt::sprintf(md, nodeid, ns); + } + + return fmt::sprintf(R"({ + "id": "%s", + "cluster": "fortioclient", + %s + "build_version": "0/1.8.0-dev//RELEASE" + })", + uid, meta); +} + +void initTestLocalNode(LocalNode *lexp) { + lexp->uid = kUID; + lexp->ns = kNS; +} + +TEST(MixerControlTest, CreateLocalAttributes) { + LocalNode lexp; + initTestLocalNode(&lexp); + + LocalAttributes la; + CreateLocalAttributes(lexp, &la); + + const auto att = la.outbound.attributes(); + std::string val; + + EXPECT_TRUE(ReadAttributeMap(att, AttributeName::kSourceUID, &val)); + EXPECT_TRUE(val == lexp.uid); + + EXPECT_TRUE(ReadAttributeMap(att, AttributeName::kSourceNamespace, &val)); + EXPECT_TRUE(val == lexp.ns); +} + +TEST(MixerControlTest, WithMetadata) { + LocalNode lexp; + initTestLocalNode(&lexp); + + envoy::api::v2::core::Node node; + auto status = + ParseJsonMessage(genNodeConfig("new_id", lexp.uid, lexp.ns), &node); + EXPECT_OK(status) << status; + + LocalNode largs; + EXPECT_TRUE(ExtractNodeInfo(node, &largs)); + + ASSERT_LOCAL_NODE(lexp, largs); +} + +TEST(MixerControlTest, NoMetadata) { + LocalNode lexp; + initTestLocalNode(&lexp); + + envoy::api::v2::core::Node node; + auto status = ParseJsonMessage(genNodeConfig(kNodeID, "", ""), &node); + EXPECT_OK(status) << status; + + LocalNode largs; + EXPECT_TRUE(ExtractNodeInfo(node, &largs)); + + ASSERT_LOCAL_NODE(lexp, largs); +} + +} // namespace diff --git a/src/envoy/utils/utils.cc b/src/envoy/utils/utils.cc index 6e982dd5a0b..ced3180b407 100644 --- a/src/envoy/utils/utils.cc +++ b/src/envoy/utils/utils.cc @@ -75,11 +75,10 @@ bool GetIpPort(const Network::Address::Ip* ip, std::string* str_ip, int* port) { return false; } -bool GetDestinationUID( - const std::shared_ptr metadata, - std::string* uid) { - const auto filter_it = metadata->filter_metadata().find(kPerHostMetadataKey); - if (filter_it == metadata->filter_metadata().end()) { +bool GetDestinationUID(const envoy::api::v2::core::Metadata& metadata, + std::string* uid) { + const auto filter_it = metadata.filter_metadata().find(kPerHostMetadataKey); + if (filter_it == metadata.filter_metadata().end()) { return false; } const Struct& struct_pb = filter_it->second; @@ -140,5 +139,21 @@ Status ParseJsonMessage(const std::string& json, Message* output) { return ::google::protobuf::util::JsonStringToMessage(json, output, options); } +void CheckResponseInfoToStreamInfo( + const istio::mixerclient::CheckResponseInfo& check_response, + StreamInfo::StreamInfo& stream_info) { + static std::string metadata_key = "istio.mixer"; + + if (!check_response.response_status.ok()) { + stream_info.setResponseFlag( + StreamInfo::ResponseFlag::UnauthorizedExternalService); + ProtobufWkt::Struct metadata; + auto& fields = *metadata.mutable_fields(); + fields["status"].set_string_value( + check_response.response_status.ToString()); + stream_info.setDynamicMetadata(metadata_key, metadata); + } +} + } // namespace Utils -} // namespace Envoy +} // namespace Envoy \ No newline at end of file diff --git a/src/envoy/utils/utils.h b/src/envoy/utils/utils.h index 907585ff4da..cd73d0e3cb1 100644 --- a/src/envoy/utils/utils.h +++ b/src/envoy/utils/utils.h @@ -21,6 +21,7 @@ #include "envoy/http/header_map.h" #include "envoy/network/connection.h" #include "google/protobuf/util/json_util.h" +#include "include/istio/mixerclient/check_response.h" namespace Envoy { namespace Utils { @@ -34,9 +35,8 @@ void ExtractHeaders(const Http::HeaderMap& header_map, bool GetIpPort(const Network::Address::Ip* ip, std::string* str_ip, int* port); // Get destination.uid attribute value from metadata. -bool GetDestinationUID( - const std::shared_ptr metadata, - std::string* uid); +bool GetDestinationUID(const envoy::api::v2::core::Metadata& metadata, + std::string* uid); // Get peer or local principal URI. bool GetPrincipal(const Network::Connection* connection, bool peer, @@ -53,5 +53,10 @@ bool GetRequestedServerName(const Network::Connection* connection, ::google::protobuf::util::Status ParseJsonMessage( const std::string& json, ::google::protobuf::Message* output); +// Add result of check to envoy stream info to allow better logging. +void CheckResponseInfoToStreamInfo( + const istio::mixerclient::CheckResponseInfo& check_response, + StreamInfo::StreamInfo& stream_info); + } // namespace Utils -} // namespace Envoy +} // namespace Envoy \ No newline at end of file diff --git a/src/envoy/utils/utils_test.cc b/src/envoy/utils/utils_test.cc index 8d38b995e77..acd2bb4f9de 100644 --- a/src/envoy/utils/utils_test.cc +++ b/src/envoy/utils/utils_test.cc @@ -15,12 +15,14 @@ #include "src/envoy/utils/utils.h" #include "mixer/v1/config/client/client_config.pb.h" +#include "test/mocks/stream_info/mocks.h" #include "test/test_common/utility.h" -using Envoy::Utils::ParseJsonMessage; - namespace { +using Envoy::Utils::CheckResponseInfoToStreamInfo; +using Envoy::Utils::ParseJsonMessage; + TEST(UtilsTest, ParseNormalMessage) { std::string config_str = R"({ "default_destination_service": "service.svc.cluster.local", @@ -44,4 +46,25 @@ TEST(UtilsTest, ParseMessageWithUnknownField) { EXPECT_EQ(http_config.default_destination_service(), "service.svc.cluster.local"); } + +TEST(UtilsTest, CheckResponseInfoToStreamInfo) { + ::istio::mixerclient::CheckResponseInfo + check_response; // by default status is unknown + Envoy::StreamInfo::MockStreamInfo mock_stream_info; + + EXPECT_CALL( + mock_stream_info, + setResponseFlag( + Envoy::StreamInfo::ResponseFlag::UnauthorizedExternalService)); + EXPECT_CALL(mock_stream_info, setDynamicMetadata(_, _)) + .WillOnce(Invoke( + [](const std::string& key, const Envoy::ProtobufWkt::Struct& value) { + EXPECT_EQ("istio.mixer", key); + EXPECT_EQ(1, value.fields().count("status")); + EXPECT_EQ("UNKNOWN", value.fields().at("status").string_value()); + })); + + CheckResponseInfoToStreamInfo(check_response, mock_stream_info); +} + } // namespace diff --git a/src/istio/api_spec/path_matcher.h b/src/istio/api_spec/path_matcher.h index 02537c23639..f8b5e68cb82 100644 --- a/src/istio/api_spec/path_matcher.h +++ b/src/istio/api_spec/path_matcher.h @@ -54,13 +54,6 @@ class PathMatcher { public: ~PathMatcher(){}; - // TODO: Do not template VariableBinding - template - Method Lookup(const std::string& http_method, const std::string& path, - const std::string& query_params, - std::vector* variable_bindings, - std::string* body_field_path) const; - Method Lookup(const std::string& http_method, const std::string& path) const; private: @@ -139,177 +132,6 @@ std::vector& split(const std::string& s, char delim, return elems; } -inline bool IsReservedChar(char c) { - // Reserved characters according to RFC 6570 - switch (c) { - case '!': - case '#': - case '$': - case '&': - case '\'': - case '(': - case ')': - case '*': - case '+': - case ',': - case '/': - case ':': - case ';': - case '=': - case '?': - case '@': - case '[': - case ']': - return true; - default: - return false; - } -} - -// Check if an ASCII character is a hex digit. We can't use ctype's -// isxdigit() because it is affected by locale. This function is applied -// to the escaped characters in a url, not to natural-language -// strings, so locale should not be taken into account. -inline bool ascii_isxdigit(char c) { - return ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F') || - ('0' <= c && c <= '9'); -} - -inline int hex_digit_to_int(char c) { - /* Assume ASCII. */ - int x = static_cast(c); - if (x > '9') { - x += 9; - } - return x & 0xf; -} - -// This is a helper function for UrlUnescapeString. It takes a string and -// the index of where we are within that string. -// -// The function returns true if the next three characters are of the format: -// "%[0-9A-Fa-f]{2}". -// -// If the next three characters are an escaped character then this function will -// also return what character is escaped. -bool GetEscapedChar(const std::string& src, size_t i, - bool unescape_reserved_chars, char* out) { - if (i + 2 < src.size() && src[i] == '%') { - if (ascii_isxdigit(src[i + 1]) && ascii_isxdigit(src[i + 2])) { - char c = - (hex_digit_to_int(src[i + 1]) << 4) | hex_digit_to_int(src[i + 2]); - if (!unescape_reserved_chars && IsReservedChar(c)) { - return false; - } - *out = c; - return true; - } - } - return false; -} - -// Unescapes string 'part' and returns the unescaped string. Reserved characters -// (as specified in RFC 6570) are not escaped if unescape_reserved_chars is -// false. -std::string UrlUnescapeString(const std::string& part, - bool unescape_reserved_chars) { - std::string unescaped; - // Check whether we need to escape at all. - bool needs_unescaping = false; - char ch = '\0'; - for (size_t i = 0; i < part.size(); ++i) { - if (GetEscapedChar(part, i, unescape_reserved_chars, &ch)) { - needs_unescaping = true; - break; - } - } - if (!needs_unescaping) { - unescaped = part; - return unescaped; - } - - unescaped.resize(part.size()); - - char* begin = &(unescaped)[0]; - char* p = begin; - - for (size_t i = 0; i < part.size();) { - if (GetEscapedChar(part, i, unescape_reserved_chars, &ch)) { - *p++ = ch; - i += 3; - } else { - *p++ = part[i]; - i += 1; - } - } - - unescaped.resize(p - begin); - return unescaped; -} - -template -void ExtractBindingsFromPath(const std::vector& vars, - const std::vector& parts, - std::vector* bindings) { - for (const auto& var : vars) { - // Determine the subpath bound to the variable based on the - // [start_segment, end_segment) segment range of the variable. - // - // In case of matching "**" - end_segment is negative and is relative to - // the end such that end_segment = -1 will match all subsequent segments. - VariableBinding binding; - binding.field_path = var.field_path; - // Calculate the absolute index of the ending segment in case it's negative. - size_t end_segment = (var.end_segment >= 0) - ? var.end_segment - : parts.size() + var.end_segment + 1; - // It is multi-part match if we have more than one segment. We also make - // sure that a single URL segment match with ** is also considered a - // multi-part match by checking if it->second.end_segment is negative. - bool is_multipart = - (end_segment - var.start_segment) > 1 || var.end_segment < 0; - // Joins parts with "/" to form a path string. - for (size_t i = var.start_segment; i < end_segment; ++i) { - // For multipart matches only unescape non-reserved characters. - binding.value += UrlUnescapeString(parts[i], !is_multipart); - if (i < end_segment - 1) { - binding.value += "/"; - } - } - bindings->emplace_back(binding); - } -} - -template -void ExtractBindingsFromQueryParameters( - const std::string& query_params, const std::set& system_params, - std::vector* bindings) { - // The bindings in URL the query parameters have the following form: - // =value1&=value2&...&=valueN - // Query parameters may also contain system parameters such as `api_key`. - // We'll need to ignore these. Example: - // book.id=123&book.author=Neal%20Stephenson&api_key=AIzaSyAz7fhBkC35D2M - std::vector params; - split(query_params, '&', params); - for (const auto& param : params) { - size_t pos = param.find('='); - if (pos != 0 && pos != std::string::npos) { - auto name = param.substr(0, pos); - // Make sure the query parameter is not a system parameter (e.g. - // `api_key`) before adding the binding. - if (system_params.find(name) == std::end(system_params)) { - // The name of the parameter is a field path, which is a dot-delimited - // sequence of field names that identify the (potentially deep) field - // in the request, e.g. `book.author.name`. - VariableBinding binding; - split(name, '.', binding.field_path); - binding.value = UrlUnescapeString(param.substr(pos + 1), true); - bindings->emplace_back(std::move(binding)); - } - } - } -} - // Converts a request path into a format that can be used to perform a request // lookup in the PathMatcher trie. This utility method sanitizes the request // path and then splits the path into slash separated parts. Returns an empty @@ -380,51 +202,6 @@ PathMatcher::PathMatcher(PathMatcherBuilder&& builder) custom_verbs_(std::move(builder.custom_verbs_)), methods_(std::move(builder.methods_)) {} -// Lookup is a wrapper method for the recursive node Lookup. First, the wrapper -// splits the request path into slash-separated path parts. Next, the method -// checks that the |http_method| is supported. If not, then it returns an empty -// WrapperGraph::SharedPtr. Next, this method invokes the node's Lookup on -// the extracted |parts|. Finally, it fills the mapping from variables to their -// values parsed from the path. -// TODO: cache results by adding get/put methods here (if profiling reveals -// benefit) -template -template -Method PathMatcher::Lookup( - const std::string& http_method, const std::string& path, - const std::string& query_params, - std::vector* variable_bindings, - std::string* body_field_path) const { - const std::vector parts = - ExtractRequestParts(path, custom_verbs_); - - // If service_name has not been registered to ESP and strict_service_matching_ - // is set to false, tries to lookup the method in all registered services. - if (root_ptr_ == nullptr) { - return nullptr; - } - - PathMatcherLookupResult lookup_result = - LookupInPathMatcherNode(*root_ptr_, parts, http_method); - // Return nullptr if nothing is found. - // Not need to check duplication. Only first item is stored for duplicated - if (lookup_result.data == nullptr) { - return nullptr; - } - MethodData* method_data = reinterpret_cast(lookup_result.data); - if (variable_bindings != nullptr) { - variable_bindings->clear(); - ExtractBindingsFromPath(method_data->variables, parts, variable_bindings); - ExtractBindingsFromQueryParameters( - query_params, method_data->method->system_query_parameter_names(), - variable_bindings); - } - if (body_field_path != nullptr) { - *body_field_path = method_data->body_field_path; - } - return method_data->method; -} - // TODO: refactor common code with method above template Method PathMatcher::Lookup(const std::string& http_method, diff --git a/src/istio/api_spec/path_matcher_test.cc b/src/istio/api_spec/path_matcher_test.cc index 133e894eef3..20ae7b4d70d 100644 --- a/src/istio/api_spec/path_matcher_test.cc +++ b/src/istio/api_spec/path_matcher_test.cc @@ -30,83 +30,15 @@ namespace api_spec { namespace { -// VariableBinding specifies a value for a single field in the request message. -// When transcoding HTTP/REST/JSON to gRPC/proto the request message is -// constructed using the HTTP body and the variable bindings (specified through -// request url). -struct Binding { - // The location of the field in the protobuf message, where the value - // needs to be inserted, e.g. "shelf.theme" would mean the "theme" field - // of the nested "shelf" message of the request protobuf message. - std::vector field_path; - // The value to be inserted. - std::string value; -}; - -typedef std::vector Bindings; -typedef std::vector FieldPath; -class MethodInfo { - public: - MOCK_CONST_METHOD0(system_query_parameter_names, - const std::set&()); -}; - -bool operator==(const Binding& b1, const Binding& b2) { - return b1.field_path == b2.field_path && b1.value == b2.value; -} - -std::string FieldPathToString(const FieldPath& fp) { - std::string s; - for (const auto& f : fp) { - if (!s.empty()) { - s += "."; - } - s += f; - } - return s; -} - -} // namespace - -std::ostream& operator<<(std::ostream& os, const Binding& b) { - return os << "{ " << FieldPathToString(b.field_path) << "=" << b.value << "}"; -} - -std::ostream& operator<<(std::ostream& os, const Bindings& bindings) { - for (const auto& b : bindings) { - os << b << std::endl; - } - return os; -} - -namespace { +struct MethodInfo {}; class PathMatcherTest : public ::testing::Test { protected: PathMatcherTest() {} ~PathMatcherTest() {} - MethodInfo* AddPathWithBodyFieldPath(std::string http_method, - std::string http_template, - std::string body_field_path) { - auto method = new MethodInfo(); - ON_CALL(*method, system_query_parameter_names()) - .WillByDefault(ReturnRef(empty_set_)); - if (!builder_.Register(http_method, http_template, body_field_path, - method)) { - delete method; - return nullptr; - } - stored_methods_.emplace_back(method); - return method; - } - - MethodInfo* AddPathWithSystemParams( - std::string http_method, std::string http_template, - const std::set* system_params) { + MethodInfo* AddPath(std::string http_method, std::string http_template) { auto method = new MethodInfo(); - ON_CALL(*method, system_query_parameter_names()) - .WillByDefault(ReturnRef(*system_params)); if (!builder_.Register(http_method, http_template, std::string(), method)) { delete method; return nullptr; @@ -115,47 +47,18 @@ class PathMatcherTest : public ::testing::Test { return method; } - MethodInfo* AddPath(std::string http_method, std::string http_template) { - return AddPathWithBodyFieldPath(http_method, http_template, std::string()); - } - MethodInfo* AddGetPath(std::string path) { return AddPath("GET", path); } void Build() { matcher_ = builder_.Build(); } - MethodInfo* LookupWithBodyFieldPath(std::string method, std::string path, - Bindings* bindings, - std::string* body_field_path) { - return matcher_->Lookup(method, path, "", bindings, body_field_path); - } - - MethodInfo* Lookup(std::string method, std::string path, Bindings* bindings) { - std::string body_field_path; - return matcher_->Lookup(method, path, std::string(), bindings, - &body_field_path); - } - - MethodInfo* LookupWithParams(std::string method, std::string path, - std::string query_params, Bindings* bindings) { - std::string body_field_path; - return matcher_->Lookup(method, path, query_params, bindings, - &body_field_path); - } - - MethodInfo* LookupNoBindings(std::string method, std::string path) { - Bindings bindings; - std::string body_field_path; - auto result = matcher_->Lookup(method, path, std::string(), &bindings, - &body_field_path); - EXPECT_EQ(0, bindings.size()); - return result; + MethodInfo* Lookup(std::string method, std::string path) { + return matcher_->Lookup(method, path); } private: PathMatcherBuilder builder_; PathMatcherPtr matcher_; std::vector> stored_methods_; - std::set empty_set_; }; TEST_F(PathMatcherTest, WildCardMatchesRoot) { @@ -164,9 +67,9 @@ TEST_F(PathMatcherTest, WildCardMatchesRoot) { EXPECT_NE(nullptr, data); - EXPECT_EQ(LookupNoBindings("GET", "/"), data); - EXPECT_EQ(LookupNoBindings("GET", "/a"), data); - EXPECT_EQ(LookupNoBindings("GET", "/a/"), data); + EXPECT_EQ(Lookup("GET", "/"), data); + EXPECT_EQ(Lookup("GET", "/a"), data); + EXPECT_EQ(Lookup("GET", "/a/"), data); } TEST_F(PathMatcherTest, WildCardMatches) { @@ -184,20 +87,20 @@ TEST_F(PathMatcherTest, WildCardMatches) { EXPECT_NE(nullptr, c_de); EXPECT_NE(nullptr, cfde); - EXPECT_EQ(LookupNoBindings("GET", "/a/b"), a__); - EXPECT_EQ(LookupNoBindings("GET", "/a/b/c"), a__); - EXPECT_EQ(LookupNoBindings("GET", "/b/c"), b_); + EXPECT_EQ(Lookup("GET", "/a/b"), a__); + EXPECT_EQ(Lookup("GET", "/a/b/c"), a__); + EXPECT_EQ(Lookup("GET", "/b/c"), b_); - EXPECT_EQ(LookupNoBindings("GET", "b/c/d"), nullptr); - EXPECT_EQ(LookupNoBindings("GET", "/c/u/d/v"), c_d__); - EXPECT_EQ(LookupNoBindings("GET", "/c/v/d/w/x"), c_d__); - EXPECT_EQ(LookupNoBindings("GET", "/c/x/y/d/z"), nullptr); - EXPECT_EQ(LookupNoBindings("GET", "/c//v/d/w/x"), nullptr); + EXPECT_EQ(Lookup("GET", "b/c/d"), nullptr); + EXPECT_EQ(Lookup("GET", "/c/u/d/v"), c_d__); + EXPECT_EQ(Lookup("GET", "/c/v/d/w/x"), c_d__); + EXPECT_EQ(Lookup("GET", "/c/x/y/d/z"), nullptr); + EXPECT_EQ(Lookup("GET", "/c//v/d/w/x"), nullptr); // Test that more specific match overrides wildcard "**"" match. - EXPECT_EQ(LookupNoBindings("GET", "/c/x/d/e"), c_de); + EXPECT_EQ(Lookup("GET", "/c/x/d/e"), c_de); // Test that more specific match overrides wildcard "*"" match. - EXPECT_EQ(LookupNoBindings("GET", "/c/f/d/e"), cfde); + EXPECT_EQ(Lookup("GET", "/c/f/d/e"), cfde); } TEST_F(PathMatcherTest, WildCardMethodMatches) { @@ -210,302 +113,12 @@ TEST_F(PathMatcherTest, WildCardMethodMatches) { std::vector all_methods{"GET", "POST", "DELETE", "PATCH", "PUT"}; for (const auto& method : all_methods) { - EXPECT_EQ(LookupNoBindings(method, "/a/b"), a__); - EXPECT_EQ(LookupNoBindings(method, "/a/b/c"), a__); - EXPECT_EQ(LookupNoBindings(method, "/b/c"), b_); - } -} - -TEST_F(PathMatcherTest, VariableBindings) { - MethodInfo* a_cde = AddGetPath("/a/{x}/c/d/e"); - MethodInfo* a_b_c = AddGetPath("/{x=a/*}/b/{y=*}/c"); - MethodInfo* ab_d__ = AddGetPath("/a/{x=b/*}/{y=d/**}"); - MethodInfo* alpha_beta__gamma = AddGetPath("/alpha/{x=*}/beta/{y=**}/gamma"); - MethodInfo* _a = AddGetPath("/{x=*}/a"); - MethodInfo* __ab = AddGetPath("/{x=**}/a/b"); - MethodInfo* ab_ = AddGetPath("/a/b/{x=*}"); - MethodInfo* abc__ = AddGetPath("/a/b/c/{x=**}"); - MethodInfo* _def__ = AddGetPath("/{x=*}/d/e/f/{y=**}"); - Build(); - - EXPECT_NE(nullptr, a_cde); - EXPECT_NE(nullptr, a_b_c); - EXPECT_NE(nullptr, ab_d__); - EXPECT_NE(nullptr, alpha_beta__gamma); - EXPECT_NE(nullptr, _a); - EXPECT_NE(nullptr, __ab); - EXPECT_NE(nullptr, ab_); - EXPECT_NE(nullptr, abc__); - EXPECT_NE(nullptr, _def__); - - Bindings bindings; - EXPECT_EQ(Lookup("GET", "/a/book/c/d/e", &bindings), a_cde); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "book"}, - }), - bindings); - - EXPECT_EQ(Lookup("GET", "/a/hello/b/world/c", &bindings), a_b_c); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "a/hello"}, - Binding{FieldPath{"y"}, "world"}, - }), - bindings); - - EXPECT_EQ(Lookup("GET", "/a/b/zoo/d/animal/tiger", &bindings), ab_d__); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "b/zoo"}, - Binding{FieldPath{"y"}, "d/animal/tiger"}, - }), - bindings); - - EXPECT_EQ(Lookup("GET", "/alpha/dog/beta/eat/bones/gamma", &bindings), - alpha_beta__gamma); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "dog"}, - Binding{FieldPath{"y"}, "eat/bones"}, - }), - bindings); - - EXPECT_EQ(Lookup("GET", "/foo/a", &bindings), _a); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "foo"}, - }), - bindings); - - EXPECT_EQ(Lookup("GET", "/foo/bar/a/b", &bindings), __ab); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "foo/bar"}, - }), - bindings); - - EXPECT_EQ(Lookup("GET", "/a/b/foo", &bindings), ab_); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "foo"}, - }), - bindings); - - EXPECT_EQ(Lookup("GET", "/a/b/c/foo/bar/baz", &bindings), abc__); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "foo/bar/baz"}, - }), - bindings); - - EXPECT_EQ(Lookup("GET", "/foo/d/e/f/bar/baz", &bindings), _def__); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "foo"}, - Binding{FieldPath{"y"}, "bar/baz"}, - }), - bindings); -} - -TEST_F(PathMatcherTest, PercentEscapesUnescapedForSingleSegment) { - MethodInfo* a_c = AddGetPath("/a/{x}/c"); - Build(); - - EXPECT_NE(nullptr, a_c); - - Bindings bindings; - EXPECT_EQ(Lookup("GET", "/a/p%20q%2Fr/c", &bindings), a_c); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "p q/r"}, - }), - bindings); -} - -namespace { - -char HexDigit(unsigned char digit, bool uppercase) { - if (digit < 10) { - return '0' + digit; - } else if (uppercase) { - return 'A' + digit - 10; - } else { - return 'a' + digit - 10; - } -} - -} // namespace - -TEST_F(PathMatcherTest, PercentEscapesUnescapedForSingleSegmentAllAsciiChars) { - MethodInfo* a_c = AddGetPath("/{x}"); - Build(); - - EXPECT_NE(nullptr, a_c); - - for (int u = 0; u < 2; ++u) { - for (char c = 0; c < 0x7f; ++c) { - std::string path("/%"); - path += HexDigit((c & 0xf0) >> 4, 0 != u); - path += HexDigit(c & 0x0f, 0 != u); - - Bindings bindings; - EXPECT_EQ(Lookup("GET", path, &bindings), a_c); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, std::string(1, (char)c)}, - }), - bindings); - } + EXPECT_EQ(Lookup(method, "/a/b"), a__); + EXPECT_EQ(Lookup(method, "/a/b/c"), a__); + EXPECT_EQ(Lookup(method, "/b/c"), b_); } } -TEST_F(PathMatcherTest, PercentEscapesNotUnescapedForMultiSegment1) { - MethodInfo* ap_q_c = AddGetPath("/a/{x=p/*/q/*}/c"); - Build(); - - EXPECT_NE(nullptr, ap_q_c); - - Bindings bindings; - EXPECT_EQ(Lookup("GET", "/a/p/foo%20foo/q/bar%2Fbar/c", &bindings), ap_q_c); - // space (%20) is escaped, but slash (%2F) isn't. - EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "p/foo foo/q/bar%2Fbar"}}), - bindings); -} - -TEST_F(PathMatcherTest, PercentEscapesNotUnescapedForMultiSegment2) { - MethodInfo* a__c = AddGetPath("/a/{x=**}/c"); - Build(); - - EXPECT_NE(nullptr, a__c); - - Bindings bindings; - EXPECT_EQ(Lookup("GET", "/a/p/foo%20foo/q/bar%2Fbar/c", &bindings), a__c); - // space (%20) is escaped, but slash (%2F) isn't. - EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "p/foo foo/q/bar%2Fbar"}}), - bindings); -} - -TEST_F(PathMatcherTest, OnlyUnreservedCharsAreUnescapedForMultiSegmentMatch) { - MethodInfo* a__c = AddGetPath("/a/{x=**}/c"); - Build(); - - EXPECT_NE(nullptr, a__c); - - Bindings bindings; - EXPECT_EQ( - Lookup("GET", - "/a/%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D/c", - &bindings), - a__c); - - // All %XX are reserved characters, they should be intact. - EXPECT_EQ(Bindings({Binding{ - FieldPath{"x"}, - "%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D"}}), - bindings); -} - -TEST_F(PathMatcherTest, VariableBindingsWithCustomVerb) { - MethodInfo* a_verb = AddGetPath("/a/{y=*}:verb"); - MethodInfo* ad__verb = AddGetPath("/a/{y=d/**}:verb"); - MethodInfo* _averb = AddGetPath("/{x=*}/a:verb"); - MethodInfo* __bverb = AddGetPath("/{x=**}/b:verb"); - MethodInfo* e_fverb = AddGetPath("/e/{x=*}/f:verb"); - MethodInfo* g__hverb = AddGetPath("/g/{x=**}/h:verb"); - Build(); - - EXPECT_NE(nullptr, a_verb); - EXPECT_NE(nullptr, ad__verb); - EXPECT_NE(nullptr, _averb); - EXPECT_NE(nullptr, __bverb); - EXPECT_NE(nullptr, e_fverb); - EXPECT_NE(nullptr, g__hverb); - - Bindings bindings; - EXPECT_EQ(Lookup("GET", "/a/world:verb", &bindings), a_verb); - EXPECT_EQ(Bindings({Binding{FieldPath{"y"}, "world"}}), bindings); - - EXPECT_EQ(Lookup("GET", "/a/d/animal/tiger:verb", &bindings), ad__verb); - EXPECT_EQ(Bindings({Binding{FieldPath{"y"}, "d/animal/tiger"}}), bindings); - - EXPECT_EQ(Lookup("GET", "/foo/a:verb", &bindings), _averb); - EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "foo"}}), bindings); - - EXPECT_EQ(Lookup("GET", "/foo/bar/baz/b:verb", &bindings), __bverb); - EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "foo/bar/baz"}}), bindings); - - EXPECT_EQ(Lookup("GET", "/e/foo/f:verb", &bindings), e_fverb); - EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "foo"}}), bindings); - - EXPECT_EQ(Lookup("GET", "/g/foo/bar/h:verb", &bindings), g__hverb); - EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "foo/bar"}}), bindings); -} - -TEST_F(PathMatcherTest, ConstantSuffixesWithVariable) { - MethodInfo* ab__ = AddGetPath("/a/{x=b/**}"); - MethodInfo* ab__z = AddGetPath("/a/{x=b/**}/z"); - MethodInfo* ab__yz = AddGetPath("/a/{x=b/**}/y/z"); - MethodInfo* ab__verb = AddGetPath("/a/{x=b/**}:verb"); - MethodInfo* a__ = AddGetPath("/a/{x=**}"); - MethodInfo* c_d__e = AddGetPath("/c/{x=*}/{y=d/**}/e"); - MethodInfo* c_d__everb = AddGetPath("/c/{x=*}/{y=d/**}/e:verb"); - MethodInfo* f___g = AddGetPath("/f/{x=*}/{y=**}/g"); - MethodInfo* f___gverb = AddGetPath("/f/{x=*}/{y=**}/g:verb"); - MethodInfo* ab_yz__foo = AddGetPath("/a/{x=b/*/y/z/**}/foo"); - MethodInfo* ab___yzfoo = AddGetPath("/a/{x=b/*/**/y/z}/foo"); - Build(); - - EXPECT_NE(nullptr, ab__); - EXPECT_NE(nullptr, ab__z); - EXPECT_NE(nullptr, ab__yz); - EXPECT_NE(nullptr, ab__verb); - EXPECT_NE(nullptr, c_d__e); - EXPECT_NE(nullptr, c_d__everb); - EXPECT_NE(nullptr, f___g); - EXPECT_NE(nullptr, f___gverb); - EXPECT_NE(nullptr, ab_yz__foo); - EXPECT_NE(nullptr, ab___yzfoo); - - Bindings bindings; - - EXPECT_EQ(Lookup("GET", "/a/b/hello/world/c", &bindings), ab__); - EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "b/hello/world/c"}}), bindings); - - EXPECT_EQ(Lookup("GET", "/a/b/world/c/z", &bindings), ab__z); - EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "b/world/c"}}), bindings); - - EXPECT_EQ(Lookup("GET", "/a/b/world/c/y/z", &bindings), ab__yz); - EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "b/world/c"}}), bindings); - - EXPECT_EQ(Lookup("GET", "/a/b/world/c:verb", &bindings), ab__verb); - EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "b/world/c"}}), bindings); - - EXPECT_EQ(Lookup("GET", "/a/hello/b/world/c", &bindings), a__); - EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "hello/b/world/c"}}), bindings); - - EXPECT_EQ(Lookup("GET", "/c/hello/d/esp/world/e", &bindings), c_d__e); - EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "hello"}, - Binding{FieldPath{"y"}, "d/esp/world"}}), - bindings); - - EXPECT_EQ(Lookup("GET", "/c/hola/d/esp/mundo/e:verb", &bindings), c_d__everb); - EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "hola"}, - Binding{FieldPath{"y"}, "d/esp/mundo"}}), - bindings); - - EXPECT_EQ(Lookup("GET", "/f/foo/bar/baz/g", &bindings), f___g); - EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "foo"}, - Binding{FieldPath{"y"}, "bar/baz"}}), - bindings); - - EXPECT_EQ(Lookup("GET", "/f/foo/bar/baz/g:verb", &bindings), f___gverb); - EXPECT_EQ(Bindings({Binding{FieldPath{"x"}, "foo"}, - Binding{FieldPath{"y"}, "bar/baz"}}), - bindings); - - EXPECT_EQ(Lookup("GET", "/a/b/foo/y/z/bar/baz/foo", &bindings), ab_yz__foo); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "b/foo/y/z/bar/baz"}, - }), - bindings); - - EXPECT_EQ(Lookup("GET", "/a/b/foo/bar/baz/y/z/foo", &bindings), ab___yzfoo); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "b/foo/bar/baz/y/z"}, - }), - bindings); -} - TEST_F(PathMatcherTest, InvalidTemplates) { EXPECT_EQ(nullptr, AddGetPath("/a{x=b/**}/{y=*}")); EXPECT_EQ(nullptr, AddGetPath("/a{x=b/**}/bb/{y=*}")); @@ -532,21 +145,20 @@ TEST_F(PathMatcherTest, CustomVerbMatches) { EXPECT_NE(nullptr, other__verb); EXPECT_NE(nullptr, other__const_verb); - EXPECT_EQ(LookupNoBindings("GET", "/some/const:verb"), some_const_verb); - EXPECT_EQ(LookupNoBindings("GET", "/some/other:verb"), some__verb); - EXPECT_EQ(LookupNoBindings("GET", "/some/other:verb/"), nullptr); - EXPECT_EQ(LookupNoBindings("GET", "/some/bar/foo:verb"), some__foo_verb); - EXPECT_EQ(LookupNoBindings("GET", "/some/foo1/foo2/foo:verb"), nullptr); - EXPECT_EQ(LookupNoBindings("GET", "/some/foo/bar:verb"), nullptr); - EXPECT_EQ(LookupNoBindings("GET", "/other/bar/foo:verb"), other__verb); - EXPECT_EQ(LookupNoBindings("GET", "/other/bar/foo/const:verb"), - other__const_verb); + EXPECT_EQ(Lookup("GET", "/some/const:verb"), some_const_verb); + EXPECT_EQ(Lookup("GET", "/some/other:verb"), some__verb); + EXPECT_EQ(Lookup("GET", "/some/other:verb/"), nullptr); + EXPECT_EQ(Lookup("GET", "/some/bar/foo:verb"), some__foo_verb); + EXPECT_EQ(Lookup("GET", "/some/foo1/foo2/foo:verb"), nullptr); + EXPECT_EQ(Lookup("GET", "/some/foo/bar:verb"), nullptr); + EXPECT_EQ(Lookup("GET", "/other/bar/foo:verb"), other__verb); + EXPECT_EQ(Lookup("GET", "/other/bar/foo/const:verb"), other__const_verb); } TEST_F(PathMatcherTest, CustomVerbMatch2) { MethodInfo* verb = AddGetPath("/*/*:verb"); Build(); - EXPECT_EQ(LookupNoBindings("GET", "/some:verb/const:verb"), verb); + EXPECT_EQ(Lookup("GET", "/some:verb/const:verb"), verb); } TEST_F(PathMatcherTest, CustomVerbMatch3) { @@ -554,7 +166,7 @@ TEST_F(PathMatcherTest, CustomVerbMatch3) { Build(); // This is not custom verb since it was not configured. - EXPECT_EQ(LookupNoBindings("GET", "/foo/other:verb"), verb); + EXPECT_EQ(Lookup("GET", "/foo/other:verb"), verb); } TEST_F(PathMatcherTest, CustomVerbMatch4) { @@ -564,7 +176,7 @@ TEST_F(PathMatcherTest, CustomVerbMatch4) { EXPECT_NE(nullptr, a); // last slash is before last colon. - EXPECT_EQ(LookupNoBindings("GET", "/foo/other:verb/hello"), a); + EXPECT_EQ(Lookup("GET", "/foo/other:verb/hello"), a); } TEST_F(PathMatcherTest, RejectPartialMatches) { @@ -577,20 +189,19 @@ TEST_F(PathMatcherTest, RejectPartialMatches) { EXPECT_NE(nullptr, prefix_middle); EXPECT_NE(nullptr, prefix); - EXPECT_EQ(LookupNoBindings("GET", "/prefix/middle/suffix"), - prefix_middle_suffix); - EXPECT_EQ(LookupNoBindings("GET", "/prefix/middle"), prefix_middle); - EXPECT_EQ(LookupNoBindings("GET", "/prefix"), prefix); + EXPECT_EQ(Lookup("GET", "/prefix/middle/suffix"), prefix_middle_suffix); + EXPECT_EQ(Lookup("GET", "/prefix/middle"), prefix_middle); + EXPECT_EQ(Lookup("GET", "/prefix"), prefix); - EXPECT_EQ(LookupNoBindings("GET", "/prefix/middle/suffix/other"), nullptr); - EXPECT_EQ(LookupNoBindings("GET", "/prefix/middle/other"), nullptr); - EXPECT_EQ(LookupNoBindings("GET", "/prefix/other"), nullptr); - EXPECT_EQ(LookupNoBindings("GET", "/other"), nullptr); + EXPECT_EQ(Lookup("GET", "/prefix/middle/suffix/other"), nullptr); + EXPECT_EQ(Lookup("GET", "/prefix/middle/other"), nullptr); + EXPECT_EQ(Lookup("GET", "/prefix/other"), nullptr); + EXPECT_EQ(Lookup("GET", "/other"), nullptr); } TEST_F(PathMatcherTest, LookupReturnsNullIfMatcherEmpty) { Build(); - EXPECT_EQ(LookupNoBindings("GET", "a/b/blue/foo"), nullptr); + EXPECT_EQ(Lookup("GET", "a/b/blue/foo"), nullptr); } TEST_F(PathMatcherTest, LookupSimplePaths) { @@ -607,18 +218,17 @@ TEST_F(PathMatcherTest, LookupSimplePaths) { EXPECT_NE(nullptr, oms); EXPECT_NE(nullptr, os); - EXPECT_EQ(LookupNoBindings("GET", "/prefix/not/a/path"), nullptr); - EXPECT_EQ(LookupNoBindings("GET", "/prefix/middle"), nullptr); - EXPECT_EQ(LookupNoBindings("GET", "/prefix/not/othermiddle"), nullptr); - EXPECT_EQ(LookupNoBindings("GET", "/otherprefix/suffix/othermiddle"), - nullptr); - - EXPECT_EQ(LookupNoBindings("GET", "/prefix/middle/suffix"), pms); - EXPECT_EQ(LookupNoBindings("GET", "/prefix/middle/othersuffix"), pmo); - EXPECT_EQ(LookupNoBindings("GET", "/prefix/othermiddle/suffix"), pos); - EXPECT_EQ(LookupNoBindings("GET", "/otherprefix/middle/suffix"), oms); - EXPECT_EQ(LookupNoBindings("GET", "/otherprefix/suffix"), os); - EXPECT_EQ(LookupNoBindings("GET", "/otherprefix/suffix?foo=bar"), os); + EXPECT_EQ(Lookup("GET", "/prefix/not/a/path"), nullptr); + EXPECT_EQ(Lookup("GET", "/prefix/middle"), nullptr); + EXPECT_EQ(Lookup("GET", "/prefix/not/othermiddle"), nullptr); + EXPECT_EQ(Lookup("GET", "/otherprefix/suffix/othermiddle"), nullptr); + + EXPECT_EQ(Lookup("GET", "/prefix/middle/suffix"), pms); + EXPECT_EQ(Lookup("GET", "/prefix/middle/othersuffix"), pmo); + EXPECT_EQ(Lookup("GET", "/prefix/othermiddle/suffix"), pos); + EXPECT_EQ(Lookup("GET", "/otherprefix/middle/suffix"), oms); + EXPECT_EQ(Lookup("GET", "/otherprefix/suffix"), os); + EXPECT_EQ(Lookup("GET", "/otherprefix/suffix?foo=bar"), os); } TEST_F(PathMatcherTest, ReplacevoidForPath) { @@ -630,7 +240,7 @@ TEST_F(PathMatcherTest, ReplacevoidForPath) { Build(); // Lookup result should get the first one. - EXPECT_EQ(LookupNoBindings("GET", path), first_mock_proc); + EXPECT_EQ(Lookup("GET", path), first_mock_proc); } TEST_F(PathMatcherTest, AllowDuplicate) { @@ -640,9 +250,8 @@ TEST_F(PathMatcherTest, AllowDuplicate) { EXPECT_EQ(nullptr, AddGetPath("/a/{name}")); Build(); - Bindings bindings; // Lookup result should get the first one. - EXPECT_EQ(Lookup("GET", "/a/x", &bindings), id); + EXPECT_EQ(Lookup("GET", "/a/x"), id); } TEST_F(PathMatcherTest, DuplicatedOptions) { @@ -652,12 +261,11 @@ TEST_F(PathMatcherTest, DuplicatedOptions) { EXPECT_EQ(nullptr, AddPath("OPTIONS", "/a/{name}")); Build(); - Bindings bindings; // Lookup result should get the first one. - EXPECT_EQ(Lookup("OPTIONS", "/a/x", &bindings), options_id); + EXPECT_EQ(Lookup("OPTIONS", "/a/x"), options_id); - EXPECT_EQ(Lookup("GET", "/a/x", &bindings), get_id); - EXPECT_EQ(Lookup("POST", "/a/x", &bindings), post_name); + EXPECT_EQ(Lookup("GET", "/a/x"), get_id); + EXPECT_EQ(Lookup("POST", "/a/x"), post_name); } // If a path matches a complete branch of trie, but is longer than the branch @@ -667,119 +275,21 @@ TEST_F(PathMatcherTest, LookupReturnsNullForOverspecifiedPath) { EXPECT_NE(nullptr, AddGetPath("/a/b/c")); EXPECT_NE(nullptr, AddGetPath("/a/b")); Build(); - EXPECT_EQ(LookupNoBindings("GET", "/a/b/c/d"), nullptr); + EXPECT_EQ(Lookup("GET", "/a/b/c/d"), nullptr); } TEST_F(PathMatcherTest, ReturnNullvoidSharedPtrForUnderspecifiedPath) { EXPECT_NE(nullptr, AddGetPath("/a/b/c/d")); Build(); - EXPECT_EQ(LookupNoBindings("GET", "/a/b/c"), nullptr); + EXPECT_EQ(Lookup("GET", "/a/b/c"), nullptr); } TEST_F(PathMatcherTest, DifferentHttpMethod) { auto ab = AddGetPath("/a/b"); Build(); EXPECT_NE(nullptr, ab); - EXPECT_EQ(LookupNoBindings("GET", "/a/b"), ab); - EXPECT_EQ(LookupNoBindings("POST", "/a/b"), nullptr); -} - -TEST_F(PathMatcherTest, BodyFieldPathTest) { - auto a = AddPathWithBodyFieldPath("GET", "/a", "b"); - auto cd = AddPathWithBodyFieldPath("GET", "/c/d", "e.f.g"); - Build(); - EXPECT_NE(nullptr, a); - EXPECT_NE(nullptr, cd); - std::string body_field_path; - EXPECT_EQ(LookupWithBodyFieldPath("GET", "/a", nullptr, &body_field_path), a); - EXPECT_EQ("b", body_field_path); - EXPECT_EQ(LookupWithBodyFieldPath("GET", "/c/d", nullptr, &body_field_path), - cd); - EXPECT_EQ("e.f.g", body_field_path); -} - -TEST_F(PathMatcherTest, VariableBindingsWithQueryParams) { - MethodInfo* a = AddGetPath("/a"); - MethodInfo* a_b = AddGetPath("/a/{x}/b"); - MethodInfo* a_b_c = AddGetPath("/a/{x}/b/{y}/c"); - Build(); - - EXPECT_NE(nullptr, a); - EXPECT_NE(nullptr, a_b); - EXPECT_NE(nullptr, a_b_c); - - Bindings bindings; - EXPECT_EQ(LookupWithParams("GET", "/a", "x=hello", &bindings), a); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "hello"}, - }), - bindings); - - EXPECT_EQ(LookupWithParams("GET", "/a/book/b", "y=shelf&z=author", &bindings), - a_b); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "book"}, - Binding{FieldPath{"y"}, "shelf"}, - Binding{FieldPath{"z"}, "author"}, - }), - bindings); - - EXPECT_EQ(LookupWithParams("GET", "/a/hello/b/endpoints/c", - "z=server&t=proxy", &bindings), - a_b_c); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "hello"}, - Binding{FieldPath{"y"}, "endpoints"}, - Binding{FieldPath{"z"}, "server"}, - Binding{FieldPath{"t"}, "proxy"}, - }), - bindings); -} - -TEST_F(PathMatcherTest, VariableBindingsWithQueryParamsEncoding) { - MethodInfo* a = AddGetPath("/a"); - Build(); - - EXPECT_NE(nullptr, a); - - Bindings bindings; - EXPECT_EQ(LookupWithParams("GET", "/a", "x=Hello%20world", &bindings), a); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "Hello world"}, - }), - bindings); - - EXPECT_EQ(LookupWithParams("GET", "/a", "x=%24%25%2F%20%0A", &bindings), a); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "$%/ \n"}, - }), - bindings); -} - -TEST_F(PathMatcherTest, VariableBindingsWithQueryParamsAndSystemParams) { - std::set system_params{"key", "api_key"}; - MethodInfo* a_b = AddPathWithSystemParams("GET", "/a/{x}/b", &system_params); - Build(); - - EXPECT_NE(nullptr, a_b); - - Bindings bindings; - EXPECT_EQ(LookupWithParams("GET", "/a/hello/b", "y=world&api_key=secret", - &bindings), - a_b); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "hello"}, - Binding{FieldPath{"y"}, "world"}, - }), - bindings); - EXPECT_EQ( - LookupWithParams("GET", "/a/hello/b", "key=secret&y=world", &bindings), - a_b); - EXPECT_EQ(Bindings({ - Binding{FieldPath{"x"}, "hello"}, - Binding{FieldPath{"y"}, "world"}, - }), - bindings); + EXPECT_EQ(Lookup("GET", "/a/b"), ab); + EXPECT_EQ(Lookup("POST", "/a/b"), nullptr); } } // namespace diff --git a/src/istio/authn/BUILD b/src/istio/authn/BUILD index b87c9b7bde4..79f3c18011c 100644 --- a/src/istio/authn/BUILD +++ b/src/istio/authn/BUILD @@ -25,4 +25,5 @@ load( envoy_proto_library( name = "context_proto", srcs = ["context.proto"], + external_deps = ["well_known_protos"], ) diff --git a/src/istio/authn/context.proto b/src/istio/authn/context.proto index b9537ee8490..d23284ad000 100644 --- a/src/istio/authn/context.proto +++ b/src/istio/authn/context.proto @@ -16,6 +16,8 @@ syntax = "proto3"; package istio.authn; +import "google/protobuf/struct.proto"; + // Container to hold authenticated attributes from JWT. message JwtPayload { // This is a string of the issuer (iss) and subject (sub) claims within a @@ -33,8 +35,10 @@ message JwtPayload { // id. Example 123456789012.my-svc.com string presenter = 3; - // Only raw JWT string claims are kept. - map claims = 5; + // JWT claims stored as protobuf.Struct + // Only string and string-list claims are extracted into claims. + // A string claim is stored as a string list of one item. + google.protobuf.Struct claims = 5; // All original claims in JsonString format, which can be parsed into json // object (map) to access other claims that not cover with the string claims diff --git a/src/istio/control/client_context_base.cc b/src/istio/control/client_context_base.cc index e814fd56e00..c2ff0ef551e 100644 --- a/src/istio/control/client_context_base.cc +++ b/src/istio/control/client_context_base.cc @@ -31,6 +31,8 @@ using ::istio::mixerclient::QuotaOptions; using ::istio::mixerclient::ReportOptions; using ::istio::mixerclient::Statistics; using ::istio::mixerclient::TransportCheckFunc; +using ::istio::utils::CreateLocalAttributes; +using ::istio::utils::LocalNode; namespace istio { namespace control { @@ -69,11 +71,14 @@ ReportOptions GetReportOptions(const TransportConfig& config) { } // namespace ClientContextBase::ClientContextBase(const TransportConfig& config, - const Environment& env) { + const Environment& env, bool outbound, + const LocalNode& local_node) + : outbound_(outbound) { MixerClientOptions options(GetCheckOptions(config), GetReportOptions(config), GetQuotaOptions(config)); options.env = env; mixer_client_ = ::istio::mixerclient::CreateMixerClient(options); + CreateLocalAttributes(local_node, &local_attributes_); } CancelFunc ClientContextBase::SendCheck(TransportCheckFunc transport, @@ -85,7 +90,7 @@ CancelFunc ClientContextBase::SendCheck(TransportCheckFunc transport, // save the check status code request->check_status = check_response_info.response_status; - utils::AttributesBuilder builder(&request->attributes); + utils::AttributesBuilder builder(request->attributes); builder.AddBool(utils::AttributeName::kCheckCacheHit, check_response_info.is_check_cache_hit); builder.AddBool(utils::AttributeName::kQuotaCacheHit, @@ -95,21 +100,36 @@ CancelFunc ClientContextBase::SendCheck(TransportCheckFunc transport, // TODO: add debug message // GOOGLE_LOG(INFO) << "Check attributes: " << - // request->attributes.DebugString(); - return mixer_client_->Check(request->attributes, request->quotas, transport, + // request->attributes->DebugString(); + return mixer_client_->Check(*request->attributes, request->quotas, transport, local_on_done); } void ClientContextBase::SendReport(const RequestContext& request) { // TODO: add debug message // GOOGLE_LOG(INFO) << "Report attributes: " << - // request.attributes.DebugString(); - mixer_client_->Report(request.attributes); + // request.attributes->DebugString(); + mixer_client_->Report(*request.attributes); } void ClientContextBase::GetStatistics(Statistics* stat) const { mixer_client_->GetStatistics(stat); } +void ClientContextBase::AddLocalNodeAttributes( + ::istio::mixer::v1::Attributes* request) const { + if (outbound_) { + request->MergeFrom(local_attributes_.outbound); + } else { + request->MergeFrom(local_attributes_.inbound); + } +} + +void ClientContextBase::AddLocalNodeForwardAttribues( + ::istio::mixer::v1::Attributes* request) const { + if (outbound_) { + request->MergeFrom(local_attributes_.forward); + } +} } // namespace control } // namespace istio diff --git a/src/istio/control/client_context_base.h b/src/istio/control/client_context_base.h index 54a6a89ace4..560c090083c 100644 --- a/src/istio/control/client_context_base.h +++ b/src/istio/control/client_context_base.h @@ -17,6 +17,8 @@ #define ISTIO_CONTROL_CLIENT_CONTEXT_BASE_H #include "include/istio/mixerclient/client.h" +#include "include/istio/utils/attribute_names.h" +#include "include/istio/utils/local_attributes.h" #include "mixer/v1/config/client/client_config.pb.h" #include "request_context.h" @@ -29,12 +31,16 @@ class ClientContextBase { public: ClientContextBase( const ::istio::mixer::v1::config::client::TransportConfig& config, - const ::istio::mixerclient::Environment& env); + const ::istio::mixerclient::Environment& env, bool outbound, + const ::istio::utils::LocalNode& local_node); // A constructor for unit-test to pass in a mock mixer_client ClientContextBase( - std::unique_ptr<::istio::mixerclient::MixerClient> mixer_client) - : mixer_client_(std::move(mixer_client)) {} + std::unique_ptr<::istio::mixerclient::MixerClient> mixer_client, + bool outbound, ::istio::utils::LocalAttributes& local_attributes) + : mixer_client_(std::move(mixer_client)), + outbound_(outbound), + local_attributes_(local_attributes) {} // virtual destrutor virtual ~ClientContextBase() {} @@ -49,9 +55,20 @@ class ClientContextBase { // Get statistics. void GetStatistics(::istio::mixerclient::Statistics* stat) const; + void AddLocalNodeAttributes(::istio::mixer::v1::Attributes* request) const; + + void AddLocalNodeForwardAttribues( + ::istio::mixer::v1::Attributes* request) const; + private: // The mixer client object with check cache and report batch features. std::unique_ptr<::istio::mixerclient::MixerClient> mixer_client_; + + // If this is an outbound client context. + bool outbound_; + + // local attributes - owned by the client context. + ::istio::utils::LocalAttributes local_attributes_; }; } // namespace control diff --git a/src/istio/control/http/attributes_builder.cc b/src/istio/control/http/attributes_builder.cc index 91835c7df67..ae175749673 100644 --- a/src/istio/control/http/attributes_builder.cc +++ b/src/istio/control/http/attributes_builder.cc @@ -35,7 +35,7 @@ const std::set kGrpcContentTypes{ } // namespace void AttributesBuilder::ExtractRequestHeaderAttributes(CheckData *check_data) { - utils::AttributesBuilder builder(&request_->attributes); + utils::AttributesBuilder builder(request_->attributes); std::map headers = check_data->GetRequestHeaders(); builder.AddStringMap(utils::AttributeName::kRequestHeaders, headers); @@ -79,7 +79,7 @@ void AttributesBuilder::ExtractRequestHeaderAttributes(CheckData *check_data) { } void AttributesBuilder::ExtractAuthAttributes(CheckData *check_data) { - utils::AttributesBuilder builder(&request_->attributes); + utils::AttributesBuilder builder(request_->attributes); std::string destination_principal; if (check_data->GetPrincipal(false, &destination_principal)) { @@ -90,6 +90,7 @@ void AttributesBuilder::ExtractAuthAttributes(CheckData *check_data) { utils::AttributeName::kRequestAuthPrincipal, utils::AttributeName::kSourceUser, utils::AttributeName::kSourcePrincipal, + utils::AttributeName::kSourceNamespace, utils::AttributeName::kRequestAuthAudiences, utils::AttributeName::kRequestAuthPresenter, utils::AttributeName::kRequestAuthRawClaims, @@ -129,18 +130,37 @@ void AttributesBuilder::ExtractForwardedAttributes(CheckData *check_data) { if (!check_data->ExtractIstioAttributes(&forwarded_data)) { return; } + Attributes v2_format; - if (v2_format.ParseFromString(forwarded_data)) { - request_->attributes.MergeFrom(v2_format); + if (!v2_format.ParseFromString(forwarded_data)) { return; } + + static const std::set kForwardWhitelist = { + utils::AttributeName::kSourceUID, + utils::AttributeName::kDestinationServiceName, + utils::AttributeName::kDestinationServiceUID, + utils::AttributeName::kDestinationServiceHost, + utils::AttributeName::kDestinationServiceNamespace, + }; + + auto fwd = v2_format.attributes(); + utils::AttributesBuilder builder(request_->attributes); + for (const auto &attribute : kForwardWhitelist) { + const auto &iter = fwd.find(attribute); + if (iter != fwd.end() && !iter->second.string_value().empty()) { + builder.AddString(attribute, iter->second.string_value()); + } + } + + return; } void AttributesBuilder::ExtractCheckAttributes(CheckData *check_data) { ExtractRequestHeaderAttributes(check_data); ExtractAuthAttributes(check_data); - utils::AttributesBuilder builder(&request_->attributes); + utils::AttributesBuilder builder(request_->attributes); // connection remote IP is always reported as origin IP std::string source_ip; @@ -180,7 +200,7 @@ void AttributesBuilder::ForwardAttributes(const Attributes &forward_attributes, } void AttributesBuilder::ExtractReportAttributes(ReportData *report_data) { - utils::AttributesBuilder builder(&request_->attributes); + utils::AttributesBuilder builder(request_->attributes); std::string dest_ip; int dest_port; @@ -251,8 +271,10 @@ void AttributesBuilder::ExtractReportAttributes(ReportData *report_data) { rbac_info.permissive_policy_id); } } + + builder.FlattenMapOfStringToStruct(report_data->GetDynamicFilterState()); } } // namespace http } // namespace control -} // namespace istio +} // namespace istio \ No newline at end of file diff --git a/src/istio/control/http/attributes_builder.h b/src/istio/control/http/attributes_builder.h index a8346f7f5ec..365447452f6 100644 --- a/src/istio/control/http/attributes_builder.h +++ b/src/istio/control/http/attributes_builder.h @@ -60,4 +60,4 @@ class AttributesBuilder { } // namespace control } // namespace istio -#endif // ISTIO_CONTROL_HTTP_ATTRIBUTES_BUILDER_H +#endif // ISTIO_CONTROL_HTTP_ATTRIBUTES_BUILDER_H \ No newline at end of file diff --git a/src/istio/control/http/attributes_builder_test.cc b/src/istio/control/http/attributes_builder_test.cc index 305fac3b44b..a5aa7d589ef 100644 --- a/src/istio/control/http/attributes_builder_test.cc +++ b/src/istio/control/http/attributes_builder_test.cc @@ -31,6 +31,7 @@ using ::istio::mixer::v1::Attributes_StringMap; using ::testing::_; using ::testing::Invoke; +using ::testing::ReturnRef; namespace istio { namespace control { @@ -139,7 +140,7 @@ attributes { attributes { key: "source.principal" value { - string_value: "test_user" + string_value: "sa/test_user/ns/ns_ns/" } } )"; @@ -224,16 +225,22 @@ attributes { string_value: "www.google.com" } } +attributes { + key: "source.namespace" + value { + string_value: "ns_ns" + } +} attributes { key: "source.principal" value { - string_value: "test_user" + string_value: "sa/test_user/ns/ns_ns/" } } attributes { key: "source.user" value { - string_value: "test_user" + string_value: "sa/test_user/ns/ns_ns/" } } attributes { @@ -400,6 +407,21 @@ attributes { string_value: "policy-foo" } } +attributes { + key: "foo.bar.com" + value { + string_map_value { + entries { + key: "str" + value: "abc" + } + entries { + key: "list" + value: "a,b,c" + } + } + } +} )"; constexpr char kAuthenticationResultStruct[] = R"( @@ -489,35 +511,41 @@ fields { string_value: "test_raw_claims" } } +fields { + key: "source.namespace" + value { + string_value: "ns_ns" + } +} fields { key: "source.principal" value { - string_value: "test_user" + string_value: "sa/test_user/ns/ns_ns/" } } fields { key: "source.user" value { - string_value: "test_user" + string_value: "sa/test_user/ns/ns_ns/" } } )"; void ClearContextTime(const std::string &name, RequestContext *request) { // Override timestamp with - - utils::AttributesBuilder builder(&request->attributes); + utils::AttributesBuilder builder(request->attributes); std::chrono::time_point time0; builder.AddTimestamp(name, time0); } void SetDestinationIp(RequestContext *request, const std::string &ip) { - utils::AttributesBuilder builder(&request->attributes); + utils::AttributesBuilder builder(request->attributes); builder.AddBytes(utils::AttributeName::kDestinationIp, ip); } TEST(AttributesBuilderTest, TestExtractForwardedAttributes) { Attributes attr; - (*attr.mutable_attributes())["test_key"].set_string_value("test_value"); + (*attr.mutable_attributes())["source.uid"].set_string_value("test_value"); ::testing::StrictMock mock_data; EXPECT_CALL(mock_data, ExtractIstioAttributes(_)) @@ -529,7 +557,7 @@ TEST(AttributesBuilderTest, TestExtractForwardedAttributes) { RequestContext request; AttributesBuilder builder(&request); builder.ExtractForwardedAttributes(&mock_data); - EXPECT_THAT(request.attributes, EqualsAttribute(attr)); + EXPECT_THAT(*request.attributes, EqualsAttribute(attr)); } TEST(AttributesBuilderTest, TestForwardAttributes) { @@ -556,7 +584,7 @@ TEST(AttributesBuilderTest, TestCheckAttributesWithoutAuthnFilter) { EXPECT_CALL(mock_data, GetPrincipal(_, _)) .WillRepeatedly(Invoke([](bool peer, std::string *user) -> bool { if (peer) { - *user = "test_user"; + *user = "sa/test_user/ns/ns_ns/"; } else { *user = "destination_user"; } @@ -619,7 +647,7 @@ TEST(AttributesBuilderTest, TestCheckAttributesWithoutAuthnFilter) { Attributes expected_attributes; ASSERT_TRUE(TextFormat::ParseFromString(kCheckAttributesWithoutAuthnFilter, &expected_attributes)); - EXPECT_THAT(request.attributes, EqualsAttribute(expected_attributes)); + EXPECT_THAT(*request.attributes, EqualsAttribute(expected_attributes)); } TEST(AttributesBuilderTest, TestCheckAttributes) { @@ -630,7 +658,7 @@ TEST(AttributesBuilderTest, TestCheckAttributes) { EXPECT_CALL(mock_data, GetPrincipal(_, _)) .WillRepeatedly(Invoke([](bool peer, std::string *user) -> bool { if (peer) { - *user = "test_user"; + *user = "sa/test_user/ns/ns_ns/"; } else { *user = "destination_user"; } @@ -693,11 +721,30 @@ TEST(AttributesBuilderTest, TestCheckAttributes) { Attributes expected_attributes; ASSERT_TRUE( TextFormat::ParseFromString(kCheckAttributes, &expected_attributes)); - EXPECT_THAT(request.attributes, EqualsAttribute(expected_attributes)); + EXPECT_THAT(*request.attributes, EqualsAttribute(expected_attributes)); } +/* TEST(AttributesBuilderTest, TestReportAttributes) { ::testing::StrictMock mock_data; + + ::google::protobuf::Map + filter_metadata; + ::google::protobuf::Struct struct_obj; + ::google::protobuf::Value strval, numval, boolval, listval; + strval.set_string_value("abc"); + (*struct_obj.mutable_fields())["str"] = strval; + numval.set_number_value(12.3); + (*struct_obj.mutable_fields())["num"] = numval; + boolval.set_bool_value(true); + (*struct_obj.mutable_fields())["bool"] = boolval; + listval.mutable_list_value()->add_values()->set_string_value("a"); + listval.mutable_list_value()->add_values()->set_string_value("b"); + listval.mutable_list_value()->add_values()->set_string_value("c"); + (*struct_obj.mutable_fields())["list"] = listval; + filter_metadata["foo.bar.com"] = struct_obj; + filter_metadata["istio.mixer"] = struct_obj; // to be ignored + EXPECT_CALL(mock_data, GetDestinationIpPort(_, _)) .WillOnce(Invoke([](std::string *ip, int *port) -> bool { *ip = "1.2.3.4"; @@ -738,6 +785,8 @@ TEST(AttributesBuilderTest, TestReportAttributes) { report_info->permissive_policy_id = "policy-foo"; return true; })); + EXPECT_CALL(mock_data, GetDynamicFilterState()) + .WillOnce(ReturnRef(filter_metadata)); RequestContext request; AttributesBuilder builder(&request); @@ -757,11 +806,29 @@ TEST(AttributesBuilderTest, TestReportAttributes) { (*expected_attributes .mutable_attributes())[utils::AttributeName::kResponseGrpcMessage] .set_string_value("grpc-message"); - EXPECT_THAT(request.attributes, EqualsAttribute(expected_attributes)); + EXPECT_THAT(*request.attributes, EqualsAttribute(expected_attributes)); } +*/ TEST(AttributesBuilderTest, TestReportAttributesWithDestIP) { ::testing::StrictMock mock_data; + + ::google::protobuf::Map + filter_metadata; + ::google::protobuf::Struct struct_obj; + ::google::protobuf::Value strval, numval, boolval, listval; + strval.set_string_value("abc"); + (*struct_obj.mutable_fields())["str"] = strval; + numval.set_number_value(12.3); + (*struct_obj.mutable_fields())["num"] = numval; + boolval.set_bool_value(true); + (*struct_obj.mutable_fields())["bool"] = boolval; + listval.mutable_list_value()->add_values()->set_string_value("a"); + listval.mutable_list_value()->add_values()->set_string_value("b"); + listval.mutable_list_value()->add_values()->set_string_value("c"); + (*struct_obj.mutable_fields())["list"] = listval; + filter_metadata["foo.bar.com"] = struct_obj; + EXPECT_CALL(mock_data, GetDestinationIpPort(_, _)) .WillOnce(Invoke([](std::string *ip, int *port) -> bool { *ip = "2.3.4.5"; @@ -793,6 +860,8 @@ TEST(AttributesBuilderTest, TestReportAttributesWithDestIP) { report_info->permissive_policy_id = "policy-foo"; return true; })); + EXPECT_CALL(mock_data, GetDynamicFilterState()) + .WillOnce(ReturnRef(filter_metadata)); RequestContext request; SetDestinationIp(&request, "1.2.3.4"); @@ -804,10 +873,10 @@ TEST(AttributesBuilderTest, TestReportAttributesWithDestIP) { Attributes expected_attributes; ASSERT_TRUE( TextFormat::ParseFromString(kReportAttributes, &expected_attributes)); - EXPECT_THAT(request.attributes, EqualsAttribute(expected_attributes)); + EXPECT_THAT(*request.attributes, EqualsAttribute(expected_attributes)); } } // namespace } // namespace http } // namespace control -} // namespace istio +} // namespace istio \ No newline at end of file diff --git a/src/istio/control/http/client_context.cc b/src/istio/control/http/client_context.cc index 02b108ea89f..e3acf7fc895 100644 --- a/src/istio/control/http/client_context.cc +++ b/src/istio/control/http/client_context.cc @@ -14,23 +14,30 @@ */ #include "src/istio/control/http/client_context.h" +#include "include/istio/utils/attribute_names.h" +using ::istio::mixer::v1::Attributes_AttributeValue; using ::istio::mixer::v1::config::client::ServiceConfig; +using ::istio::utils::AttributeName; namespace istio { namespace control { namespace http { ClientContext::ClientContext(const Controller::Options& data) - : ClientContextBase(data.config.transport(), data.env), + : ClientContextBase( + data.config.transport(), data.env, + ::istio::utils::IsOutbound(data.config.mixer_attributes()), + data.local_node), config_(data.config), service_config_cache_size_(data.service_config_cache_size) {} ClientContext::ClientContext( std::unique_ptr<::istio::mixerclient::MixerClient> mixer_client, const ::istio::mixer::v1::config::client::HttpClientConfig& config, - int service_config_cache_size) - : ClientContextBase(std::move(mixer_client)), + int service_config_cache_size, + ::istio::utils::LocalAttributes& local_attributes, bool outbound) + : ClientContextBase(std::move(mixer_client), outbound, local_attributes), config_(config), service_config_cache_size_(service_config_cache_size) {} diff --git a/src/istio/control/http/client_context.h b/src/istio/control/http/client_context.h index c7966078a14..acb5609f192 100644 --- a/src/istio/control/http/client_context.h +++ b/src/istio/control/http/client_context.h @@ -17,6 +17,8 @@ #define ISTIO_CONTROL_HTTP_CLIENT_CONTEXT_H #include "include/istio/control/http/controller.h" +#include "include/istio/utils/local_attributes.h" +#include "mixer/v1/attributes.pb.h" #include "src/istio/control/client_context_base.h" namespace istio { @@ -33,7 +35,8 @@ class ClientContext : public ClientContextBase { ClientContext( std::unique_ptr<::istio::mixerclient::MixerClient> mixer_client, const ::istio::mixer::v1::config::client::HttpClientConfig& config, - int service_config_cache_size); + int service_config_cache_size, + ::istio::utils::LocalAttributes& local_attributes, bool outbound); // Retrieve mixer client config. const ::istio::mixer::v1::config::client::HttpClientConfig& config() const { diff --git a/src/istio/control/http/controller_impl.h b/src/istio/control/http/controller_impl.h index 090e4b8cf05..08269ed8e07 100644 --- a/src/istio/control/http/controller_impl.h +++ b/src/istio/control/http/controller_impl.h @@ -20,6 +20,7 @@ #include #include "include/istio/control/http/controller.h" +#include "include/istio/utils/attribute_names.h" #include "include/istio/utils/simple_lru_cache.h" #include "include/istio/utils/simple_lru_cache_inl.h" #include "src/istio/control/http/client_context.h" diff --git a/src/istio/control/http/mock_report_data.h b/src/istio/control/http/mock_report_data.h index 423a1b6ef4f..64c0402ef9b 100644 --- a/src/istio/control/http/mock_report_data.h +++ b/src/istio/control/http/mock_report_data.h @@ -27,11 +27,15 @@ namespace http { class MockReportData : public ReportData { public: MOCK_CONST_METHOD0(GetResponseHeaders, std::map()); - MOCK_CONST_METHOD1(GetReportInfo, void(ReportInfo* info)); - MOCK_CONST_METHOD2(GetDestinationIpPort, bool(std::string* ip, int* port)); - MOCK_CONST_METHOD1(GetDestinationUID, bool(std::string* ip)); - MOCK_CONST_METHOD1(GetGrpcStatus, bool(GrpcStatus* status)); - MOCK_CONST_METHOD1(GetRbacReportInfo, bool(RbacReportInfo* info)); + MOCK_CONST_METHOD1(GetReportInfo, void(ReportInfo *info)); + MOCK_CONST_METHOD2(GetDestinationIpPort, bool(std::string *ip, int *port)); + MOCK_CONST_METHOD1(GetDestinationUID, bool(std::string *ip)); + MOCK_CONST_METHOD1(GetGrpcStatus, bool(GrpcStatus *status)); + MOCK_CONST_METHOD1(GetRbacReportInfo, bool(RbacReportInfo *info)); + MOCK_CONST_METHOD0( + GetDynamicFilterState, + const ::google::protobuf::Map + &()); }; } // namespace http diff --git a/src/istio/control/http/request_handler_impl.cc b/src/istio/control/http/request_handler_impl.cc index 9b9ca09d924..56bb7c4ed7a 100644 --- a/src/istio/control/http/request_handler_impl.cc +++ b/src/istio/control/http/request_handler_impl.cc @@ -29,15 +29,31 @@ namespace http { RequestHandlerImpl::RequestHandlerImpl( std::shared_ptr service_context) - : service_context_(service_context) {} + : service_context_(service_context), + check_attributes_added_(false), + forward_attributes_added_(false) {} + +void RequestHandlerImpl::AddForwardAttributes(CheckData* check_data) { + if (forward_attributes_added_) { + return; + } + forward_attributes_added_ = true; + + AttributesBuilder builder(&request_context_); + builder.ExtractForwardedAttributes(check_data); +} + +void RequestHandlerImpl::AddCheckAttributes(CheckData* check_data) { + if (check_attributes_added_) { + return; + } + check_attributes_added_ = true; -void RequestHandlerImpl::ExtractRequestAttributes(CheckData* check_data) { if (service_context_->enable_mixer_check() || service_context_->enable_mixer_report()) { service_context_->AddStaticAttributes(&request_context_); AttributesBuilder builder(&request_context_); - builder.ExtractForwardedAttributes(check_data); builder.ExtractCheckAttributes(check_data); service_context_->AddApiAttributes(check_data, &request_context_); @@ -48,7 +64,10 @@ CancelFunc RequestHandlerImpl::Check(CheckData* check_data, HeaderUpdate* header_update, TransportCheckFunc transport, CheckDoneFunc on_done) { - ExtractRequestAttributes(check_data); + // Forwarded attributes need to be stored regardless Check is needed + // or not since the header will be updated or removed. + AddCheckAttributes(check_data); + AddForwardAttributes(check_data); header_update->RemoveIstioAttributes(); service_context_->InjectForwardedAttributes(header_update); @@ -66,10 +85,15 @@ CancelFunc RequestHandlerImpl::Check(CheckData* check_data, } // Make remote report call. -void RequestHandlerImpl::Report(ReportData* report_data) { +void RequestHandlerImpl::Report(CheckData* check_data, + ReportData* report_data) { if (!service_context_->enable_mixer_report()) { return; } + + AddForwardAttributes(check_data); + AddCheckAttributes(check_data); + AttributesBuilder builder(&request_context_); builder.ExtractReportAttributes(report_data); diff --git a/src/istio/control/http/request_handler_impl.h b/src/istio/control/http/request_handler_impl.h index e858094a724..0e8c2b28072 100644 --- a/src/istio/control/http/request_handler_impl.h +++ b/src/istio/control/http/request_handler_impl.h @@ -37,16 +37,24 @@ class RequestHandlerImpl : public RequestHandler { ::istio::mixerclient::CheckDoneFunc on_done) override; // Make a Report call. - void Report(ReportData* report_data) override; - - void ExtractRequestAttributes(CheckData* check_data) override; + void Report(CheckData* check_data, ReportData* report_data) override; private: + // Add Forward attributes, allow re-entry + void AddForwardAttributes(CheckData* check_data); + // Add check attributes, allow re-entry + void AddCheckAttributes(CheckData* check_data); + // The request context object. RequestContext request_context_; // The service context. std::shared_ptr service_context_; + + // If true, request attributes are added + bool check_attributes_added_; + // If true, forward attributes are added + bool forward_attributes_added_; }; } // namespace http diff --git a/src/istio/control/http/request_handler_impl_test.cc b/src/istio/control/http/request_handler_impl_test.cc index dcbbaf3d49f..325614e5f1a 100644 --- a/src/istio/control/http/request_handler_impl_test.cc +++ b/src/istio/control/http/request_handler_impl_test.cc @@ -34,14 +34,44 @@ using ::istio::mixerclient::DoneFunc; using ::istio::mixerclient::MixerClient; using ::istio::mixerclient::TransportCheckFunc; using ::istio::quota_config::Requirement; +using ::istio::utils::LocalAttributes; using ::testing::_; using ::testing::Invoke; +using ::testing::ReturnRef; namespace istio { namespace control { namespace http { +// local inbound +const char kLocalInbound[] = R"( +attributes { + key: "destination.uid" + value { + string_value: "kubernetes://dest-client-84469dc8d7-jbbxt.default" + } +} +)"; + +const char kLocalOutbound[] = R"( +attributes { + key: "source.uid" + value { + string_value: "kubernetes://src-client-84469dc8d7-jbbxt.default" + } +} +)"; + +const char kLocalForward[] = R"( +attributes { + key: "source.uid" + value { + string_value: "kubernetes://client-84469dc8d7-jbbxt.default" + } +} +)"; + // The default client config const char kDefaultClientConfig[] = R"( service_configs { @@ -98,33 +128,60 @@ forward_attributes { class RequestHandlerImplTest : public ::testing::Test { public: + RequestHandlerImplTest(bool outbound = false) : outbound_(outbound) {} void SetUp() { SetUpMockController(kDefaultClientConfig); } - void SetUpMockController(const std::string& config_text) { + void SetUpMockController(const std::string &config_text) { + SetUpMockController(config_text, kLocalInbound, kLocalOutbound, + kLocalForward); + } + + void SetUpMockController(const std::string &config_text, + const std::string &local_inbound_attributes, + const std::string &local_outbound_attributes, + const std::string &local_forward_attributes) { ASSERT_TRUE(TextFormat::ParseFromString(config_text, &client_config_)); + LocalAttributes la; + ASSERT_TRUE( + TextFormat::ParseFromString(local_inbound_attributes, &la.inbound)); + ASSERT_TRUE( + TextFormat::ParseFromString(local_outbound_attributes, &la.outbound)); + ASSERT_TRUE( + TextFormat::ParseFromString(local_forward_attributes, &la.forward)); + mock_client_ = new ::testing::NiceMock; // set LRU cache size is 3 + client_context_ = std::make_shared( - std::unique_ptr(mock_client_), client_config_, 3); + std::unique_ptr(mock_client_), client_config_, 3, la, + outbound_); controller_ = std::unique_ptr(new ControllerImpl(client_context_)); } - void SetServiceConfig(const std::string& name, const ServiceConfig& config) { + void SetServiceConfig(const std::string &name, const ServiceConfig &config) { (*client_config_.mutable_service_configs())[name] = config; } - void ApplyPerRouteConfig(const ServiceConfig& service_config, - Controller::PerRouteConfig* per_route) { + void ApplyPerRouteConfig(const ServiceConfig &service_config, + Controller::PerRouteConfig *per_route) { per_route->service_config_id = "1111"; controller_->AddServiceConfig(per_route->service_config_id, service_config); } std::shared_ptr client_context_; HttpClientConfig client_config_; - ::testing::NiceMock* mock_client_; + ::testing::NiceMock *mock_client_; std::unique_ptr controller_; + + private: + bool outbound_; +}; + +class OutboundRequestHandlerImplTest : public RequestHandlerImplTest { + public: + OutboundRequestHandlerImplTest() : RequestHandlerImplTest(true) {} }; TEST_F(RequestHandlerImplTest, TestServiceConfigManage) { @@ -163,7 +220,7 @@ TEST_F(RequestHandlerImplTest, TestHandlerDisabledCheckReport) { auto handler = controller_->CreateRequestHandler(per_route); handler->Check(&mock_data, &mock_header, nullptr, - [](const CheckResponseInfo& info) { + [](const CheckResponseInfo &info) { EXPECT_TRUE(info.response_status.ok()); }); } @@ -171,7 +228,7 @@ TEST_F(RequestHandlerImplTest, TestHandlerDisabledCheckReport) { TEST_F(RequestHandlerImplTest, TestHandlerDisabledCheck) { ::testing::NiceMock mock_data; ::testing::NiceMock mock_header; - // Report is enabled so Attributes are extracted. + // Report is enabled so Check Attributes are extracted but not sent. EXPECT_CALL(mock_data, GetSourceIpPort(_, _)).Times(1); EXPECT_CALL(mock_data, GetPrincipal(_, _)).Times(2); @@ -185,7 +242,7 @@ TEST_F(RequestHandlerImplTest, TestHandlerDisabledCheck) { auto handler = controller_->CreateRequestHandler(per_route); handler->Check(&mock_data, &mock_header, nullptr, - [](const CheckResponseInfo& info) { + [](const CheckResponseInfo &info) { EXPECT_TRUE(info.response_status.ok()); }); } @@ -198,8 +255,8 @@ TEST_F(RequestHandlerImplTest, TestPerRouteAttributes) { // Check should be called. EXPECT_CALL(*mock_client_, Check(_, _, _, _)) - .WillOnce(Invoke([](const Attributes& attributes, - const std::vector& quotas, + .WillOnce(Invoke([](const Attributes &attributes, + const std::vector "as, TransportCheckFunc transport, CheckDoneFunc on_done) -> CancelFunc { auto map = attributes.attributes(); @@ -226,8 +283,8 @@ TEST_F(RequestHandlerImplTest, TestDefaultRouteAttributes) { // Check should be called. EXPECT_CALL(*mock_client_, Check(_, _, _, _)) - .WillOnce(Invoke([](const Attributes& attributes, - const std::vector& quotas, + .WillOnce(Invoke([](const Attributes &attributes, + const std::vector "as, TransportCheckFunc transport, CheckDoneFunc on_done) -> CancelFunc { auto map = attributes.attributes(); @@ -238,7 +295,7 @@ TEST_F(RequestHandlerImplTest, TestDefaultRouteAttributes) { // Attribute is forwarded: route override EXPECT_CALL(mock_header, AddIstioAttributes(_)) - .WillOnce(Invoke([](const std::string& data) { + .WillOnce(Invoke([](const std::string &data) { Attributes forwarded_attr; EXPECT_TRUE(forwarded_attr.ParseFromString(data)); auto map = forwarded_attr.attributes(); @@ -265,8 +322,8 @@ TEST_F(RequestHandlerImplTest, TestRouteAttributes) { // Check should be called. EXPECT_CALL(*mock_client_, Check(_, _, _, _)) - .WillOnce(Invoke([](const Attributes& attributes, - const std::vector& quotas, + .WillOnce(Invoke([](const Attributes &attributes, + const std::vector "as, TransportCheckFunc transport, CheckDoneFunc on_done) -> CancelFunc { auto map = attributes.attributes(); @@ -277,7 +334,7 @@ TEST_F(RequestHandlerImplTest, TestRouteAttributes) { // Attribute is forwarded: global EXPECT_CALL(mock_header, AddIstioAttributes(_)) - .WillOnce(Invoke([](const std::string& data) { + .WillOnce(Invoke([](const std::string &data) { Attributes forwarded_attr; EXPECT_TRUE(forwarded_attr.ParseFromString(data)); auto map = forwarded_attr.attributes(); @@ -296,8 +353,8 @@ TEST_F(RequestHandlerImplTest, TestPerRouteQuota) { // Check should be called. EXPECT_CALL(*mock_client_, Check(_, _, _, _)) - .WillOnce(Invoke([](const Attributes& attributes, - const std::vector& quotas, + .WillOnce(Invoke([](const Attributes &attributes, + const std::vector "as, TransportCheckFunc transport, CheckDoneFunc on_done) -> CancelFunc { auto map = attributes.attributes(); @@ -324,7 +381,7 @@ TEST_F(RequestHandlerImplTest, TestPerRouteApiSpec) { ::testing::NiceMock mock_header; EXPECT_CALL(mock_data, FindHeaderByType(_, _)) .WillRepeatedly( - Invoke([](CheckData::HeaderType type, std::string* value) -> bool { + Invoke([](CheckData::HeaderType type, std::string *value) -> bool { if (type == CheckData::HEADER_PATH) { *value = "/books/120"; return true; @@ -338,8 +395,8 @@ TEST_F(RequestHandlerImplTest, TestPerRouteApiSpec) { // Check should be called. EXPECT_CALL(*mock_client_, Check(_, _, _, _)) - .WillOnce(Invoke([](const Attributes& attributes, - const std::vector& quotas, + .WillOnce(Invoke([](const Attributes &attributes, + const std::vector "as, TransportCheckFunc transport, CheckDoneFunc on_done) -> CancelFunc { auto map = attributes.attributes(); @@ -388,7 +445,7 @@ TEST_F(RequestHandlerImplTest, TestDefaultApiKey) { ::testing::NiceMock mock_header; EXPECT_CALL(mock_data, FindQueryParameter(_, _)) .WillRepeatedly( - Invoke([](const std::string& name, std::string* value) -> bool { + Invoke([](const std::string &name, std::string *value) -> bool { if (name == "key") { *value = "test-api-key"; return true; @@ -398,8 +455,8 @@ TEST_F(RequestHandlerImplTest, TestDefaultApiKey) { // Check should be called. EXPECT_CALL(*mock_client_, Check(_, _, _, _)) - .WillOnce(Invoke([](const Attributes& attributes, - const std::vector& quotas, + .WillOnce(Invoke([](const Attributes &attributes, + const std::vector "as, TransportCheckFunc transport, CheckDoneFunc on_done) -> CancelFunc { auto map = attributes.attributes(); @@ -415,9 +472,16 @@ TEST_F(RequestHandlerImplTest, TestDefaultApiKey) { } TEST_F(RequestHandlerImplTest, TestHandlerReport) { - ::testing::NiceMock mock_data; - EXPECT_CALL(mock_data, GetResponseHeaders()).Times(1); - EXPECT_CALL(mock_data, GetReportInfo(_)).Times(1); + ::testing::NiceMock mock_check; + ::testing::NiceMock mock_report; + ::google::protobuf::Map + filter_metadata; + EXPECT_CALL(mock_check, GetSourceIpPort(_, _)).Times(1); + EXPECT_CALL(mock_report, GetResponseHeaders()).Times(1); + EXPECT_CALL(mock_report, GetReportInfo(_)).Times(1); + EXPECT_CALL(mock_report, GetDynamicFilterState()) + .Times(1) + .WillOnce(ReturnRef(filter_metadata)); // Report should be called. EXPECT_CALL(*mock_client_, Report(_)).Times(1); @@ -427,13 +491,16 @@ TEST_F(RequestHandlerImplTest, TestHandlerReport) { ApplyPerRouteConfig(config, &per_route); auto handler = controller_->CreateRequestHandler(per_route); - handler->Report(&mock_data); + handler->Report(&mock_check, &mock_report); } TEST_F(RequestHandlerImplTest, TestHandlerDisabledReport) { - ::testing::NiceMock mock_data; - EXPECT_CALL(mock_data, GetResponseHeaders()).Times(0); - EXPECT_CALL(mock_data, GetReportInfo(_)).Times(0); + ::testing::NiceMock mock_check; + ::testing::NiceMock mock_report; + EXPECT_CALL(mock_check, GetSourceIpPort(_, _)).Times(0); + EXPECT_CALL(mock_report, GetResponseHeaders()).Times(0); + EXPECT_CALL(mock_report, GetReportInfo(_)).Times(0); + EXPECT_CALL(mock_report, GetDynamicFilterState()).Times(0); // Report should NOT be called. EXPECT_CALL(*mock_client_, Report(_)).Times(0); @@ -444,7 +511,7 @@ TEST_F(RequestHandlerImplTest, TestHandlerDisabledReport) { ApplyPerRouteConfig(config, &per_route); auto handler = controller_->CreateRequestHandler(per_route); - handler->Report(&mock_data); + handler->Report(&mock_check, &mock_report); } TEST_F(RequestHandlerImplTest, TestEmptyConfig) { @@ -458,7 +525,7 @@ TEST_F(RequestHandlerImplTest, TestEmptyConfig) { // Attributes is forwarded. EXPECT_CALL(mock_header, AddIstioAttributes(_)) - .WillOnce(Invoke([](const std::string& data) { + .WillOnce(Invoke([](const std::string &data) { Attributes forwarded_attr; EXPECT_TRUE(forwarded_attr.ParseFromString(data)); auto map = forwarded_attr.attributes(); @@ -471,6 +538,7 @@ TEST_F(RequestHandlerImplTest, TestEmptyConfig) { ::testing::NiceMock mock_report; EXPECT_CALL(mock_report, GetResponseHeaders()).Times(0); EXPECT_CALL(mock_report, GetReportInfo(_)).Times(0); + EXPECT_CALL(mock_report, GetDynamicFilterState()).Times(0); // Report should NOT be called. EXPECT_CALL(*mock_client_, Report(_)).Times(0); @@ -478,10 +546,66 @@ TEST_F(RequestHandlerImplTest, TestEmptyConfig) { Controller::PerRouteConfig config; auto handler = controller_->CreateRequestHandler(config); handler->Check(&mock_check, &mock_header, nullptr, - [](const CheckResponseInfo& info) { + [](const CheckResponseInfo &info) { EXPECT_TRUE(info.response_status.ok()); }); - handler->Report(&mock_report); + handler->Report(&mock_check, &mock_report); +} + +TEST_F(OutboundRequestHandlerImplTest, TestLocalAttributes) { + ::testing::NiceMock mock_data; + ::testing::NiceMock mock_header; + // Check should be called. + EXPECT_CALL(*mock_client_, Check(_, _, _, _)) + .WillOnce(Invoke([](const Attributes &attributes, + const std::vector "as, + TransportCheckFunc transport, + CheckDoneFunc on_done) -> CancelFunc { + auto map = attributes.attributes(); + EXPECT_EQ(map["source.uid"].string_value(), + "kubernetes://src-client-84469dc8d7-jbbxt.default"); + return nullptr; + })); + + ServiceConfig config; + Controller::PerRouteConfig per_route; + ApplyPerRouteConfig(config, &per_route); + auto handler = controller_->CreateRequestHandler(per_route); + handler->Check(&mock_data, &mock_header, nullptr, nullptr); +} + +TEST_F(OutboundRequestHandlerImplTest, TestLocalAttributesOverride) { + ::testing::NiceMock mock_data; + ::testing::NiceMock mock_header; + + EXPECT_CALL(mock_data, ExtractIstioAttributes(_)) + .WillOnce(Invoke([](std::string *data) -> bool { + Attributes fwd_attr; + (*fwd_attr.mutable_attributes())["source.uid"].set_string_value( + "fwded"); + (*fwd_attr.mutable_attributes())["destination.uid"].set_string_value( + "ignored"); + fwd_attr.SerializeToString(data); + return true; + })); + + // Check should be called. + EXPECT_CALL(*mock_client_, Check(_, _, _, _)) + .WillOnce(Invoke([](const Attributes &attributes, + const std::vector "as, + TransportCheckFunc transport, + CheckDoneFunc on_done) -> CancelFunc { + auto map = attributes.attributes(); + EXPECT_EQ(map["source.uid"].string_value(), "fwded"); + EXPECT_NE(map["destination.uid"].string_value(), "ignored"); + return nullptr; + })); + + ServiceConfig config; + Controller::PerRouteConfig per_route; + ApplyPerRouteConfig(config, &per_route); + auto handler = controller_->CreateRequestHandler(per_route); + handler->Check(&mock_data, &mock_header, nullptr, nullptr); } } // namespace http diff --git a/src/istio/control/http/service_context.cc b/src/istio/control/http/service_context.cc index 5f91eb82b01..dc1332db698 100644 --- a/src/istio/control/http/service_context.cc +++ b/src/istio/control/http/service_context.cc @@ -52,11 +52,14 @@ void ServiceContext::BuildParsers() { // Add static mixer attributes. void ServiceContext::AddStaticAttributes(RequestContext *request) const { + client_context_->AddLocalNodeAttributes(request->attributes); + if (client_context_->config().has_mixer_attributes()) { - request->attributes.MergeFrom(client_context_->config().mixer_attributes()); + request->attributes->MergeFrom( + client_context_->config().mixer_attributes()); } if (service_config_ && service_config_->has_mixer_attributes()) { - request->attributes.MergeFrom(service_config_->mixer_attributes()); + request->attributes->MergeFrom(service_config_->mixer_attributes()); } } @@ -65,6 +68,8 @@ void ServiceContext::InjectForwardedAttributes( HeaderUpdate *header_update) const { Attributes attributes; + client_context_->AddLocalNodeForwardAttribues(&attributes); + if (client_context_->config().has_forward_attributes()) { attributes.MergeFrom(client_context_->config().forward_attributes()); } @@ -86,13 +91,13 @@ void ServiceContext::AddApiAttributes(CheckData *check_data, std::string path; if (check_data->FindHeaderByType(CheckData::HEADER_METHOD, &http_method) && check_data->FindHeaderByType(CheckData::HEADER_PATH, &path)) { - api_spec_parser_->AddAttributes(http_method, path, &request->attributes); + api_spec_parser_->AddAttributes(http_method, path, request->attributes); } std::string api_key; if (api_spec_parser_->ExtractApiKey(check_data, &api_key)) { (*request->attributes - .mutable_attributes())[utils::AttributeName::kRequestApiKey] + ->mutable_attributes())[utils::AttributeName::kRequestApiKey] .set_string_value(api_key); } } @@ -100,7 +105,7 @@ void ServiceContext::AddApiAttributes(CheckData *check_data, // Add quota requirements from quota configs. void ServiceContext::AddQuotas(RequestContext *request) const { for (const auto &parser : quota_parsers_) { - parser->GetRequirements(request->attributes, &request->quotas); + parser->GetRequirements(*request->attributes, &request->quotas); } } diff --git a/src/istio/control/request_context.h b/src/istio/control/request_context.h index 55dd4b3fa4f..5e655abbba0 100644 --- a/src/istio/control/request_context.h +++ b/src/istio/control/request_context.h @@ -16,6 +16,7 @@ #ifndef ISTIO_CONTROL_REQUEST_CONTEXT_H #define ISTIO_CONTROL_REQUEST_CONTEXT_H +#include "google/protobuf/arena.h" #include "google/protobuf/stubs/status.h" #include "include/istio/quota_config/requirement.h" #include "mixer/v1/attributes.pb.h" @@ -27,8 +28,15 @@ namespace control { // The context to hold request data for both HTTP and TCP. struct RequestContext { + RequestContext() { + attributes = + google::protobuf::Arena::CreateMessage<::istio::mixer::v1::Attributes>( + &arena_); + } + // protobuf arena + google::protobuf::Arena arena_; // The attributes for both Check and Report. - ::istio::mixer::v1::Attributes attributes; + ::istio::mixer::v1::Attributes* attributes; // The quota requirements std::vector<::istio::quota_config::Requirement> quotas; // The check status. diff --git a/src/istio/control/tcp/BUILD b/src/istio/control/tcp/BUILD index 5794abd1771..438f7de4d02 100644 --- a/src/istio/control/tcp/BUILD +++ b/src/istio/control/tcp/BUILD @@ -31,6 +31,7 @@ cc_library( "//include/istio/utils:attribute_names_header", "//src/istio/control:common_lib", "//src/istio/utils:attribute_names_lib", + "//src/istio/utils:utils_lib", ], ) @@ -45,6 +46,7 @@ cc_test( linkstatic = 1, deps = [ ":control_lib", + "//src/istio/utils:utils_lib", "//external:googletest_main", ], ) diff --git a/src/istio/control/tcp/attributes_builder.cc b/src/istio/control/tcp/attributes_builder.cc index e61bc0e3917..0c8876e7923 100644 --- a/src/istio/control/tcp/attributes_builder.cc +++ b/src/istio/control/tcp/attributes_builder.cc @@ -14,6 +14,7 @@ */ #include "src/istio/control/tcp/attributes_builder.h" +#include "src/istio/utils/utils.h" #include "include/istio/utils/attribute_names.h" #include "include/istio/utils/attributes_builder.h" @@ -28,8 +29,8 @@ const std::string kConnectionContinue("continue"); const std::string kConnectionClose("close"); } // namespace -void AttributesBuilder::ExtractCheckAttributes(CheckData* check_data) { - utils::AttributesBuilder builder(&request_->attributes); +void AttributesBuilder::ExtractCheckAttributes(CheckData *check_data) { + utils::AttributesBuilder builder(request_->attributes); std::string source_ip; int source_port; @@ -49,6 +50,10 @@ void AttributesBuilder::ExtractCheckAttributes(CheckData* check_data) { // over. https://github.com/istio/istio/issues/4689 builder.AddString(utils::AttributeName::kSourceUser, source_user); builder.AddString(utils::AttributeName::kSourcePrincipal, source_user); + std::string source_ns(""); + if (utils::GetSourceNamespace(source_user, &source_ns)) { + builder.AddString(utils::AttributeName::kSourceNamespace, source_ns); + } } std::string destination_principal; @@ -76,15 +81,15 @@ void AttributesBuilder::ExtractCheckAttributes(CheckData* check_data) { } void AttributesBuilder::ExtractReportAttributes( - ReportData* report_data, ReportData::ConnectionEvent event, - ReportData::ReportInfo* last_report_info) { - utils::AttributesBuilder builder(&request_->attributes); + ReportData *report_data, ReportData::ConnectionEvent event, + ReportData::ReportInfo *last_report_info) { + utils::AttributesBuilder builder(request_->attributes); ReportData::ReportInfo info; report_data->GetReportInfo(&info); - builder.AddInt64(utils::AttributeName::kConnectionReceviedBytes, + builder.AddInt64(utils::AttributeName::kConnectionReceivedBytes, info.received_bytes - last_report_info->received_bytes); - builder.AddInt64(utils::AttributeName::kConnectionReceviedTotalBytes, + builder.AddInt64(utils::AttributeName::kConnectionReceivedTotalBytes, info.received_bytes); builder.AddInt64(utils::AttributeName::kConnectionSendBytes, info.send_bytes - last_report_info->send_bytes); @@ -127,6 +132,8 @@ void AttributesBuilder::ExtractReportAttributes( builder.AddString(utils::AttributeName::kDestinationUID, uid); } + builder.FlattenMapOfStringToStruct(report_data->GetDynamicFilterState()); + builder.AddTimestamp(utils::AttributeName::kContextTime, std::chrono::system_clock::now()); } diff --git a/src/istio/control/tcp/attributes_builder_test.cc b/src/istio/control/tcp/attributes_builder_test.cc index 52ee3a984a2..96fb310cdc9 100644 --- a/src/istio/control/tcp/attributes_builder_test.cc +++ b/src/istio/control/tcp/attributes_builder_test.cc @@ -22,6 +22,7 @@ #include "include/istio/utils/attributes_builder.h" #include "src/istio/control/tcp/mock_check_data.h" #include "src/istio/control/tcp/mock_report_data.h" +#include "src/istio/utils/utils.h" using ::google::protobuf::TextFormat; using ::google::protobuf::util::MessageDifferencer; @@ -29,6 +30,7 @@ using ::google::protobuf::util::MessageDifferencer; using ::testing::_; using ::testing::Invoke; using ::testing::Return; +using ::testing::ReturnRef; namespace istio { namespace control { @@ -73,16 +75,22 @@ attributes { string_value: "www.google.com" } } +attributes { + key: "source.namespace" + value { + string_value: "ns_ns" + } +} attributes { key: "source.principal" value { - string_value: "test_user" + string_value: "cluster.local/sa/test_user/ns/ns_ns/" } } attributes { key: "source.user" value { - string_value: "test_user" + string_value: "cluster.local/sa/test_user/ns/ns_ns/" } } attributes { @@ -155,6 +163,21 @@ attributes { string_value: "pod1.ns2" } } +attributes { + key: "foo.bar.com" + value { + string_map_value { + entries { + key: "str" + value: "abc" + } + entries { + key: "list" + value: "a,b,c" + } + } + } +} )"; const char kReportAttributes[] = R"( @@ -233,6 +256,21 @@ attributes { string_value: "pod1.ns2" } } +attributes { + key: "foo.bar.com" + value { + string_map_value { + entries { + key: "str" + value: "abc" + } + entries { + key: "list" + value: "a,b,c" + } + } + } +} )"; const char kDeltaOneReportAttributes[] = R"( @@ -291,6 +329,21 @@ attributes { string_value: "pod1.ns2" } } +attributes { + key: "foo.bar.com" + value { + string_map_value { + entries { + key: "str" + value: "abc" + } + entries { + key: "list" + value: "a,b,c" + } + } + } +} )"; const char kDeltaTwoReportAttributes[] = R"( @@ -349,11 +402,26 @@ attributes { string_value: "pod1.ns2" } } +attributes { + key: "foo.bar.com" + value { + string_map_value { + entries { + key: "str" + value: "abc" + } + entries { + key: "list" + value: "a,b,c" + } + } + } +} )"; -void ClearContextTime(RequestContext* request) { +void ClearContextTime(RequestContext *request) { // Override timestamp with - - utils::AttributesBuilder builder(&request->attributes); + utils::AttributesBuilder builder(request->attributes); std::chrono::time_point time0; builder.AddTimestamp(utils::AttributeName::kContextTime, time0); } @@ -361,7 +429,7 @@ void ClearContextTime(RequestContext* request) { TEST(AttributesBuilderTest, TestCheckAttributes) { ::testing::NiceMock mock_data; EXPECT_CALL(mock_data, GetSourceIpPort(_, _)) - .WillOnce(Invoke([](std::string* ip, int* port) -> bool { + .WillOnce(Invoke([](std::string *ip, int *port) -> bool { *ip = "1.2.3.4"; *port = 8080; return true; @@ -370,9 +438,9 @@ TEST(AttributesBuilderTest, TestCheckAttributes) { return true; })); EXPECT_CALL(mock_data, GetPrincipal(_, _)) - .WillRepeatedly(Invoke([](bool peer, std::string* user) -> bool { + .WillRepeatedly(Invoke([](bool peer, std::string *user) -> bool { if (peer) { - *user = "test_user"; + *user = "cluster.local/sa/test_user/ns/ns_ns/"; } else { *user = "destination_user"; } @@ -380,7 +448,7 @@ TEST(AttributesBuilderTest, TestCheckAttributes) { })); EXPECT_CALL(mock_data, GetConnectionId()).WillOnce(Return("1234-5")); EXPECT_CALL(mock_data, GetRequestedServerName(_)) - .WillOnce(Invoke([](std::string* name) -> bool { + .WillOnce(Invoke([](std::string *name) -> bool { *name = "www.google.com"; return true; })); @@ -391,49 +459,71 @@ TEST(AttributesBuilderTest, TestCheckAttributes) { ClearContextTime(&request); std::string out_str; - TextFormat::PrintToString(request.attributes, &out_str); + TextFormat::PrintToString(*request.attributes, &out_str); GOOGLE_LOG(INFO) << "===" << out_str << "==="; ::istio::mixer::v1::Attributes expected_attributes; ASSERT_TRUE( TextFormat::ParseFromString(kCheckAttributes, &expected_attributes)); EXPECT_TRUE( - MessageDifferencer::Equals(request.attributes, expected_attributes)); + MessageDifferencer::Equals(*request.attributes, expected_attributes)); } +/* TEST(AttributesBuilderTest, TestReportAttributes) { ::testing::NiceMock mock_data; + + ::google::protobuf::Map + filter_metadata; + ::google::protobuf::Struct struct_obj; + ::google::protobuf::Value strval, numval, boolval, listval; + strval.set_string_value("abc"); + (*struct_obj.mutable_fields())["str"] = strval; + numval.set_number_value(12.3); + (*struct_obj.mutable_fields())["num"] = numval; + boolval.set_bool_value(true); + (*struct_obj.mutable_fields())["bool"] = boolval; + listval.mutable_list_value()->add_values()->set_string_value("a"); + listval.mutable_list_value()->add_values()->set_string_value("b"); + listval.mutable_list_value()->add_values()->set_string_value("c"); + (*struct_obj.mutable_fields())["list"] = listval; + filter_metadata["foo.bar.com"] = struct_obj; + filter_metadata["istio.mixer"] = struct_obj; // to be ignored + EXPECT_CALL(mock_data, GetDestinationIpPort(_, _)) .Times(4) - .WillRepeatedly(Invoke([](std::string* ip, int* port) -> bool { + .WillRepeatedly(Invoke([](std::string *ip, int *port) -> bool { *ip = "1.2.3.4"; *port = 8080; return true; })); EXPECT_CALL(mock_data, GetDestinationUID(_)) .Times(4) - .WillRepeatedly(Invoke([](std::string* uid) -> bool { + .WillRepeatedly(Invoke([](std::string *uid) -> bool { *uid = "pod1.ns2"; return true; })); + EXPECT_CALL(mock_data, GetDynamicFilterState()) + .Times(4) + .WillRepeatedly(ReturnRef(filter_metadata)); EXPECT_CALL(mock_data, GetReportInfo(_)) .Times(4) - .WillOnce(Invoke([](ReportData::ReportInfo* info) { + .WillOnce(Invoke([](ReportData::ReportInfo *info) { info->received_bytes = 0; info->send_bytes = 0; info->duration = std::chrono::nanoseconds(1); })) - .WillOnce(Invoke([](ReportData::ReportInfo* info) { + .WillOnce(Invoke([](ReportData::ReportInfo *info) { info->received_bytes = 100; info->send_bytes = 200; info->duration = std::chrono::nanoseconds(2); })) - .WillOnce(Invoke([](ReportData::ReportInfo* info) { + .WillOnce(Invoke([](ReportData::ReportInfo *info) { info->received_bytes = 201; info->send_bytes = 404; info->duration = std::chrono::nanoseconds(3); })) - .WillOnce(Invoke([](ReportData::ReportInfo* info) { + .WillOnce(Invoke([](ReportData::ReportInfo *info) { info->received_bytes = 345; info->send_bytes = 678; info->duration = std::chrono::nanoseconds(4); @@ -452,14 +542,14 @@ TEST(AttributesBuilderTest, TestReportAttributes) { ClearContextTime(&request); std::string out_str; - TextFormat::PrintToString(request.attributes, &out_str); + TextFormat::PrintToString(*request.attributes, &out_str); GOOGLE_LOG(INFO) << "===" << out_str << "==="; ::istio::mixer::v1::Attributes expected_open_attributes; ASSERT_TRUE(TextFormat::ParseFromString(kFirstReportAttributes, &expected_open_attributes)); - EXPECT_TRUE( - MessageDifferencer::Equals(request.attributes, expected_open_attributes)); + EXPECT_TRUE(MessageDifferencer::Equals(*request.attributes, + expected_open_attributes)); EXPECT_EQ(0, last_report_info.received_bytes); EXPECT_EQ(0, last_report_info.send_bytes); @@ -468,13 +558,13 @@ TEST(AttributesBuilderTest, TestReportAttributes) { &mock_data, ReportData::ConnectionEvent::CONTINUE, &last_report_info); ClearContextTime(&request); - TextFormat::PrintToString(request.attributes, &out_str); + TextFormat::PrintToString(*request.attributes, &out_str); GOOGLE_LOG(INFO) << "===" << out_str << "==="; ::istio::mixer::v1::Attributes expected_delta_attributes; ASSERT_TRUE(TextFormat::ParseFromString(kDeltaOneReportAttributes, &expected_delta_attributes)); - EXPECT_TRUE(MessageDifferencer::Equals(request.attributes, + EXPECT_TRUE(MessageDifferencer::Equals(*request.attributes, expected_delta_attributes)); EXPECT_EQ(100, last_report_info.received_bytes); EXPECT_EQ(200, last_report_info.send_bytes); @@ -485,13 +575,13 @@ TEST(AttributesBuilderTest, TestReportAttributes) { ClearContextTime(&request); out_str.clear(); - TextFormat::PrintToString(request.attributes, &out_str); + TextFormat::PrintToString(*request.attributes, &out_str); GOOGLE_LOG(INFO) << "===" << out_str << "==="; expected_delta_attributes.Clear(); ASSERT_TRUE(TextFormat::ParseFromString(kDeltaTwoReportAttributes, &expected_delta_attributes)); - EXPECT_TRUE(MessageDifferencer::Equals(request.attributes, + EXPECT_TRUE(MessageDifferencer::Equals(*request.attributes, expected_delta_attributes)); EXPECT_EQ(201, last_report_info.received_bytes); EXPECT_EQ(404, last_report_info.send_bytes); @@ -502,17 +592,17 @@ TEST(AttributesBuilderTest, TestReportAttributes) { ClearContextTime(&request); out_str.clear(); - TextFormat::PrintToString(request.attributes, &out_str); + TextFormat::PrintToString(*request.attributes, &out_str); GOOGLE_LOG(INFO) << "===" << out_str << "==="; ::istio::mixer::v1::Attributes expected_final_attributes; ASSERT_TRUE(TextFormat::ParseFromString(kReportAttributes, &expected_final_attributes)); - EXPECT_TRUE(MessageDifferencer::Equals(request.attributes, + EXPECT_TRUE(MessageDifferencer::Equals(*request.attributes, expected_final_attributes)); -} +} */ } // namespace } // namespace tcp } // namespace control -} // namespace istio +} // namespace istio \ No newline at end of file diff --git a/src/istio/control/tcp/client_context.h b/src/istio/control/tcp/client_context.h index e093b9745ef..38072694aed 100644 --- a/src/istio/control/tcp/client_context.h +++ b/src/istio/control/tcp/client_context.h @@ -18,6 +18,7 @@ #include "include/istio/control/tcp/controller.h" #include "include/istio/quota_config/config_parser.h" +#include "include/istio/utils/local_attributes.h" #include "src/istio/control/client_context_base.h" #include "src/istio/control/request_context.h" @@ -31,7 +32,10 @@ namespace tcp { class ClientContext : public ClientContextBase { public: ClientContext(const Controller::Options& data) - : ClientContextBase(data.config.transport(), data.env), + : ClientContextBase( + data.config.transport(), data.env, + ::istio::utils::IsOutbound(data.config.mixer_attributes()), + data.local_node), config_(data.config) { BuildQuotaParser(); } @@ -39,22 +43,26 @@ class ClientContext : public ClientContextBase { // A constructor for unit-test to pass in a mock mixer_client ClientContext( std::unique_ptr<::istio::mixerclient::MixerClient> mixer_client, - const ::istio::mixer::v1::config::client::TcpClientConfig& config) - : ClientContextBase(std::move(mixer_client)), config_(config) { + const ::istio::mixer::v1::config::client::TcpClientConfig& config, + bool outbound, ::istio::utils::LocalAttributes& local_attributes) + : ClientContextBase(std::move(mixer_client), outbound, local_attributes), + config_(config) { BuildQuotaParser(); } // Add static mixer attributes. void AddStaticAttributes(RequestContext* request) const { + AddLocalNodeAttributes(request->attributes); + if (config_.has_mixer_attributes()) { - request->attributes.MergeFrom(config_.mixer_attributes()); + request->attributes->MergeFrom(config_.mixer_attributes()); } } // Add quota requirements from quota configs. void AddQuotas(RequestContext* request) const { if (quota_parser_) { - quota_parser_->GetRequirements(request->attributes, &request->quotas); + quota_parser_->GetRequirements(*request->attributes, &request->quotas); } } diff --git a/src/istio/control/tcp/mock_report_data.h b/src/istio/control/tcp/mock_report_data.h index 65c18492c82..85c749ff360 100644 --- a/src/istio/control/tcp/mock_report_data.h +++ b/src/istio/control/tcp/mock_report_data.h @@ -26,9 +26,13 @@ namespace tcp { // The mock object for ReportData interface. class MockReportData : public ReportData { public: - MOCK_CONST_METHOD2(GetDestinationIpPort, bool(std::string* ip, int* port)); - MOCK_CONST_METHOD1(GetDestinationUID, bool(std::string*)); - MOCK_CONST_METHOD1(GetReportInfo, void(ReportInfo* info)); + MOCK_CONST_METHOD2(GetDestinationIpPort, bool(std::string *ip, int *port)); + MOCK_CONST_METHOD1(GetDestinationUID, bool(std::string *)); + MOCK_CONST_METHOD0( + GetDynamicFilterState, + const ::google::protobuf::Map + &()); + MOCK_CONST_METHOD1(GetReportInfo, void(ReportInfo *info)); }; } // namespace tcp diff --git a/src/istio/control/tcp/request_handler_impl_test.cc b/src/istio/control/tcp/request_handler_impl_test.cc index 259fd06d7ce..c27763b3bc4 100644 --- a/src/istio/control/tcp/request_handler_impl_test.cc +++ b/src/istio/control/tcp/request_handler_impl_test.cc @@ -13,6 +13,7 @@ * limitations under the License. */ +#include "google/protobuf/text_format.h" #include "gtest/gtest.h" #include "src/istio/control/mock_mixer_client.h" #include "src/istio/control/tcp/client_context.h" @@ -20,6 +21,7 @@ #include "src/istio/control/tcp/mock_check_data.h" #include "src/istio/control/tcp/mock_report_data.h" +using ::google::protobuf::TextFormat; using ::google::protobuf::util::Status; using ::istio::mixer::v1::Attributes; using ::istio::mixer::v1::config::client::TcpClientConfig; @@ -30,14 +32,46 @@ using ::istio::mixerclient::DoneFunc; using ::istio::mixerclient::MixerClient; using ::istio::mixerclient::TransportCheckFunc; using ::istio::quota_config::Requirement; +using ::istio::utils::LocalAttributes; using ::testing::_; using ::testing::Invoke; +using ::testing::ReturnRef; namespace istio { namespace control { namespace tcp { +namespace { +// local inbound +const char kLocalInbound[] = R"( +attributes { + key: "destination.uid" + value { + string_value: "kubernetes://client-84469dc8d7-jbbxt.default" + } +} +)"; + +const char kLocalOutbound[] = R"( +attributes { + key: "source.uid" + value { + string_value: "kubernetes://client-84469dc8d7-jbbxt.default" + } +} +)"; + +const char kLocalForward[] = R"( +attributes { + key: "source.uid" + value { + string_value: "kubernetes://client-84469dc8d7-jbbxt.default" + } +} +)"; +} // namespace + class RequestHandlerImplTest : public ::testing::Test { public: void SetUp() { @@ -50,16 +84,21 @@ class RequestHandlerImplTest : public ::testing::Test { quota->set_quota("quota"); quota->set_charge(5); + LocalAttributes la; + ASSERT_TRUE(TextFormat::ParseFromString(kLocalInbound, &la.inbound)); + ASSERT_TRUE(TextFormat::ParseFromString(kLocalOutbound, &la.outbound)); + ASSERT_TRUE(TextFormat::ParseFromString(kLocalForward, &la.forward)); + mock_client_ = new ::testing::NiceMock; client_context_ = std::make_shared( - std::unique_ptr(mock_client_), client_config_); + std::unique_ptr(mock_client_), client_config_, false, la); controller_ = std::unique_ptr(new ControllerImpl(client_context_)); } std::shared_ptr client_context_; TcpClientConfig client_config_; - ::testing::NiceMock* mock_client_; + ::testing::NiceMock *mock_client_; std::unique_ptr controller_; }; @@ -73,7 +112,7 @@ TEST_F(RequestHandlerImplTest, TestHandlerDisabledCheck) { client_config_.set_disable_check_calls(true); auto handler = controller_->CreateRequestHandler(); - handler->Check(&mock_data, [](const CheckResponseInfo& info) { + handler->Check(&mock_data, [](const CheckResponseInfo &info) { EXPECT_TRUE(info.response_status.ok()); }); } @@ -85,8 +124,8 @@ TEST_F(RequestHandlerImplTest, TestHandlerCheck) { // Check should be called. EXPECT_CALL(*mock_client_, Check(_, _, _, _)) - .WillOnce(Invoke([](const Attributes& attributes, - const std::vector& quotas, + .WillOnce(Invoke([](const Attributes &attributes, + const std::vector "as, TransportCheckFunc transport, CheckDoneFunc on_done) -> CancelFunc { auto map = attributes.attributes(); @@ -103,9 +142,14 @@ TEST_F(RequestHandlerImplTest, TestHandlerCheck) { TEST_F(RequestHandlerImplTest, TestHandlerReport) { ::testing::NiceMock mock_data; + ::google::protobuf::Map + filter_metadata; EXPECT_CALL(mock_data, GetDestinationIpPort(_, _)).Times(1); EXPECT_CALL(mock_data, GetDestinationUID(_)).Times(1); EXPECT_CALL(mock_data, GetReportInfo(_)).Times(1); + EXPECT_CALL(mock_data, GetDynamicFilterState()) + .Times(1) + .WillOnce(ReturnRef(filter_metadata)); // Report should be called. EXPECT_CALL(*mock_client_, Report(_)).Times(1); diff --git a/src/istio/mixerclient/BUILD b/src/istio/mixerclient/BUILD index a34cc53e6c5..6d5d8010b38 100644 --- a/src/istio/mixerclient/BUILD +++ b/src/istio/mixerclient/BUILD @@ -42,8 +42,6 @@ cc_library( "check_cache.h", "client_impl.cc", "client_impl.h", - "delta_update.cc", - "delta_update.h", "global_dictionary.cc", "global_dictionary.h", "quota_cache.cc", @@ -60,7 +58,6 @@ cc_library( "//include/istio/quota_config:requirement_header", "//include/istio/utils:simple_lru_cache", "//src/istio/prefetch:quota_prefetch_lib", - "//src/istio/utils:md5_lib", "//src/istio/utils:utils_lib", ], ) @@ -96,17 +93,6 @@ cc_test( ], ) -cc_test( - name = "delta_update_test", - size = "small", - srcs = ["delta_update_test.cc"], - linkstatic = 1, - deps = [ - ":mixerclient_lib", - "//external:googletest_main", - ], -) - cc_test( name = "report_batch_test", size = "small", diff --git a/src/istio/mixerclient/attribute_compressor.cc b/src/istio/mixerclient/attribute_compressor.cc index f60e6f26ea0..34c980ae35e 100644 --- a/src/istio/mixerclient/attribute_compressor.cc +++ b/src/istio/mixerclient/attribute_compressor.cc @@ -14,8 +14,8 @@ */ #include "src/istio/mixerclient/attribute_compressor.h" +#include "google/protobuf/arena.h" #include "include/istio/utils/protobuf.h" -#include "src/istio/mixerclient/delta_update.h" #include "src/istio/mixerclient/global_dictionary.h" using ::istio::mixer::v1::Attributes; @@ -59,6 +59,11 @@ class MessageDictionary { const std::vector& GetWords() const { return message_words_; } + void Clear() { + message_words_.clear(); + message_dict_.clear(); + } + private: const GlobalDictionary& global_dict_; @@ -77,10 +82,8 @@ ::istio::mixer::v1::StringMap CreateStringMap( return compressed_map; } -bool CompressByDict(const Attributes& attributes, MessageDictionary& dict, - DeltaUpdate& delta_update, CompressedAttributes* pb) { - delta_update.Start(); - +void CompressByDict(const Attributes& attributes, MessageDictionary& dict, + CompressedAttributes* pb) { // Fill attributes. for (const auto& it : attributes.attributes()) { const std::string& name = it.first; @@ -88,11 +91,6 @@ bool CompressByDict(const Attributes& attributes, MessageDictionary& dict, int index = dict.GetIndex(name); - // Check delta update. If same, skip it. - if (delta_update.Check(index, value)) { - continue; - } - // Fill the attribute to proper map. switch (value.value_case()) { case Attributes_AttributeValue::kStringValue: @@ -124,41 +122,36 @@ bool CompressByDict(const Attributes& attributes, MessageDictionary& dict, break; } } - - return delta_update.Finish(); } class BatchCompressorImpl : public BatchCompressor { public: BatchCompressorImpl(const GlobalDictionary& global_dict) - : dict_(global_dict), - delta_update_(DeltaUpdate::Create()), - report_(new ::istio::mixer::v1::ReportRequest) { - report_->set_global_word_count(global_dict.size()); - } + : global_dict_(global_dict), dict_(global_dict) {} - bool Add(const Attributes& attributes) override { - CompressedAttributes pb; - if (!CompressByDict(attributes, dict_, *delta_update_, &pb)) { - return false; - } - pb.GetReflection()->Swap(report_->add_attributes(), &pb); - return true; + void Add(const Attributes& attributes) override { + CompressByDict(attributes, dict_, report_.add_attributes()); } - int size() const override { return report_->attributes_size(); } + int size() const override { return report_.attributes_size(); } - std::unique_ptr<::istio::mixer::v1::ReportRequest> Finish() override { + const ::istio::mixer::v1::ReportRequest& Finish() override { for (const std::string& word : dict_.GetWords()) { - report_->add_default_words(word); + report_.add_default_words(word); } - return std::move(report_); + report_.set_global_word_count(global_dict_.size()); + return report_; + } + + void Clear() override { + dict_.Clear(); + report_.Clear(); } private: + const GlobalDictionary& global_dict_; MessageDictionary dict_; - std::unique_ptr delta_update_; - std::unique_ptr<::istio::mixer::v1::ReportRequest> report_; + ::istio::mixer::v1::ReportRequest report_; }; } // namespace @@ -194,9 +187,7 @@ void AttributeCompressor::Compress( const Attributes& attributes, ::istio::mixer::v1::CompressedAttributes* pb) const { MessageDictionary dict(global_dict_); - std::unique_ptr delta_update = DeltaUpdate::CreateNoOp(); - - CompressByDict(attributes, dict, *delta_update, pb); + CompressByDict(attributes, dict, pb); for (const std::string& word : dict.GetWords()) { pb->add_words(word); diff --git a/src/istio/mixerclient/attribute_compressor.h b/src/istio/mixerclient/attribute_compressor.h index f37072f692b..0b0ed899a89 100644 --- a/src/istio/mixerclient/attribute_compressor.h +++ b/src/istio/mixerclient/attribute_compressor.h @@ -19,7 +19,7 @@ #include #include "mixer/v1/attributes.pb.h" -#include "mixer/v1/report.pb.h" +#include "mixer/v1/mixer.pb.h" namespace istio { namespace mixerclient { @@ -50,14 +50,16 @@ class BatchCompressor { virtual ~BatchCompressor() {} // Add an attribute set to the batch. - // Return false if it could not be added for delta update. - virtual bool Add(const ::istio::mixer::v1::Attributes& attributes) = 0; + virtual void Add(const ::istio::mixer::v1::Attributes& attributes) = 0; // Get the batched size. virtual int size() const = 0; // Finish the batch and create the batched report request. - virtual std::unique_ptr<::istio::mixer::v1::ReportRequest> Finish() = 0; + virtual const ::istio::mixer::v1::ReportRequest& Finish() = 0; + + // Reset the object data. + virtual void Clear() = 0; }; // Compress attributes. diff --git a/src/istio/mixerclient/attribute_compressor_test.cc b/src/istio/mixerclient/attribute_compressor_test.cc index 34fa59b3e67..614381bb16c 100644 --- a/src/istio/mixerclient/attribute_compressor_test.cc +++ b/src/istio/mixerclient/attribute_compressor_test.cc @@ -146,10 +146,22 @@ attributes { } } attributes { + strings { + key: 2 + value: 127 + } + strings { + key: 6 + value: 101 + } int64s { key: 1 value: 135 } + int64s { + key: 8 + value: 8080 + } int64s { key: 27 value: 111 @@ -162,6 +174,75 @@ attributes { key: 71 value: false } + timestamps { + key: 132 + value { + } + } + durations { + key: 29 + value { + seconds: 5 + } + } + bytes { + key: 0 + value: "text/html; charset=utf-8" + } + string_maps { + key: 15 + value { + entries { + key: 32 + value: 90 + } + entries { + key: 58 + value: 104 + } + } + } +} +attributes { + strings { + key: 2 + value: 127 + } + strings { + key: 6 + value: 101 + } + int64s { + key: 1 + value: 135 + } + int64s { + key: 8 + value: 8080 + } + doubles { + key: 78 + value: 123.99 + } + bools { + key: 71 + value: false + } + timestamps { + key: 132 + value { + } + } + durations { + key: 29 + value { + seconds: 5 + } + } + bytes { + key: 0 + value: "text/html; charset=utf-8" + } string_maps { key: 15 value { @@ -177,7 +258,7 @@ attributes { } } default_words: "JWT-Token" -global_word_count: 111 +global_word_count: 221 )"; class AttributeCompressorTest : public ::testing::Test { @@ -234,7 +315,7 @@ TEST_F(AttributeCompressorTest, BatchCompressTest) { AttributeCompressor compressor; auto batch_compressor = compressor.CreateBatchCompressor(); - EXPECT_TRUE(batch_compressor->Add(attributes_)); + batch_compressor->Add(attributes_); // modify some attributes utils::AttributesBuilder builder(&attributes_); @@ -245,25 +326,25 @@ TEST_F(AttributeCompressorTest, BatchCompressTest) { builder.AddStringMap("request.headers", {{"content-type", "application/json"}, {":method", "GET"}}); - // Since there is no deletion, batch is good - EXPECT_TRUE(batch_compressor->Add(attributes_)); + // Batch the second one with added attributes + batch_compressor->Add(attributes_); // remove a key attributes_.mutable_attributes()->erase("response.size"); - // Batch should fail. - EXPECT_FALSE(batch_compressor->Add(attributes_)); + // Batch the third with a removed attribute. + batch_compressor->Add(attributes_); auto report_pb = batch_compressor->Finish(); std::string out_str; - TextFormat::PrintToString(*report_pb, &out_str); + TextFormat::PrintToString(report_pb, &out_str); GOOGLE_LOG(INFO) << "===" << out_str << "==="; ::istio::mixer::v1::ReportRequest expected_report_pb; ASSERT_TRUE( TextFormat::ParseFromString(kReportAttributes, &expected_report_pb)); - report_pb->set_global_word_count(111); - EXPECT_TRUE(MessageDifferencer::Equals(*report_pb, expected_report_pb)); + report_pb.set_global_word_count(221); + EXPECT_TRUE(MessageDifferencer::Equals(report_pb, expected_report_pb)); } } // namespace diff --git a/src/istio/mixerclient/check_cache.cc b/src/istio/mixerclient/check_cache.cc index 0e3407fc58e..2f10557ce2a 100644 --- a/src/istio/mixerclient/check_cache.cc +++ b/src/istio/mixerclient/check_cache.cc @@ -106,7 +106,7 @@ Status CheckCache::Check(const Attributes &attributes, Tick time_now, std::lock_guard lock(cache_mutex_); for (const auto &it : referenced_map_) { const Referenced &reference = it.second; - std::string signature; + utils::HashType signature; if (!reference.Signature(attributes, "", &signature)) { continue; } @@ -145,7 +145,7 @@ Status CheckCache::CacheResponse(const Attributes &attributes, // Failed to decode referenced_attributes, not to cache this result. return ConvertRpcStatus(response.precondition().status()); } - std::string signature; + utils::HashType signature; if (!referenced.Signature(attributes, "", &signature)) { GOOGLE_LOG(ERROR) << "Response referenced mismatchs with request"; GOOGLE_LOG(ERROR) << "Request attributes: " << attributes.DebugString(); @@ -154,7 +154,7 @@ Status CheckCache::CacheResponse(const Attributes &attributes, } std::lock_guard lock(cache_mutex_); - std::string hash = referenced.Hash(); + utils::HashType hash = referenced.Hash(); if (referenced_map_.find(hash) == referenced_map_.end()) { referenced_map_[hash] = referenced; GOOGLE_LOG(INFO) << "Add a new Referenced for check cache: " diff --git a/src/istio/mixerclient/check_cache.h b/src/istio/mixerclient/check_cache.h index 36e9080f5cd..9e2d6d3fc2d 100644 --- a/src/istio/mixerclient/check_cache.h +++ b/src/istio/mixerclient/check_cache.h @@ -76,7 +76,7 @@ class CheckCache { // Check status. ::google::protobuf::util::Status status_; - // Route directive (if status is OK). + // Route directive ::istio::mixer::v1::RouteDirective route_directive_; // The function to set check response. @@ -155,13 +155,13 @@ class CheckCache { // Key is the signature of the Attributes. Value is the CacheElem. // It is a LRU cache with maximum size. // When the maximum size is reached, oldest idle items will be removed. - using CheckLRUCache = utils::SimpleLRUCache; + using CheckLRUCache = utils::SimpleLRUCache; // The check options. CheckOptions options_; // Referenced map keyed with their hashes - std::unordered_map referenced_map_; + std::unordered_map referenced_map_; // Mutex guarding the access of cache_; std::mutex cache_mutex_; diff --git a/src/istio/mixerclient/client_impl.cc b/src/istio/mixerclient/client_impl.cc index b80ce6a8c83..2a563039f41 100644 --- a/src/istio/mixerclient/client_impl.cc +++ b/src/istio/mixerclient/client_impl.cc @@ -13,6 +13,7 @@ * limitations under the License. */ #include "src/istio/mixerclient/client_impl.h" +#include #include "include/istio/mixerclient/check_response.h" #include "include/istio/utils/protobuf.h" @@ -82,26 +83,32 @@ CancelFunc MixerClientImpl::Check( quota_cache_->Check(attributes, quotas, check_result->IsCacheHit(), quota_result.get()); - CheckRequest request; - bool quota_call = quota_result->BuildRequest(&request); + auto arena = new google::protobuf::Arena; + CheckRequest *request = + google::protobuf::Arena::CreateMessage(arena); + bool quota_call = quota_result->BuildRequest(request); check_response_info.is_quota_cache_hit = quota_result->IsCacheHit(); check_response_info.response_status = quota_result->status(); if (check_result->IsCacheHit() && quota_result->IsCacheHit()) { on_done(check_response_info); on_done = nullptr; if (!quota_call) { + delete arena; return nullptr; } } - compressor_.Compress(attributes, request.mutable_attributes()); - request.set_global_word_count(compressor_.global_word_count()); - request.set_deduplication_id(deduplication_id_base_ + - std::to_string(deduplication_id_.fetch_add(1))); + compressor_.Compress(attributes, request->mutable_attributes()); + request->set_global_word_count(compressor_.global_word_count()); + request->set_deduplication_id(deduplication_id_base_ + + std::to_string(deduplication_id_.fetch_add(1))); // Need to make a copy for processing the response for check cache. - Attributes *request_copy = new Attributes(attributes); - auto response = new CheckResponse; + Attributes *attributes_copy = + google::protobuf::Arena::CreateMessage(arena); + CheckResponse *response = + google::protobuf::Arena::CreateMessage(arena); + *attributes_copy = attributes; // Lambda capture could not pass unique_ptr, use raw pointer. CheckCache::CheckResult *raw_check_result = check_result.release(); QuotaCache::CheckResult *raw_quota_result = quota_result.release(); @@ -121,11 +128,11 @@ CancelFunc MixerClientImpl::Check( } return transport( - request, response, - [this, request_copy, response, raw_check_result, raw_quota_result, - on_done](const Status &status) { - raw_check_result->SetResponse(status, *request_copy, *response); - raw_quota_result->SetResponse(status, *request_copy, *response); + *request, response, + [this, attributes_copy, response, raw_check_result, raw_quota_result, + on_done, arena](const Status &status) { + raw_check_result->SetResponse(status, *attributes_copy, *response); + raw_quota_result->SetResponse(status, *attributes_copy, *response); CheckResponseInfo check_response_info; if (on_done) { if (!raw_check_result->status().ok()) { @@ -139,8 +146,7 @@ CancelFunc MixerClientImpl::Check( } delete raw_check_result; delete raw_quota_result; - delete request_copy; - delete response; + delete arena; if (utils::InvalidDictionaryStatus(status)) { compressor_.ShrinkGlobalDictionary(); diff --git a/src/istio/mixerclient/delta_update.cc b/src/istio/mixerclient/delta_update.cc deleted file mode 100644 index e4f02641671..00000000000 --- a/src/istio/mixerclient/delta_update.cc +++ /dev/null @@ -1,86 +0,0 @@ -/* Copyright 2017 Istio Authors. All Rights Reserved. - * - * Licensed 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 "src/istio/mixerclient/delta_update.h" - -#include "google/protobuf/util/message_differencer.h" - -#include - -using ::google::protobuf::util::MessageDifferencer; -using ::istio::mixer::v1::Attributes_AttributeValue; - -namespace istio { -namespace mixerclient { -namespace { - -class DeltaUpdateImpl : public DeltaUpdate { - public: - // Start a update for a request. - void Start() override { - prev_set_.clear(); - for (const auto& it : prev_map_) { - prev_set_.insert(it.first); - } - } - - bool Check(int index, const Attributes_AttributeValue& value) override { - bool same = false; - const auto& it = prev_map_.find(index); - if (it != prev_map_.end()) { - if (MessageDifferencer::Equals(it->second, value)) { - same = true; - } - } - if (!same) { - prev_map_[index] = value; - } - prev_set_.erase(index); - return same; - } - - // "deleted" is not supported for now. If some attributes are missing, - // return false to indicate delta update is not supported. - bool Finish() override { return prev_set_.empty(); } - - private: - // The remaining attributes from previous. - std::set prev_set_; - - // The attribute map from previous. - std::map prev_map_; -}; - -// An optimization for non-delta update case. -class DeltaUpdateNoOpImpl : public DeltaUpdate { - public: - void Start() override {} - bool Check(int index, const Attributes_AttributeValue& value) override { - return false; - } - bool Finish() override { return true; } -}; - -} // namespace - -std::unique_ptr DeltaUpdate::Create() { - return std::unique_ptr(new DeltaUpdateImpl); -} - -std::unique_ptr DeltaUpdate::CreateNoOp() { - return std::unique_ptr(new DeltaUpdateNoOpImpl); -} - -} // namespace mixerclient -} // namespace istio diff --git a/src/istio/mixerclient/delta_update.h b/src/istio/mixerclient/delta_update.h deleted file mode 100644 index 3ec16a65f5d..00000000000 --- a/src/istio/mixerclient/delta_update.h +++ /dev/null @@ -1,59 +0,0 @@ -/* Copyright 2017 Istio Authors. All Rights Reserved. - * - * Licensed 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. - */ - -#ifndef ISTIO_MIXERCLIENT_DELTA_UPDATE_H -#define ISTIO_MIXERCLIENT_DELTA_UPDATE_H - -#include "mixer/v1/attributes.pb.h" - -#include - -namespace istio { -namespace mixerclient { - -// A class to support attribute delta update. -// It has previous attribute values and check -// for the current one. -class DeltaUpdate { - public: - virtual ~DeltaUpdate() {} - - // Start a new delta update - virtual void Start() = 0; - - // Check an attribute, return true if it is in the previous - // set with same value, so no need to send it again. - // Each attribute in the current set needs to call this method. - virtual bool Check( - int index, - const ::istio::mixer::v1::Attributes_AttributeValue& value) = 0; - - // Finish a delta update. - // Return false if delta update is not supported. - // For example, "deleted" is not supported, if some attributes are - // missing, delta update will not be supported. - virtual bool Finish() = 0; - - // Create an instance. - static std::unique_ptr Create(); - - // Create an no-op instance; an optimization for no delta update cases. - static std::unique_ptr CreateNoOp(); -}; - -} // namespace mixerclient -} // namespace istio - -#endif // ISTIO_MIXERCLIENT_DELTA_UPDATE_H diff --git a/src/istio/mixerclient/delta_update_test.cc b/src/istio/mixerclient/delta_update_test.cc deleted file mode 100644 index b2fda95b297..00000000000 --- a/src/istio/mixerclient/delta_update_test.cc +++ /dev/null @@ -1,102 +0,0 @@ -/* Copyright 2017 Istio Authors. All Rights Reserved. - * - * Licensed 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 "src/istio/mixerclient/delta_update.h" -#include "gtest/gtest.h" - -using ::istio::mixer::v1::Attributes_AttributeValue; - -namespace istio { -namespace mixerclient { - -class DeltaUpdateTest : public ::testing::Test { - public: - void SetUp() { - string_map_value_ = StringMapValue({{"foo", "bar"}}); - update_ = DeltaUpdate::Create(); - - update_->Start(); - EXPECT_FALSE(update_->Check(1, Int64Value(1))); - EXPECT_FALSE(update_->Check(2, Int64Value(2))); - EXPECT_FALSE(update_->Check(3, string_map_value_)); - EXPECT_TRUE(update_->Finish()); - } - - ::istio::mixer::v1::Attributes_AttributeValue Int64Value(int64_t i) { - ::istio::mixer::v1::Attributes_AttributeValue v; - v.set_int64_value(i); - return v; - } - - ::istio::mixer::v1::Attributes_AttributeValue StringValue( - const std::string& str) { - ::istio::mixer::v1::Attributes_AttributeValue v; - v.set_string_value(str); - return v; - } - - ::istio::mixer::v1::Attributes_AttributeValue StringMapValue( - std::map&& string_map) { - ::istio::mixer::v1::Attributes_AttributeValue v; - auto entries = v.mutable_string_map_value()->mutable_entries(); - for (const auto& map_it : string_map) { - (*entries)[map_it.first] = map_it.second; - } - return v; - } - - std::unique_ptr update_; - Attributes_AttributeValue string_map_value_; -}; - -TEST_F(DeltaUpdateTest, TestUpdateNoDelete) { - update_->Start(); - // 1: value is the same. - EXPECT_TRUE(update_->Check(1, Int64Value(1))); - // 2: value is different. - EXPECT_FALSE(update_->Check(2, Int64Value(3))); - // 3: compare string map. - EXPECT_TRUE(update_->Check(3, string_map_value_)); - // 4: an new attribute. - EXPECT_FALSE(update_->Check(4, Int64Value(4))); - // No missing item - EXPECT_TRUE(update_->Finish()); -} - -TEST_F(DeltaUpdateTest, TestUpdateWithDelete) { - update_->Start(); - // 1: value is the same. - EXPECT_TRUE(update_->Check(1, Int64Value(1))); - - // 2: is missing - - // 3: compare string map - EXPECT_FALSE(update_->Check(3, StringMapValue({}))); - - // 4: an new attribute. - EXPECT_FALSE(update_->Check(4, Int64Value(4))); - - // There is a missing item - EXPECT_FALSE(update_->Finish()); -} - -TEST_F(DeltaUpdateTest, TestDifferentType) { - update_->Start(); - // 1 is differnt type. - EXPECT_FALSE(update_->Check(1, StringValue(""))); -} - -} // namespace mixerclient -} // namespace istio diff --git a/src/istio/mixerclient/quota_cache.cc b/src/istio/mixerclient/quota_cache.cc index cfd97385d6c..4d77a038fff 100644 --- a/src/istio/mixerclient/quota_cache.cc +++ b/src/istio/mixerclient/quota_cache.cc @@ -174,7 +174,7 @@ void QuotaCache::CheckCache(const Attributes& request, bool check_use_cache, PerQuotaReferenced& quota_ref = quota_referenced_map_[quota->name]; for (const auto& it : quota_ref.referenced_map) { const Referenced& referenced = it.second; - std::string signature; + utils::HashType signature; if (!referenced.Signature(request, quota->name, &signature)) { continue; } @@ -216,7 +216,7 @@ void QuotaCache::SetResponse(const Attributes& attributes, return; } - std::string signature; + utils::HashType signature; if (!referenced.Signature(attributes, quota_name, &signature)) { GOOGLE_LOG(ERROR) << "Quota response referenced mismatchs with request"; GOOGLE_LOG(ERROR) << "Request attributes: " << attributes.DebugString(); @@ -232,7 +232,7 @@ void QuotaCache::SetResponse(const Attributes& attributes, } PerQuotaReferenced& quota_ref = quota_referenced_map_[quota_name]; - std::string hash = referenced.Hash(); + utils::HashType hash = referenced.Hash(); if (quota_ref.referenced_map.find(hash) == quota_ref.referenced_map.end()) { quota_ref.referenced_map[hash] = referenced; GOOGLE_LOG(INFO) << "Add a new Referenced for quota cache: " << quota_name diff --git a/src/istio/mixerclient/quota_cache.h b/src/istio/mixerclient/quota_cache.h index ce7d1025f9e..231e4324795 100644 --- a/src/istio/mixerclient/quota_cache.h +++ b/src/istio/mixerclient/quota_cache.h @@ -140,7 +140,7 @@ class QuotaCache { std::unique_ptr pending_item; // Referenced map keyed with their hashes - std::unordered_map referenced_map; + std::unordered_map referenced_map; }; // Set a quota response. @@ -154,7 +154,7 @@ class QuotaCache { // Key is the signature of the Attributes. Value is the CacheElem. // It is a LRU cache with MaxIdelTime as response_expiration_time. - using QuotaLRUCache = utils::SimpleLRUCache; + using QuotaLRUCache = utils::SimpleLRUCache; // The quota options. QuotaOptions options_; diff --git a/src/istio/mixerclient/referenced.cc b/src/istio/mixerclient/referenced.cc index afc51f72527..464e44a8611 100644 --- a/src/istio/mixerclient/referenced.cc +++ b/src/istio/mixerclient/referenced.cc @@ -31,6 +31,7 @@ namespace mixerclient { namespace { const char kDelimiter[] = "\0"; const int kDelimiterLength = 1; +const size_t kMaxConcatHashSize = 4096; const std::string kWordDelimiter = ":"; // Decode dereferences index into str using global and local word lists. @@ -63,7 +64,7 @@ bool Decode(int idx, const std::vector &global_words, // Updates hasher with keys void Referenced::UpdateHash(const std::vector &keys, - utils::MD5 *hasher) { + utils::ConcatHash *hasher) { // keys are already sorted during Fill for (const AttributeRef &key : keys) { hasher->Update(key.name); @@ -116,7 +117,7 @@ bool Referenced::Fill(const Attributes &attributes, bool Referenced::Signature(const Attributes &attributes, const std::string &extra_key, - std::string *signature) const { + utils::HashType *signature) const { if (!CheckAbsentKeys(attributes) || !CheckExactKeys(attributes)) { return false; } @@ -200,10 +201,10 @@ bool Referenced::CheckExactKeys(const Attributes &attributes) const { void Referenced::CalculateSignature(const Attributes &attributes, const std::string &extra_key, - std::string *signature) const { + utils::HashType *signature) const { const auto &attributes_map = attributes.attributes(); - utils::MD5 hasher; + utils::ConcatHash hasher(kMaxConcatHashSize); for (std::size_t i = 0; i < exact_keys_.size(); ++i) { const auto &key = exact_keys_[i]; const auto it = attributes_map.find(key.name); @@ -274,18 +275,18 @@ void Referenced::CalculateSignature(const Attributes &attributes, } hasher.Update(extra_key); - *signature = hasher.Digest(); + *signature = hasher.getHash(); } -std::string Referenced::Hash() const { - utils::MD5 hasher; +utils::HashType Referenced::Hash() const { + utils::ConcatHash hasher(kMaxConcatHashSize); // keys are sorted during Fill UpdateHash(absence_keys_, &hasher); hasher.Update(kWordDelimiter); UpdateHash(exact_keys_, &hasher); - return hasher.Digest(); + return hasher.getHash(); } std::string Referenced::DebugString() const { diff --git a/src/istio/mixerclient/referenced.h b/src/istio/mixerclient/referenced.h index ec95680dd7b..002b057efb6 100644 --- a/src/istio/mixerclient/referenced.h +++ b/src/istio/mixerclient/referenced.h @@ -18,8 +18,8 @@ #include -#include "include/istio/utils/md5.h" -#include "mixer/v1/check.pb.h" +#include "include/istio/utils/concat_hash.h" +#include "mixer/v1/mixer.pb.h" namespace istio { namespace mixerclient { @@ -37,13 +37,13 @@ class Referenced { // Calculate a cache signature for the attributes. // Return false if attributes are mismatched, such as "absence" attributes - // present - // or "exact" match attributes don't present. + // present or "exact" match attributes don't present. bool Signature(const ::istio::mixer::v1::Attributes &attributes, - const std::string &extra_key, std::string *signature) const; + const std::string &extra_key, + utils::HashType *signature) const; // A hash value to identify an instance. - std::string Hash() const; + utils::HashType Hash() const; // For debug logging only. std::string DebugString() const; @@ -58,7 +58,7 @@ class Referenced { // Do the actual signature calculation. void CalculateSignature(const ::istio::mixer::v1::Attributes &attributes, const std::string &extra_key, - std::string *signature) const; + utils::HashType *signature) const; // Holds reference to an attribute and potentially a map key struct AttributeRef { @@ -86,7 +86,7 @@ class Referenced { // Updates hasher with keys static void UpdateHash(const std::vector &keys, - utils::MD5 *hasher); + utils::ConcatHash *hasher); }; } // namespace mixerclient diff --git a/src/istio/mixerclient/referenced_test.cc b/src/istio/mixerclient/referenced_test.cc index 3eb934468a5..7a55431da10 100644 --- a/src/istio/mixerclient/referenced_test.cc +++ b/src/istio/mixerclient/referenced_test.cc @@ -16,7 +16,7 @@ #include "src/istio/mixerclient/referenced.h" #include "include/istio/utils/attributes_builder.h" -#include "include/istio/utils/md5.h" +#include "include/istio/utils/concat_hash.h" #include "google/protobuf/text_format.h" #include "gtest/gtest.h" @@ -176,9 +176,6 @@ TEST(ReferencedTest, FillSuccessTest) { "target.service, Exact-keys: bool-key, bytes-key, double-key, " "duration-key, int-key, string-key, string-map-key[If-Match], " "time-key, "); - - EXPECT_EQ(utils::MD5::DebugString(referenced.Hash()), - "602d5bbd45b623c3560d2bdb6104f3ab"); } TEST(ReferencedTest, FillFail1Test) { @@ -207,7 +204,7 @@ TEST(ReferencedTest, NegativeSignature1Test) { Referenced referenced; EXPECT_TRUE(referenced.Fill(attrs, pb)); - std::string signature; + utils::HashType signature; Attributes attributes1; // "target.service" should be absence. @@ -246,11 +243,8 @@ TEST(ReferencedTest, OKSignature1Test) { Referenced referenced; EXPECT_TRUE(referenced.Fill(attributes, pb)); - std::string signature; + utils::HashType signature; EXPECT_TRUE(referenced.Signature(attributes, "extra", &signature)); - - EXPECT_EQ(utils::MD5::DebugString(signature), - "751b028b2e2c230ef9c4e59ac556ca04"); } TEST(ReferencedTest, StringMapReferencedTest) { @@ -269,10 +263,8 @@ TEST(ReferencedTest, StringMapReferencedTest) { Referenced referenced; EXPECT_TRUE(referenced.Fill(attrs, pb)); - std::string signature; + utils::HashType signature; EXPECT_TRUE(referenced.Signature(attrs, "extra", &signature)); - EXPECT_EQ(utils::MD5::DebugString(signature), - "bc055468af1a0d4d03ec7f6fa2265b9b"); // negative test: map-key3 must absence ::istio::mixer::v1::Attributes attr1(attrs); diff --git a/src/istio/mixerclient/report_batch.cc b/src/istio/mixerclient/report_batch.cc index a5a5a6c821b..5bb07ef436f 100644 --- a/src/istio/mixerclient/report_batch.cc +++ b/src/istio/mixerclient/report_batch.cc @@ -33,6 +33,7 @@ ReportBatch::ReportBatch(const ReportOptions& options, transport_(transport), timer_create_(timer_create), compressor_(compressor), + batch_compressor_(compressor.CreateBatchCompressor()), total_report_calls_(0), total_remote_report_calls_(0) {} @@ -41,17 +42,7 @@ ReportBatch::~ReportBatch() { Flush(); } void ReportBatch::Report(const Attributes& request) { std::lock_guard lock(mutex_); ++total_report_calls_; - if (!batch_compressor_) { - batch_compressor_ = compressor_.CreateBatchCompressor(); - } - - if (!batch_compressor_->Add(request)) { - FlushWithLock(); - - batch_compressor_ = compressor_.CreateBatchCompressor(); - batch_compressor_->Add(request); - } - + batch_compressor_->Add(request); if (batch_compressor_->size() >= options_.max_batch_entries) { FlushWithLock(); } else { @@ -65,19 +56,18 @@ void ReportBatch::Report(const Attributes& request) { } void ReportBatch::FlushWithLock() { - if (!batch_compressor_) { + if (batch_compressor_->size() == 0) { return; } - ++total_remote_report_calls_; - std::unique_ptr request = batch_compressor_->Finish(); - batch_compressor_.reset(); if (timer_) { timer_->Stop(); } + ++total_remote_report_calls_; + auto request = batch_compressor_->Finish(); ReportResponse* response = new ReportResponse; - transport_(*request, response, [this, response](const Status& status) { + transport_(request, response, [this, response](const Status& status) { delete response; if (!status.ok()) { GOOGLE_LOG(ERROR) << "Mixer Report failed with: " << status.ToString(); @@ -86,6 +76,8 @@ void ReportBatch::FlushWithLock() { } } }); + + batch_compressor_->Clear(); } void ReportBatch::Flush() { diff --git a/src/istio/mixerclient/report_batch_test.cc b/src/istio/mixerclient/report_batch_test.cc index 8893d8e4535..06cc7156ded 100644 --- a/src/istio/mixerclient/report_batch_test.cc +++ b/src/istio/mixerclient/report_batch_test.cc @@ -65,7 +65,7 @@ class ReportBatchTest : public ::testing::Test { }; } - MockReportTransport mock_report_transport_; + ::testing::NiceMock mock_report_transport_; MockTimer* mock_timer_; AttributeCompressor compressor_; std::unique_ptr batch_; @@ -106,29 +106,6 @@ TEST_F(ReportBatchTest, TestBatchReport) { EXPECT_EQ(report_call_count, 4); } -TEST_F(ReportBatchTest, TestNoDeltaUpdate) { - int report_call_count = 0; - EXPECT_CALL(mock_report_transport_, Report(_, _, _)) - .WillRepeatedly(Invoke([&](const ReportRequest& request, - ReportResponse* response, DoneFunc on_done) { - report_call_count++; - on_done(Status::OK); - })); - - Attributes report; - utils::AttributesBuilder(&report).AddString("key", "value"); - batch_->Report(report); - EXPECT_EQ(report_call_count, 0); - - // Erase a key, so delta update fail to push the batched result. - report.mutable_attributes()->erase("key"); - batch_->Report(report); - EXPECT_EQ(report_call_count, 1); - - batch_->Flush(); - EXPECT_EQ(report_call_count, 2); -} - TEST_F(ReportBatchTest, TestBatchReportWithTimeout) { int report_call_count = 0; EXPECT_CALL(mock_report_transport_, Report(_, _, _)) diff --git a/src/istio/utils/BUILD b/src/istio/utils/BUILD index a1ec20a2851..4cc7ff72413 100644 --- a/src/istio/utils/BUILD +++ b/src/istio/utils/BUILD @@ -17,53 +17,46 @@ licenses(["notice"]) cc_library( name = "utils_lib", srcs = [ + "local_attributes.cc", "protobuf.cc", "status.cc", + "utils.cc" ], - visibility = ["//visibility:public"], - deps = [ - "//external:protobuf", - "//include/istio/utils:headers_lib", + hdrs = [ + "utils.h", ], -) - -cc_library( - name = "md5_lib", - srcs = ["md5.cc"], visibility = ["//visibility:public"], deps = [ - "//external:boringssl_crypto", + ":attribute_names_lib", + "//external:protobuf", "//include/istio/utils:headers_lib", + "//include/istio/utils:attribute_names_header", + "//external:mixer_client_config_cc_proto", ], ) cc_test( - name = "simple_lru_cache_test", + name = "utils_test", size = "small", - srcs = ["simple_lru_cache_test.cc"], - linkopts = [ - "-lm", - "-lpthread", - ], - linkstatic = 1, + srcs = ["utils_test.cc"], deps = [ + ":utils_lib", "//external:googletest_main", - "//include/istio/utils:simple_lru_cache", ], ) cc_test( - name = "md5_test", + name = "simple_lru_cache_test", size = "small", - srcs = ["md5_test.cc"], + srcs = ["simple_lru_cache_test.cc"], linkopts = [ "-lm", "-lpthread", ], linkstatic = 1, deps = [ - ":md5_lib", "//external:googletest_main", + "//include/istio/utils:simple_lru_cache", ], ) diff --git a/src/istio/utils/attribute_names.cc b/src/istio/utils/attribute_names.cc index d42e0666b38..1e30084c91b 100644 --- a/src/istio/utils/attribute_names.cc +++ b/src/istio/utils/attribute_names.cc @@ -21,8 +21,18 @@ namespace utils { // Define attribute names const char AttributeName::kSourceUser[] = "source.user"; const char AttributeName::kSourcePrincipal[] = "source.principal"; +const char AttributeName::kSourceNamespace[] = "source.namespace"; +const char AttributeName::kSourceUID[] = "source.uid"; const char AttributeName::kDestinationPrincipal[] = "destination.principal"; +const char AttributeName::kDestinationServiceName[] = + "destination.service.name"; +const char AttributeName::kDestinationServiceUID[] = "destination.service.uid"; +const char AttributeName::kDestinationServiceHost[] = + "destination.service.host"; +const char AttributeName::kDestinationServiceNamespace[] = + "destination.service.namespace"; + const char AttributeName::kRequestHeaders[] = "request.headers"; const char AttributeName::kRequestHost[] = "request.host"; const char AttributeName::kRequestMethod[] = "request.method"; @@ -52,10 +62,11 @@ const char AttributeName::kSourcePort[] = "source.port"; const char AttributeName::kDestinationIp[] = "destination.ip"; const char AttributeName::kDestinationPort[] = "destination.port"; const char AttributeName::kDestinationUID[] = "destination.uid"; +const char AttributeName::kDestinationNamespace[] = "destination.namespace"; const char AttributeName::kOriginIp[] = "origin.ip"; -const char AttributeName::kConnectionReceviedBytes[] = +const char AttributeName::kConnectionReceivedBytes[] = "connection.received.bytes"; -const char AttributeName::kConnectionReceviedTotalBytes[] = +const char AttributeName::kConnectionReceivedTotalBytes[] = "connection.received.bytes_total"; const char AttributeName::kConnectionSendBytes[] = "connection.sent.bytes"; const char AttributeName::kConnectionSendTotalBytes[] = @@ -71,8 +82,10 @@ const char AttributeName::kConnectionEvent[] = "connection.event"; // Context attributes const char AttributeName::kContextProtocol[] = "context.protocol"; +const char AttributeName::kContextReporterKind[] = "context.reporter.kind"; const char AttributeName::kContextTime[] = "context.time"; const char AttributeName::kContextProxyErrorCode[] = "context.proxy_error_code"; +const char AttributeName::kContextReporterUID[] = "context.reporter.uid"; // Check error code and message. const char AttributeName::kCheckErrorCode[] = "check.error_code"; diff --git a/src/istio/utils/local_attributes.cc b/src/istio/utils/local_attributes.cc new file mode 100644 index 00000000000..3b209ea7f19 --- /dev/null +++ b/src/istio/utils/local_attributes.cc @@ -0,0 +1,72 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "include/istio/utils/local_attributes.h" +#include "include/istio/utils/attribute_names.h" +#include "include/istio/utils/attributes_builder.h" + +namespace istio { +namespace utils { + +namespace { +const char kReporterOutbound[] = "outbound"; +} // namespace + +// create Local attributes object and return a pointer to it. +// Should be freed by the caller. +void CreateLocalAttributes(const LocalNode& local, + LocalAttributes* local_attributes) { + ::istio::mixer::v1::Attributes inbound; + AttributesBuilder ib(&local_attributes->inbound); + ib.AddString(AttributeName::kDestinationUID, local.uid); + ib.AddString(AttributeName::kContextReporterUID, local.uid); + ib.AddString(AttributeName::kDestinationNamespace, local.ns); + + AttributesBuilder ob(&local_attributes->outbound); + ob.AddString(AttributeName::kSourceUID, local.uid); + ob.AddString(AttributeName::kContextReporterUID, local.uid); + ob.AddString(AttributeName::kSourceNamespace, local.ns); + + AttributesBuilder(&local_attributes->forward) + .AddString(AttributeName::kSourceUID, local.uid); +} + +// create preserialized header to send to proxy that is fronting mixer. +// This header is used for istio self monitoring. +bool SerializeForwardedAttributes(const LocalNode& local, + std::string* serialized_forward_attributes) { + ::istio::mixer::v1::Attributes attributes; + AttributesBuilder(&attributes) + .AddString(AttributeName::kSourceUID, local.uid); + return attributes.SerializeToString(serialized_forward_attributes); +} + +// check if this listener is outbound based on "context.reporter.kind" attribute +bool IsOutbound(const ::istio::mixer::v1::Attributes& attributes) { + bool outbound = false; + const auto& attributes_map = attributes.attributes(); + const auto it = + attributes_map.find(::istio::utils::AttributeName::kContextReporterKind); + if (it != attributes_map.end()) { + const ::istio::mixer::v1::Attributes_AttributeValue& value = it->second; + if (kReporterOutbound == value.string_value()) { + outbound = true; + } + } + return outbound; +} + +} // namespace utils +} // namespace istio diff --git a/src/istio/utils/md5.cc b/src/istio/utils/md5.cc deleted file mode 100644 index e1c3b89d341..00000000000 --- a/src/istio/utils/md5.cc +++ /dev/null @@ -1,55 +0,0 @@ -/* Copyright 2017 Istio Authors. All Rights Reserved. - * - * Licensed 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 "include/istio/utils/md5.h" -#include - -namespace istio { -namespace utils { - -MD5::MD5() : finalized_(false) { MD5_Init(&ctx_); } - -MD5& MD5::Update(const void* data, size_t size) { - // Not update after finalized. - assert(!finalized_); - MD5_Update(&ctx_, data, size); - return *this; -} - -std::string MD5::Digest() { - if (!finalized_) { - MD5_Final(digest_, &ctx_); - finalized_ = true; - } - return std::string(reinterpret_cast(digest_), kDigestLength); -} - -std::string MD5::DebugString(const std::string& digest) { - assert(digest.size() == kDigestLength); - char buf[kDigestLength * 2 + 1]; - char* p = buf; - for (int i = 0; i < kDigestLength; i++, p += 2) { - sprintf(p, "%02x", (unsigned char)digest[i]); - } - *p = 0; - return std::string(buf, kDigestLength * 2); -} - -std::string MD5::operator()(const void* data, size_t size) { - return Update(data, size).Digest(); -} - -} // namespace utils -} // namespace istio diff --git a/src/istio/utils/md5_test.cc b/src/istio/utils/md5_test.cc deleted file mode 100644 index b8279217167..00000000000 --- a/src/istio/utils/md5_test.cc +++ /dev/null @@ -1,41 +0,0 @@ -/* Copyright 2017 Istio Authors. All Rights Reserved. - * - * Licensed 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 "include/istio/utils/md5.h" -#include "gtest/gtest.h" - -namespace istio { -namespace utils { -namespace { - -TEST(MD5Test, TestPriableGigest) { - static const char data[] = "Test Data"; - ASSERT_EQ("0a22b2ac9d829ff3605d81d5ae5e9d16", - MD5::DebugString(MD5()(data, sizeof(data)))); -} - -TEST(MD5Test, TestGigestEqual) { - static const char data1[] = "Test Data1"; - static const char data2[] = "Test Data2"; - auto d1 = MD5()(data1, sizeof(data1)); - auto d11 = MD5()(data1, sizeof(data1)); - auto d2 = MD5()(data2, sizeof(data2)); - ASSERT_EQ(d11, d1); - ASSERT_NE(d1, d2); -} - -} // namespace -} // namespace utils -} // namespace istio diff --git a/src/istio/utils/utils.cc b/src/istio/utils/utils.cc new file mode 100644 index 00000000000..0d6151a9ca8 --- /dev/null +++ b/src/istio/utils/utils.cc @@ -0,0 +1,49 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "src/istio/utils/utils.h" + +#include +#include + +namespace istio { +namespace utils { + +namespace { +const std::string kNamespaceKey("/ns/"); +const char kDelimiter = '/'; +} // namespace + +bool GetSourceNamespace(const std::string& principal, + std::string* source_namespace) { + if (source_namespace) { + // The namespace is a substring in principal with format: + // "/ns//sa/". '/' is not allowed to + // appear in actual content except as delimiter between tokens. + size_t begin = principal.find(kNamespaceKey); + if (begin == std::string::npos) { + return false; + } + begin += kNamespaceKey.length(); + size_t end = principal.find(kDelimiter, begin); + size_t len = (end == std::string::npos ? end : end - begin); + *source_namespace = principal.substr(begin, len); + return true; + } + return false; +} + +} // namespace utils +} // namespace istio diff --git a/src/envoy/alts/transport_security_interface_wrapper.h b/src/istio/utils/utils.h similarity index 53% rename from src/envoy/alts/transport_security_interface_wrapper.h rename to src/istio/utils/utils.h index 9e6c60e60b2..15914a3aad2 100644 --- a/src/envoy/alts/transport_security_interface_wrapper.h +++ b/src/istio/utils/utils.h @@ -13,14 +13,16 @@ * limitations under the License. */ -// Some gRPC headers contains old style cast and unused parameter which doesn't -// compile with -Werror, ignoring those compiler warning since we don't have -// control on those source codes. This works with GCC and Clang. +#pragma once -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" -#pragma GCC diagnostic ignored "-Wold-style-cast" -#include "grpc/grpc_security.h" -#include "src/core/tsi/alts/handshaker/alts_tsi_handshaker.h" -#include "src/core/tsi/transport_security_interface.h" -#pragma GCC diagnostic pop +#include +#include "google/protobuf/struct.pb.h" + +namespace istio { +namespace utils { + +// Get source.namespace attribute from principal. +bool GetSourceNamespace(const std::string& principal, + std::string* source_namespace); +} // namespace utils +} // namespace istio \ No newline at end of file diff --git a/src/istio/utils/utils_test.cc b/src/istio/utils/utils_test.cc new file mode 100644 index 00000000000..4282e4edccd --- /dev/null +++ b/src/istio/utils/utils_test.cc @@ -0,0 +1,68 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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 "src/istio/utils/utils.h" +#include "gtest/gtest.h" + +namespace istio { +namespace utils { +namespace { + +class UtilsTest : public ::testing::Test { + protected: + void checkFalse(const std::string& principal) { + std::string output_ns = "none"; + EXPECT_FALSE(GetSourceNamespace(principal, &output_ns)); + EXPECT_EQ(output_ns, output_ns); + } + + void checkTrue(const std::string& principal, const std::string& ns) { + std::string output_ns = "none"; + EXPECT_TRUE(GetSourceNamespace(principal, &output_ns)); + EXPECT_EQ(ns, output_ns); + } +}; + +TEST_F(UtilsTest, GetSourceNamespace) { + checkFalse(""); + checkFalse("cluster.local"); + checkFalse("cluster.local/"); + checkFalse("cluster.local/ns"); + checkFalse("cluster.local/sa/user"); + checkFalse("cluster.local/sa/user/ns"); + checkFalse("cluster.local/sa/user_ns/"); + checkFalse("cluster.local/sa/user_ns/abc/xyz"); + checkFalse("cluster.local/NS/abc"); + + checkTrue("cluster.local/ns/", ""); + checkTrue("cluster.local/ns//", ""); + checkTrue("cluster.local/sa/user/ns/", ""); + checkTrue("cluster.local/ns//sa/user", ""); + checkTrue("cluster.local/ns//ns/ns", ""); + + checkTrue("cluster.local/ns/ns/ns/ns", "ns"); + checkTrue("cluster.local/ns/abc_ns", "abc_ns"); + checkTrue("cluster.local/ns/abc_ns/", "abc_ns"); + checkTrue("cluster.local/ns/abc_ns/sa/user_ns", "abc_ns"); + checkTrue("cluster.local/ns/abc_ns/sa/user_ns/other/xyz", "abc_ns"); + checkTrue("cluster.local/sa/user_ns/ns/abc", "abc"); + checkTrue("cluster.local/sa/user_ns/ns/abc/", "abc"); + checkTrue("cluster.local/sa/user_ns/ns/abc_ns", "abc_ns"); + checkTrue("cluster.local/sa/user_ns/ns/abc_ns/", "abc_ns"); +} + +} // namespace +} // namespace utils +} // namespace istio diff --git a/test/integration/BUILD b/test/integration/BUILD index 6d4d98562b2..5ea6b708487 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -35,4 +35,20 @@ envoy_cc_test( "//src/envoy/utils:filter_names_lib", "//src/envoy/http/mixer:filter_lib", ], -) \ No newline at end of file +) + +envoy_cc_test( + name = "exchanged_token_integration_test", + srcs = ["exchanged_token_integration_test.cc"], + repository = "@envoy", + deps = [ + "@envoy//source/common/common:utility_lib", + "@envoy//test/integration:http_protocol_integration_lib", + "//include/istio/utils:attribute_names_header", + "//src/envoy/http/authn:filter_lib", + "//src/envoy/http/jwt_auth:http_filter_factory", + "//src/envoy/http/jwt_auth:jwt_lib", + "//src/envoy/utils:filter_names_lib", + "//src/envoy/http/mixer:filter_lib", + ], +) diff --git a/test/integration/exchanged_token_integration_test.cc b/test/integration/exchanged_token_integration_test.cc new file mode 100644 index 00000000000..5a72110d953 --- /dev/null +++ b/test/integration/exchanged_token_integration_test.cc @@ -0,0 +1,419 @@ +/* Copyright 2018 Istio Authors. All Rights Reserved. + * + * Licensed 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. + */ + +// The integration tests in this file test the end-to-end behaviour of +// an exchanged token when going through the HTTP filter chains +// (jwt-authn + istio-authn + istio-mixer). Filters pass on processing +// results next filters using the request info through dynamic metadata +// and the results generated by the filters can be observed at the mixer +// backend). + +#include "fmt/printf.h" +#include "gmock/gmock.h" +#include "include/istio/utils/attribute_names.h" +#include "mixer/v1/mixer.pb.h" +#include "src/envoy/utils/filter_names.h" +#include "test/integration/http_protocol_integration.h" + +using ::google::protobuf::util::error::Code; +using ::testing::Contains; +using ::testing::Not; + +namespace Envoy { +namespace { + +// An example exchanged token +constexpr char kExchangedToken[] = + "istio " + "eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0Un" + "pIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJleGFtcGxlLWF1ZGllbmNlIiwiZW1ha" + "WwiOiJmb29AZ29vZ2xlLmNvbSIsImV4cCI6NDY5ODM2MTUwOCwiaWF0IjoxNTQ0NzYxNTA4LCJ" + "pc3MiOiJodHRwczovL2V4YW1wbGUudG9rZW5fc2VydmljZS5jb20iLCJpc3Rpb19hdHRyaWJ1d" + "GVzIjpbeyJzb3VyY2UuaXAiOiIxMjcuMC4wLjEifV0sImtleTEiOlsidmFsMiIsInZhbDMiXSw" + "ib3JpZ2luYWxfY2xhaW1zIjp7ImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlzcyI6Imh0d" + "HBzOi8vYWNjb3VudHMuZXhhbXBsZS5jb20iLCJzdWIiOiJleGFtcGxlLXN1YmplY3QifSwic3V" + "iIjoiaHR0cHM6Ly9hY2NvdW50cy5leGFtcGxlLmNvbS8xMjM0NTU2Nzg5MCJ9.mLm9Gmcd748a" + "nwybiPxGPEuYgJBChqoHkVOvRhQN-H9jMqVKyF-7ynud1CJp5n72VeMB1FzvKAV0ErzSyWQc0i" + "ofQywG6whYXP6zL-Oc0igUrLDvzb6PuBDkbWOcZrvHkHM4tIYAkF4j880GqMWEP3gGrykziIEY" + "9g4povquCFSdkLjjyol2-Ge_6MFdayYoeWLLOaMP7tHiPTm_ajioQ4jcz5whBWu3DZWx4IuU5U" + "IBYlHG_miJZv5zmwwQ60T1_p_sW7zkABJgDhCvu6cHh6g-hZdQvZbATFwMfN8VDzttTjRG8wuL" + "lkQ1TTOCx5PDv-_gHfQfRWt8Z94HrIJPuQ"; + +// An example token without original_claims +constexpr char kTokenWithoutOriginalClaims[] = + "eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0Un" + "pIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJleGFtcGxlLWF1ZGllbmNlIiwiZW1ha" + "WwiOiJmb29AZ29vZ2xlLmNvbSIsImV4cCI6NDY5ODcyNzc2NiwiaWF0IjoxNTQ1MTI3NzY2LCJ" + "pc3MiOiJodHRwczovL2V4YW1wbGUudG9rZW5fc2VydmljZS5jb20iLCJpc3Rpb19hdHRyaWJ1d" + "GVzIjpbeyJzb3VyY2UuaXAiOiIxMjcuMC4wLjEifV0sImtleTEiOlsidmFsMiIsInZhbDMiXSw" + "ic3ViIjoiaHR0cHM6Ly9hY2NvdW50cy5leGFtcGxlLmNvbS8xMjM0NTU2Nzg5MCJ9.FVskjGxS" + "cTuNFtKGRnQvQgejgcdPbunCAbXlj_ZYMawrHIYnrMt_Ddw5nOojxQu2zfkwoB004196ozNjDR" + "ED4jpJA0T6HP7hyTHGbrp6h6Z4dQ_PcmAxdR2_g8GEo-bcJ-CcbATEyBtrDqLtFcgP-ev_ctAo" + "BQHGp7qMgdpkQIJ07BTT1n6mghPFFCnA__RYWjPUwMLGZs_bOtWxHYbd-bkDSwg4Kbtf5-9oPI" + "nwJc6oMGMVzdjmJYMadg5GEor5XhgYz3TThPzLlEsxa0loD9eJDBGgdwjA1cLuAGgM7_HgRfg7" + "8ameSmQgSCsNlFB4k3ODeC-YC62KYdZ5Jdrg2A"; + +constexpr char kExpectedPrincipal[] = + "https://accounts.example.com/example-subject"; +constexpr char kDestinationNamespace[] = "pod"; +constexpr char kDestinationUID[] = "kubernetes://dest.pod"; +constexpr char kSourceUID[] = "kubernetes://src.pod"; +constexpr char kTelemetryBackend[] = "telemetry-backend"; +constexpr char kPolicyBackend[] = "policy-backend"; +const std::string kHeaderForExchangedToken = "ingress-authorization"; + +// Generates basic test request header. +Http::TestHeaderMapImpl BaseRequestHeaders() { + return Http::TestHeaderMapImpl{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "10.0.0.1"}}; +} + +// Generates test request header with given token. +Http::TestHeaderMapImpl HeadersWithToken(const std::string& header, + const std::string& token) { + auto headers = BaseRequestHeaders(); + headers.addCopy(header, token); + return headers; +} + +std::string MakeJwtFilterConfig() { + constexpr char kJwtFilterTemplate[] = R"( + name: %s + config: + rules: + - issuer: "https://example.token_service.com" + from_headers: + - name: ingress-authorization + local_jwks: + inline_string: "%s" + - issuer: "testing-rbac@secure.istio.io" + local_jwks: + inline_string: "%s" + allow_missing_or_failed: true + )"; + // From + // https://github.com/istio/istio/blob/master/security/tools/jwt/samples/jwks.json + constexpr char kJwksInline[] = + "{ \"keys\":[ " + "{\"e\":\"AQAB\",\"kid\":\"DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ\"," + "\"kty\":\"RSA\",\"n\":\"xAE7eB6qugXyCAG3yhh7pkDkT65pHymX-" + "P7KfIupjf59vsdo91bSP9C8H07pSAGQO1MV" + "_xFj9VswgsCg4R6otmg5PV2He95lZdHtOcU5DXIg_" + "pbhLdKXbi66GlVeK6ABZOUW3WYtnNHD-91gVuoeJT_" + "DwtGGcp4ignkgXfkiEm4sw-4sfb4qdt5oLbyVpmW6x9cfa7vs2WTfURiCrBoUqgBo_-" + "4WTiULmmHSGZHOjzwa8WtrtOQGsAFjIbno85jp6MnGGGZPYZbDAa_b3y5u-" + "YpW7ypZrvD8BgtKVjgtQgZhLAGezMt0ua3DRrWnKqTZ0BJ_EyxOGuHJrLsn00fnMQ\"}]}"; + + return fmt::sprintf(kJwtFilterTemplate, Utils::IstioFilterName::kJwt, + StringUtil::escape(kJwksInline), + StringUtil::escape(kJwksInline)); +} + +std::string MakeAuthFilterConfig() { + constexpr char kAuthnFilterWithJwtTemplate[] = R"( + name: %s + config: + policy: + origins: + - jwt: + issuer: https://example.token_service.com + jwt_headers: + - ingress-authorization + principalBinding: USE_ORIGIN +)"; + return fmt::sprintf(kAuthnFilterWithJwtTemplate, + Utils::IstioFilterName::kAuthentication); +} + +std::string MakeRbacFilterConfig() { + constexpr char kRbacFilterTemplate[] = R"( + name: envoy.filters.http.rbac + config: + rules: + policies: + "foo": + permissions: + - any: true + principals: + - metadata: + filter: %s + path: + - key: %s + value: + string_match: + exact: %s +)"; + return fmt::sprintf( + kRbacFilterTemplate, Utils::IstioFilterName::kAuthentication, + istio::utils::AttributeName::kRequestAuthPrincipal, kExpectedPrincipal); +} + +std::string MakeMixerFilterConfig() { + constexpr char kMixerFilterTemplate[] = R"( + name: mixer + config: + defaultDestinationService: "default" + mixerAttributes: + attributes: { + } + serviceConfigs: { + "default": {} + } + transport: + attributes_for_mixer_proxy: + attributes: { + "source.uid": { + string_value: %s + } + } + report_cluster: %s + check_cluster: %s + )"; + return fmt::sprintf(kMixerFilterTemplate, kSourceUID, kTelemetryBackend, + kPolicyBackend); +} + +class ExchangedTokenIntegrationTest : public HttpProtocolIntegrationTest { + public: + void createUpstreams() override { + HttpProtocolIntegrationTest::createUpstreams(); + fake_upstreams_.emplace_back(new FakeUpstream( + 0, FakeHttpConnection::Type::HTTP2, version_, timeSystem())); + telemetry_upstream_ = fake_upstreams_.back().get(); + + fake_upstreams_.emplace_back(new FakeUpstream( + 0, FakeHttpConnection::Type::HTTP2, version_, timeSystem())); + policy_upstream_ = fake_upstreams_.back().get(); + } + + void SetUp() override { + config_helper_.addConfigModifier(addNodeMetadata()); + + config_helper_.addFilter(MakeMixerFilterConfig()); + config_helper_.addFilter(MakeRbacFilterConfig()); + config_helper_.addFilter(MakeAuthFilterConfig()); + config_helper_.addFilter(MakeJwtFilterConfig()); + + config_helper_.addConfigModifier(addCluster(kTelemetryBackend)); + config_helper_.addConfigModifier(addCluster(kPolicyBackend)); + + HttpProtocolIntegrationTest::initialize(); + } + + void TearDown() override { + cleanupConnection(fake_upstream_connection_); + cleanupConnection(telemetry_connection_); + cleanupConnection(policy_connection_); + } + + ConfigHelper::ConfigModifierFunction addNodeMetadata() { + return [](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { + ::google::protobuf::Struct meta; + MessageUtil::loadFromJson( + fmt::sprintf(R"({ + "ISTIO_VERSION": "1.0.1", + "NODE_UID": "%s", + "NODE_NAMESPACE": "%s" + })", + kDestinationUID, kDestinationNamespace), + meta); + bootstrap.mutable_node()->mutable_metadata()->MergeFrom(meta); + }; + } + + ConfigHelper::ConfigModifierFunction addCluster(const std::string& name) { + return [name](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { + auto* cluster = bootstrap.mutable_static_resources()->add_clusters(); + cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); + cluster->mutable_http2_protocol_options(); + cluster->set_name(name); + }; + } + + void waitForTelemetryRequest(::istio::mixer::v1::ReportRequest* request) { + AssertionResult result = telemetry_upstream_->waitForHttpConnection( + *dispatcher_, telemetry_connection_); + RELEASE_ASSERT(result, result.message()); + result = telemetry_connection_->waitForNewStream(*dispatcher_, + telemetry_request_); + RELEASE_ASSERT(result, result.message()); + + result = telemetry_request_->waitForGrpcMessage(*dispatcher_, *request); + RELEASE_ASSERT(result, result.message()); + } + + // Must be called after waitForTelemetryRequest + void sendTelemetryResponse() { + telemetry_request_->startGrpcStream(); + telemetry_request_->sendGrpcMessage(::istio::mixer::v1::ReportResponse{}); + telemetry_request_->finishGrpcStream(Grpc::Status::Ok); + } + + void waitForPolicyRequest(::istio::mixer::v1::CheckRequest* request) { + AssertionResult result = policy_upstream_->waitForHttpConnection( + *dispatcher_, policy_connection_); + RELEASE_ASSERT(result, result.message()); + result = + policy_connection_->waitForNewStream(*dispatcher_, policy_request_); + RELEASE_ASSERT(result, result.message()); + + result = policy_request_->waitForGrpcMessage(*dispatcher_, *request); + RELEASE_ASSERT(result, result.message()); + } + + // Must be called after waitForPolicyRequest + void sendPolicyResponse() { + policy_request_->startGrpcStream(); + ::istio::mixer::v1::CheckResponse response; + response.mutable_precondition()->mutable_status()->set_code(Code::OK); + policy_request_->sendGrpcMessage(response); + policy_request_->finishGrpcStream(Grpc::Status::Ok); + } + + void cleanupConnection(FakeHttpConnectionPtr& connection) { + if (connection != nullptr) { + AssertionResult result = connection->close(); + RELEASE_ASSERT(result, result.message()); + result = connection->waitForDisconnect(); + RELEASE_ASSERT(result, result.message()); + } + } + + FakeUpstream* telemetry_upstream_{}; + FakeHttpConnectionPtr telemetry_connection_{}; + FakeStreamPtr telemetry_request_{}; + + FakeUpstream* policy_upstream_{}; + FakeHttpConnectionPtr policy_connection_{}; + FakeStreamPtr policy_request_{}; +}; + +INSTANTIATE_TEST_CASE_P( + Protocols, ExchangedTokenIntegrationTest, + testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams()), + HttpProtocolIntegrationTest::protocolTestParamsToString); + +TEST_P(ExchangedTokenIntegrationTest, ValidExchangeToken) { + codec_client_ = + makeHttpConnection(makeClientConnection((lookupPort("http")))); + + // A valid exchanged token in the header for an exchanged token + auto response = codec_client_->makeHeaderOnlyRequest( + HeadersWithToken(kHeaderForExchangedToken, kExchangedToken)); + + ::istio::mixer::v1::CheckRequest check_request; + waitForPolicyRequest(&check_request); + // Check request should see the authn attributes in the original payload. + EXPECT_THAT( + check_request.attributes().words(), + ::testing::AllOf(Contains(kDestinationUID), Contains("10.0.0.1"), + Contains(kExpectedPrincipal), Contains("sub"), + Contains("example-subject"), Contains("iss"), + Contains("https://accounts.example.com"), + Contains("email"), Contains("user@example.com"))); + sendPolicyResponse(); + + waitForNextUpstreamRequest(0); + // Send backend response. + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "200"}}, + true); + response->waitForEndStream(); + + // Report is sent after the backend responds. + ::istio::mixer::v1::ReportRequest report_request; + waitForTelemetryRequest(&report_request); + // Report request should also see the same authn attributes. + EXPECT_THAT( + report_request.default_words(), + ::testing::AllOf(Contains(kDestinationUID), Contains("10.0.0.1"), + Contains(kExpectedPrincipal), Contains("sub"), + Contains("example-subject"), Contains("iss"), + Contains("https://accounts.example.com"), + Contains("email"), Contains("user@example.com"))); + + sendTelemetryResponse(); + + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("200", response->headers().Status()->value().c_str()); +} + +TEST_P(ExchangedTokenIntegrationTest, ValidExchangeTokenAtWrongHeader) { + codec_client_ = + makeHttpConnection(makeClientConnection((lookupPort("http")))); + + // When a token is not in the header for an exchanged token, + // it will not be regarded as an exchanged token. + auto response = codec_client_->makeHeaderOnlyRequest( + HeadersWithToken("wrong-header", kExchangedToken)); + + ::istio::mixer::v1::ReportRequest report_request; + waitForTelemetryRequest(&report_request); + EXPECT_THAT(report_request.default_words(), + ::testing::AllOf(Contains(kDestinationUID), Contains("10.0.0.1"), + Not(Contains(kExpectedPrincipal)))); + sendTelemetryResponse(); + + response->waitForEndStream(); + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("401", response->headers().Status()->value().c_str()); +} + +TEST_P(ExchangedTokenIntegrationTest, TokenWithoutOriginalClaims) { + codec_client_ = + makeHttpConnection(makeClientConnection((lookupPort("http")))); + + // When a token does not contain original_claims, + // it will be regarded as an invalid exchanged token. + auto response = codec_client_->makeHeaderOnlyRequest( + HeadersWithToken(kHeaderForExchangedToken, kTokenWithoutOriginalClaims)); + + ::istio::mixer::v1::ReportRequest report_request; + waitForTelemetryRequest(&report_request); + EXPECT_THAT(report_request.default_words(), + ::testing::AllOf(Contains(kDestinationUID), Contains("10.0.0.1"), + Not(Contains(kExpectedPrincipal)))); + sendTelemetryResponse(); + + response->waitForEndStream(); + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("401", response->headers().Status()->value().c_str()); +} + +TEST_P(ExchangedTokenIntegrationTest, InvalidExchangeToken) { + codec_client_ = + makeHttpConnection(makeClientConnection((lookupPort("http")))); + + // When an invalid exchanged token is in the header for an exchanged token, + // the request will be rejected. + auto response = codec_client_->makeHeaderOnlyRequest( + HeadersWithToken(kHeaderForExchangedToken, "invalid-token")); + + ::istio::mixer::v1::ReportRequest report_request; + waitForTelemetryRequest(&report_request); + EXPECT_THAT(report_request.default_words(), + ::testing::AllOf(Contains(kDestinationUID), Contains("10.0.0.1"), + Not(Contains(kExpectedPrincipal)))); + sendTelemetryResponse(); + + response->waitForEndStream(); + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("401", response->headers().Status()->value().c_str()); +} + +} // namespace +} // namespace Envoy diff --git a/test/integration/istio_http_integration_test.cc b/test/integration/istio_http_integration_test.cc index 059ada91897..c3f7a25912a 100644 --- a/test/integration/istio_http_integration_test.cc +++ b/test/integration/istio_http_integration_test.cc @@ -22,8 +22,7 @@ #include "fmt/printf.h" #include "gmock/gmock.h" #include "include/istio/utils/attribute_names.h" -#include "mixer/v1/check.pb.h" -#include "mixer/v1/report.pb.h" +#include "mixer/v1/mixer.pb.h" #include "src/envoy/utils/filter_names.h" #include "test/integration/http_protocol_integration.h" @@ -97,8 +96,10 @@ constexpr char kExpectedRawClaims[] = "{\"exp\":4685989700,\"foo\":\"bar\",\"iat\":1532389700,\"iss\":\"testing@" "secure.istio.io\"," "\"sub\":\"testing@secure.istio.io\"}"; -constexpr char kDestinationUID[] = "dest.pod.123"; -constexpr char kSourceUID[] = "src.pod.xyz"; + +constexpr char kDestinationNamespace[] = "pod"; +constexpr char kDestinationUID[] = "kubernetes://dest.pod"; +constexpr char kSourceUID[] = "kubernetes://src.pod"; constexpr char kTelemetryBackend[] = "telemetry-backend"; constexpr char kPolicyBackend[] = "policy-backend"; @@ -197,9 +198,6 @@ std::string MakeMixerFilterConfig() { defaultDestinationService: "default" mixerAttributes: attributes: { - "destination.uid": { - stringValue: %s - } } serviceConfigs: { "default": {} @@ -214,24 +212,26 @@ std::string MakeMixerFilterConfig() { report_cluster: %s check_cluster: %s )"; - return fmt::sprintf(kMixerFilterTemplate, kDestinationUID, kSourceUID, - kTelemetryBackend, kPolicyBackend); + return fmt::sprintf(kMixerFilterTemplate, kSourceUID, kTelemetryBackend, + kPolicyBackend); } class IstioHttpIntegrationTest : public HttpProtocolIntegrationTest { public: void createUpstreams() override { HttpProtocolIntegrationTest::createUpstreams(); - fake_upstreams_.emplace_back( - new FakeUpstream(0, FakeHttpConnection::Type::HTTP2, version_)); + fake_upstreams_.emplace_back(new FakeUpstream( + 0, FakeHttpConnection::Type::HTTP2, version_, timeSystem())); telemetry_upstream_ = fake_upstreams_.back().get(); - fake_upstreams_.emplace_back( - new FakeUpstream(0, FakeHttpConnection::Type::HTTP2, version_)); + fake_upstreams_.emplace_back(new FakeUpstream( + 0, FakeHttpConnection::Type::HTTP2, version_, timeSystem())); policy_upstream_ = fake_upstreams_.back().get(); } void SetUp() override { + config_helper_.addConfigModifier(addNodeMetadata()); + config_helper_.addFilter(MakeMixerFilterConfig()); config_helper_.addFilter(MakeRbacFilterConfig()); config_helper_.addFilter(MakeAuthFilterConfig()); @@ -249,6 +249,21 @@ class IstioHttpIntegrationTest : public HttpProtocolIntegrationTest { cleanupConnection(policy_connection_); } + ConfigHelper::ConfigModifierFunction addNodeMetadata() { + return [](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { + ::google::protobuf::Struct meta; + MessageUtil::loadFromJson( + fmt::sprintf(R"({ + "ISTIO_VERSION": "1.0.1", + "NODE_UID": "%s", + "NODE_NAMESPACE": "%s" + })", + kDestinationUID, kDestinationNamespace), + meta); + bootstrap.mutable_node()->mutable_metadata()->MergeFrom(meta); + }; + } + ConfigHelper::ConfigModifierFunction addCluster(const std::string& name) { return [name](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { auto* cluster = bootstrap.mutable_static_resources()->add_clusters(); diff --git a/tools/bazel.rc b/tools/bazel.rc index 75a93323f7c..70546ec7ed7 100644 --- a/tools/bazel.rc +++ b/tools/bazel.rc @@ -1,60 +1 @@ -# Copied from https://github.com/envoyproxy/envoy/blob/master/tools/bazel.rc -# Envoy specific Bazel build/test options. - -build --workspace_status_command=tools/bazel_get_workspace_status - -# Basic ASAN/UBSAN that works for gcc -build:asan --define ENVOY_CONFIG_ASAN=1 -build:asan --copt -fsanitize=address,undefined -build:asan --linkopt -fsanitize=address,undefined -build:asan --copt -fno-sanitize=vptr -build:asan --linkopt -fno-sanitize=vptr -build:asan --linkopt -ldl -build:asan --define tcmalloc=disabled -build:asan --build_tag_filters=-no_asan -build:asan --test_tag_filters=-no_asan -build:asan --define signal_trace=disabled - -# Clang 5.0 ASAN -build:clang-asan --define ENVOY_CONFIG_ASAN=1 -build:clang-asan --copt -D__SANITIZE_ADDRESS__ -build:clang-asan --copt -fsanitize=address,undefined -build:clang-asan --linkopt -fsanitize=address,undefined -build:clang-asan --copt -fno-sanitize=vptr -build:clang-asan --linkopt -fno-sanitize=vptr -build:clang-asan --copt -fno-sanitize-recover=all -build:clang-asan --linkopt -ldl -build:clang-asan --define tcmalloc=disabled -build:clang-asan --build_tag_filters=-no_asan -build:clang-asan --test_tag_filters=-no_asan -build:clang-asan --define signal_trace=disabled -build:clang-asan --test_env=ASAN_SYMBOLIZER_PATH - -# Clang 5.0 TSAN -build:clang-tsan --define ENVOY_CONFIG_TSAN=1 -build:clang-tsan --copt -fsanitize=thread -build:clang-tsan --linkopt -fsanitize=thread -build:clang-tsan --define tcmalloc=disabled - -# Clang 5.0 MSAN - broken today since we need to rebuild lib[std]c++ and external deps with MSAN -# support (see https://github.com/envoyproxy/envoy/issues/443). -build:clang-msan --define ENVOY_CONFIG_MSAN=1 -build:clang-msan --copt -fsanitize=memory -build:clang-msan --linkopt -fsanitize=memory -build:clang-msan --define tcmalloc=disabled -build:clang-msan --copt -fsanitize-memory-track-origins=2 - -# Test options -test --test_env=HEAPCHECK=normal --test_env=PPROF_PATH - -# Release builds without debug symbols. -build:release -c opt -build:release --strip=always - -# Release builds with debug symbols -build:release-symbol -c opt - -# Add compile option for all C++ files -build --cxxopt -Wnon-virtual-dtor -build --cxxopt -Wformat -build --cxxopt -Wformat-security +import %workspace%/.bazelrc diff --git a/tools/deb/README.md b/tools/deb/README.md index 514c3d00420..55c794e8948 100644 --- a/tools/deb/README.md +++ b/tools/deb/README.md @@ -66,7 +66,7 @@ to a service. The address is the ClusterIP of the service. }, ``` -4. For every TCP service, there is a listener on SERVICE_IP:port address, with a destionatio_ip_list. +4. For every TCP service, there is a listener on SERVICE_IP:port address, with a destination_ip_list. # RDS - or routes diff --git a/x_tools_imports.bzl b/x_tools_imports.bzl index 3f354a67b94..3f7ec45ac91 100644 --- a/x_tools_imports.bzl +++ b/x_tools_imports.bzl @@ -14,7 +14,11 @@ # ################################################################################ # -load("@bazel_tools//tools/build_defs/repo:git.bzl", "new_git_repository") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +# Jun 23, 2017 (no releases) +TOOLS_SHA = "e6cb469339aef5b7be0c89de730d5f3cc8e47e50" +TOOLS_SHA256 = "fe9489eabcb598e13137d0641525ff3813d8af151e1418e6940e611850d90136" def go_x_tools_imports_repositories(): BUILD_FILE = """ @@ -46,9 +50,10 @@ go_binary( # simple build rule that will build the binary for usage (and avoid # the need to project a more complicated BUILD file over the entire # tools repo.) - new_git_repository( + http_archive( name = "org_golang_x_tools_imports", build_file_content = BUILD_FILE, - commit = "e6cb469339aef5b7be0c89de730d5f3cc8e47e50", # Jun 23, 2017 (no releases) - remote = "https://github.com/golang/tools.git", + strip_prefix = "tools-" + TOOLS_SHA, + url = "https://github.com/golang/tools/archives/" + TOOLS_SHA + ".tar.gz", + sha256 = TOOLS_SHA256, )