diff --git a/source/common/filter/ext_authz.cc b/source/common/filter/ext_authz.cc index b89aab224158b..abefbdb681f40 100644 --- a/source/common/filter/ext_authz.cc +++ b/source/common/filter/ext_authz.cc @@ -19,14 +19,14 @@ InstanceStats Config::generateStats(const std::string& name, Stats::Scope& scope } void Instance::callCheck() { - CheckRequestUtils::createTcpCheck(filter_callbacks_, checkRequest_); + CheckRequestUtils::createTcpCheck(filter_callbacks_, check_request_); status_ = Status::Calling; config_->stats().active_.inc(); config_->stats().total_.inc(); calling_check_ = true; - client_->check(*this, checkRequest_, Tracing::NullSpan::instance()); + client_->check(*this, check_request_, Tracing::NullSpan::instance()); calling_check_ = false; } @@ -74,7 +74,8 @@ void Instance::onComplete(CheckStatus status) { } // Fail open only if configured to do so and if the check status was a error. - if (status == CheckStatus::Denied || (status == CheckStatus::Error && !config_->failOpen())) { + if (status == CheckStatus::Denied || + (status == CheckStatus::Error && !config_->failureModeAllow())) { config_->stats().cx_closed_.inc(); filter_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); } else { diff --git a/source/common/filter/ext_authz.h b/source/common/filter/ext_authz.h index b5d319a74c671..3dfc39be2d972 100644 --- a/source/common/filter/ext_authz.h +++ b/source/common/filter/ext_authz.h @@ -49,7 +49,7 @@ class Config { failure_mode_allow_(config.failure_mode_allow()) {} const InstanceStats& stats() { return stats_; } - bool failOpen() const { return failure_mode_allow_; } + bool failureModeAllow() const { return failure_mode_allow_; } void setFailModeAllow(bool value) { failure_mode_allow_ = value; } private: @@ -99,7 +99,7 @@ class Instance : public Network::ReadFilter, Network::ReadFilterCallbacks* filter_callbacks_{}; Status status_{Status::NotStarted}; bool calling_check_{}; - envoy::service::auth::v2::CheckRequest checkRequest_{}; + envoy::service::auth::v2::CheckRequest check_request_{}; }; } // TcpFilter diff --git a/source/common/http/filter/BUILD b/source/common/http/filter/BUILD index 69c784e867b2c..d8d7781c178a4 100644 --- a/source/common/http/filter/BUILD +++ b/source/common/http/filter/BUILD @@ -151,3 +151,37 @@ envoy_cc_library( "@envoy_api//envoy/config/filter/http/rate_limit/v2:rate_limit_cc", ], ) + +envoy_cc_library( + name = "ext_authz_lib", + srcs = ["ext_authz.cc"], + deps = [ + ":ext_authz_includes", + "//include/envoy/http:codes_interface", + "//source/common/common:assert_lib", + "//source/common/common:empty_string", + "//source/common/common:enum_to_int", + "//source/common/ext_authz:ext_authz_lib", + "//source/common/http:codes_lib", + "//source/common/router:config_lib", + ], +) + +envoy_cc_library( + name = "ext_authz_includes", + hdrs = ["ext_authz.h"], + deps = [ + "//include/envoy/access_log:access_log_interface", + "//include/envoy/ext_authz:ext_authz_interface", + "//include/envoy/http:filter_interface", + "//include/envoy/local_info:local_info_interface", + "//include/envoy/runtime:runtime_interface", + "//include/envoy/upstream:cluster_manager_interface", + "//source/common/common:assert_lib", + "//source/common/http:header_map_lib", + "//source/common/json:config_schemas_lib", + "//source/common/json:json_loader_lib", + "//source/common/json:json_validator_lib", + "@envoy_api//envoy/config/filter/http/ext_authz/v2:ext_authz_cc", + ], +) diff --git a/source/common/http/filter/ext_authz.cc b/source/common/http/filter/ext_authz.cc new file mode 100644 index 0000000000000..41b3d1c45b6dc --- /dev/null +++ b/source/common/http/filter/ext_authz.cc @@ -0,0 +1,125 @@ +#include "common/http/filter/ext_authz.h" + +#include +#include + +#include "envoy/http/codes.h" + +#include "common/common/assert.h" +#include "common/common/enum_to_int.h" +#include "common/ext_authz/ext_authz_impl.h" +#include "common/http/codes.h" +#include "common/router/config_impl.h" + +#include "fmt/format.h" + +namespace Envoy { +namespace Http { +namespace ExtAuthz { + +namespace { + +const Http::HeaderMap* getDeniedHeader() { + static const Http::HeaderMap* header_map = new Http::HeaderMapImpl{ + {Http::Headers::get().Status, std::to_string(enumToInt(Code::Forbidden))}}; + return header_map; +} + +} // namespace + +void Filter::initiateCall(const HeaderMap& headers) { + Router::RouteConstSharedPtr route = callbacks_->route(); + if (route == nullptr || route->routeEntry() == nullptr) { + return; + } + + const Router::RouteEntry* route_entry = route->routeEntry(); + Upstream::ThreadLocalCluster* cluster = config_->cm().get(route_entry->clusterName()); + if (cluster == nullptr) { + return; + } + cluster_ = cluster->info(); + + Envoy::ExtAuthz::CheckRequestUtils::createHttpCheck(callbacks_, headers, check_request_); + + state_ = State::Calling; + initiating_call_ = true; + client_->check(*this, check_request_, callbacks_->activeSpan()); + initiating_call_ = false; +} + +FilterHeadersStatus Filter::decodeHeaders(HeaderMap& headers, bool) { + initiateCall(headers); + return state_ == State::Calling ? FilterHeadersStatus::StopIteration + : FilterHeadersStatus::Continue; +} + +FilterDataStatus Filter::decodeData(Buffer::Instance&, bool) { + return state_ == State::Calling ? FilterDataStatus::StopIterationAndWatermark + : FilterDataStatus::Continue; +} + +FilterTrailersStatus Filter::decodeTrailers(HeaderMap&) { + return state_ == State::Calling ? FilterTrailersStatus::StopIteration + : FilterTrailersStatus::Continue; +} + +void Filter::setDecoderFilterCallbacks(StreamDecoderFilterCallbacks& callbacks) { + callbacks_ = &callbacks; +} + +void Filter::onDestroy() { + if (state_ == State::Calling) { + state_ = State::Complete; + client_->cancel(); + } +} + +void Filter::onComplete(Envoy::ExtAuthz::CheckStatus status) { + ASSERT(cluster_); + + state_ = State::Complete; + + using Envoy::ExtAuthz::CheckStatus; + + switch (status) { + case CheckStatus::OK: + cluster_->statsScope().counter("ext_authz.ok").inc(); + break; + case CheckStatus::Error: + cluster_->statsScope().counter("ext_authz.error").inc(); + break; + case CheckStatus::Denied: + cluster_->statsScope().counter("ext_authz.denied").inc(); + Http::CodeUtility::ResponseStatInfo info{config_->scope(), + cluster_->statsScope(), + EMPTY_STRING, + enumToInt(Code::Forbidden), + true, + EMPTY_STRING, + EMPTY_STRING, + EMPTY_STRING, + EMPTY_STRING, + false}; + Http::CodeUtility::chargeResponseStat(info); + break; + } + + // We fail open/fail close based of filter config + // if there is an error contacting the service. + if (status == CheckStatus::Denied || + (status == CheckStatus::Error && !config_->failureModeAllow())) { + Http::HeaderMapPtr response_headers{new HeaderMapImpl(*getDeniedHeader())}; + callbacks_->encodeHeaders(std::move(response_headers), true); + callbacks_->requestInfo().setResponseFlag(Envoy::RequestInfo::ResponseFlag::Unauthorized); + } else { + // We can get completion inline, so only call continue if that isn't happening. + if (!initiating_call_) { + callbacks_->continueDecoding(); + } + } +} + +} // namespace ExtAuthz +} // namespace Http +} // namespace Envoy diff --git a/source/common/http/filter/ext_authz.h b/source/common/http/filter/ext_authz.h new file mode 100644 index 0000000000000..d2d0f733dfd34 --- /dev/null +++ b/source/common/http/filter/ext_authz.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/config/filter/http/ext_authz/v2/ext_authz.pb.h" +#include "envoy/ext_authz/ext_authz.h" +#include "envoy/http/filter.h" +#include "envoy/local_info/local_info.h" +#include "envoy/runtime/runtime.h" +#include "envoy/upstream/cluster_manager.h" + +#include "common/common/assert.h" +#include "common/ext_authz/ext_authz_impl.h" +#include "common/http/header_map_impl.h" + +namespace Envoy { +namespace Http { +namespace ExtAuthz { + +/** + * Type of requests the filter should apply to. + */ +enum class FilterRequestType { Internal, External, Both }; + +/** + * Global configuration for the HTTP authorization (ext_authz) filter. + */ +class FilterConfig { +public: + FilterConfig(const envoy::config::filter::http::ext_authz::v2::ExtAuthz& config, + const LocalInfo::LocalInfo& local_info, Stats::Scope& scope, + Runtime::Loader& runtime, Upstream::ClusterManager& cm) + : local_info_(local_info), scope_(scope), runtime_(runtime), cm_(cm), + cluster_name_(config.grpc_service().envoy_grpc().cluster_name()), + failure_mode_allow_(config.failure_mode_allow()) {} + + const LocalInfo::LocalInfo& localInfo() const { return local_info_; } + Runtime::Loader& runtime() { return runtime_; } + Stats::Scope& scope() { return scope_; } + std::string cluster() { return cluster_name_; } + Upstream::ClusterManager& cm() { return cm_; } + bool failureModeAllow() const { return failure_mode_allow_; } + +private: + const LocalInfo::LocalInfo& local_info_; + Stats::Scope& scope_; + Runtime::Loader& runtime_; + Upstream::ClusterManager& cm_; + std::string cluster_name_; + bool failure_mode_allow_; +}; + +typedef std::shared_ptr FilterConfigSharedPtr; + +/** + * HTTP ext_authz filter. Depending on the route configuration, this filter calls the global + * ext_authz service before allowing further filter iteration. + */ +class Filter : public StreamDecoderFilter, public Envoy::ExtAuthz::RequestCallbacks { +public: + Filter(FilterConfigSharedPtr config, Envoy::ExtAuthz::ClientPtr&& client) + : config_(config), client_(std::move(client)) {} + + // Http::StreamFilterBase + void onDestroy() override; + + // Http::StreamDecoderFilter + FilterHeadersStatus decodeHeaders(HeaderMap& headers, bool end_stream) override; + FilterDataStatus decodeData(Buffer::Instance& data, bool end_stream) override; + FilterTrailersStatus decodeTrailers(HeaderMap& trailers) override; + void setDecoderFilterCallbacks(StreamDecoderFilterCallbacks& callbacks) override; + + // ExtAuthz::RequestCallbacks + void onComplete(Envoy::ExtAuthz::CheckStatus status) override; + +private: + enum class State { NotStarted, Calling, Complete }; + void initiateCall(const HeaderMap& headers); + + FilterConfigSharedPtr config_; + Envoy::ExtAuthz::ClientPtr client_; + StreamDecoderFilterCallbacks* callbacks_{}; + State state_{State::NotStarted}; + Upstream::ClusterInfoConstSharedPtr cluster_; + bool initiating_call_{}; + envoy::service::auth::v2::CheckRequest check_request_{}; +}; + +} // namespace ExtAuthz +} // namespace Http +} // namespace Envoy diff --git a/source/exe/BUILD b/source/exe/BUILD index ef3ac152adf77..f779d4dc47d00 100644 --- a/source/exe/BUILD +++ b/source/exe/BUILD @@ -37,6 +37,7 @@ envoy_cc_library( "//source/server/config/access_log:grpc_access_log_lib", "//source/server/config/http:buffer_lib", "//source/server/config/http:cors_lib", + "//source/server/config/http:ext_authz_lib", "//source/server/config/http:fault_lib", "//source/server/config/http:grpc_http1_bridge_lib", "//source/server/config/http:grpc_json_transcoder_lib", diff --git a/source/server/config/http/BUILD b/source/server/config/http/BUILD index eebdf67b228ca..30a464a6b68a3 100644 --- a/source/server/config/http/BUILD +++ b/source/server/config/http/BUILD @@ -230,3 +230,17 @@ envoy_cc_library( "//source/server:configuration_lib", ], ) + +envoy_cc_library( + name = "ext_authz_lib", + srcs = ["ext_authz.cc"], + hdrs = ["ext_authz.h"], + deps = [ + "//include/envoy/registry", + "//include/envoy/server:filter_config_interface", + "//source/common/config:well_known_names", + "//source/common/http/filter:ext_authz_lib", + "//source/common/protobuf:utility_lib", + "@envoy_api//envoy/config/filter/http/ext_authz/v2:ext_authz_cc", + ], +) diff --git a/source/server/config/http/ext_authz.cc b/source/server/config/http/ext_authz.cc new file mode 100644 index 0000000000000..ea859b0c17b0f --- /dev/null +++ b/source/server/config/http/ext_authz.cc @@ -0,0 +1,59 @@ +#include "server/config/http/ext_authz.h" + +#include +#include + +#include "envoy/config/filter/http/ext_authz/v2/ext_authz.pb.validate.h" +#include "envoy/registry/registry.h" + +#include "common/ext_authz/ext_authz_impl.h" +#include "common/http/filter/ext_authz.h" +#include "common/protobuf/utility.h" + +namespace Envoy { +namespace Server { +namespace Configuration { + +HttpFilterFactoryCb ExtAuthzFilterConfig::createFilter( + const envoy::config::filter::http::ext_authz::v2::ExtAuthz& proto_config, const std::string&, + FactoryContext& context) { + auto filter_config = std::make_shared( + proto_config, context.localInfo(), context.scope(), context.runtime(), + context.clusterManager()); + const uint32_t timeout_ms = PROTOBUF_GET_MS_OR_DEFAULT(proto_config.grpc_service(), timeout, 200); + + return [ grpc_service = proto_config.grpc_service(), &context, filter_config, + timeout_ms ](Http::FilterChainFactoryCallbacks & callbacks) { + auto async_client_factory = + context.clusterManager().grpcAsyncClientManager().factoryForGrpcService(grpc_service, + context.scope()); + auto client = std::make_unique( + async_client_factory->create(), std::chrono::milliseconds(timeout_ms)); + callbacks.addStreamDecoderFilter(Http::StreamDecoderFilterSharedPtr{ + std::make_shared(filter_config, std::move(client))}); + }; +} + +HttpFilterFactoryCb ExtAuthzFilterConfig::createFilterFactory(const Json::Object&, + const std::string&, FactoryContext&) { + NOT_IMPLEMENTED; +} + +HttpFilterFactoryCb +ExtAuthzFilterConfig::createFilterFactoryFromProto(const Protobuf::Message& proto_config, + const std::string& stats_prefix, + FactoryContext& context) { + return createFilter( + MessageUtil::downcastAndValidate( + proto_config), + stats_prefix, context); +} + +/** + * Static registration for the external authorization filter. @see RegisterFactory. + */ +static Registry::RegisterFactory register_; + +} // namespace Configuration +} // namespace Server +} // namespace Envoy diff --git a/source/server/config/http/ext_authz.h b/source/server/config/http/ext_authz.h new file mode 100644 index 0000000000000..69ade25583e4e --- /dev/null +++ b/source/server/config/http/ext_authz.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include "envoy/config/filter/http/ext_authz/v2/ext_authz.pb.h" +#include "envoy/server/filter_config.h" + +#include "common/config/well_known_names.h" + +namespace Envoy { +namespace Server { +namespace Configuration { + +/** + * Config registration for the external authorization filter. @see NamedHttpFilterConfigFactory. + */ +class ExtAuthzFilterConfig : public NamedHttpFilterConfigFactory { +public: + HttpFilterFactoryCb createFilterFactory(const Json::Object& json_config, const std::string&, + FactoryContext& context) override; + HttpFilterFactoryCb createFilterFactoryFromProto(const Protobuf::Message& proto_config, + const std::string& stats_prefix, + FactoryContext& context) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return ProtobufTypes::MessagePtr{new envoy::config::filter::http::ext_authz::v2::ExtAuthz()}; + } + + std::string name() override { return Config::HttpFilterNames::get().EXT_AUTHORIZATION; } + +private: + HttpFilterFactoryCb + createFilter(const envoy::config::filter::http::ext_authz::v2::ExtAuthz& proto_config, + const std::string& stats_prefix, FactoryContext& context); +}; + +} // namespace Configuration +} // namespace Server +} // namespace Envoy diff --git a/source/server/config/network/ext_authz.cc b/source/server/config/network/ext_authz.cc index b2c7887702c80..8b2786c7e26a1 100644 --- a/source/server/config/network/ext_authz.cc +++ b/source/server/config/network/ext_authz.cc @@ -19,11 +19,6 @@ namespace Configuration { NetworkFilterFactoryCb ExtAuthzConfigFactory::createFilter( const envoy::config::filter::network::ext_authz::v2::ExtAuthz& proto_config, FactoryContext& context) { - - ASSERT(!proto_config.stat_prefix().empty()); - ASSERT(!proto_config.grpc_service().envoy_grpc().cluster_name().empty() || - !proto_config.grpc_service().google_grpc().target_uri().empty()); - ExtAuthz::TcpFilter::ConfigSharedPtr ext_authz_config( new ExtAuthz::TcpFilter::Config(proto_config, context.scope())); const uint32_t timeout_ms = PROTOBUF_GET_MS_OR_DEFAULT(proto_config.grpc_service(), timeout, 200); @@ -39,7 +34,7 @@ NetworkFilterFactoryCb ExtAuthzConfigFactory::createFilter( auto client = std::make_unique( async_client_factory->create(), std::chrono::milliseconds(timeout_ms)); filter_manager.addReadFilter(Network::ReadFilterSharedPtr{ - new ExtAuthz::TcpFilter::Instance(ext_authz_config, std::move(client))}); + std::make_shared(ext_authz_config, std::move(client))}); }; } diff --git a/test/common/http/filter/BUILD b/test/common/http/filter/BUILD index 456fb2c858b16..91d08c94a257b 100644 --- a/test/common/http/filter/BUILD +++ b/test/common/http/filter/BUILD @@ -101,6 +101,7 @@ envoy_cc_test( "//test/mocks/tracing:tracing_mocks", "//test/mocks/upstream:upstream_mocks", "//test/test_common:utility_lib", + "@envoy_api//envoy/config/filter/http/ext_authz/v2:ext_authz_cc", ], ) @@ -118,3 +119,28 @@ envoy_cc_test( "//test/mocks/upstream:upstream_mocks", ], ) + +envoy_cc_test( + name = "ext_authz_test", + srcs = ["ext_authz_test.cc"], + deps = [ + "//source/common/buffer:buffer_lib", + "//source/common/common:empty_string", + "//source/common/config:filter_json_lib", + "//source/common/ext_authz:ext_authz_lib", + "//source/common/http:headers_lib", + "//source/common/http/filter:ext_authz_includes", + "//source/common/http/filter:ext_authz_lib", + "//source/common/json:json_loader_lib", + "//source/common/network:address_lib", + "//source/common/protobuf:utility_lib", + "//test/mocks/ext_authz:ext_authz_mocks", + "//test/mocks/http:http_mocks", + "//test/mocks/local_info:local_info_mocks", + "//test/mocks/network:network_mocks", + "//test/mocks/runtime:runtime_mocks", + "//test/mocks/tracing:tracing_mocks", + "//test/mocks/upstream:upstream_mocks", + "//test/test_common:utility_lib", + ], +) diff --git a/test/common/http/filter/ext_authz_test.cc b/test/common/http/filter/ext_authz_test.cc new file mode 100644 index 0000000000000..6d12760a6a048 --- /dev/null +++ b/test/common/http/filter/ext_authz_test.cc @@ -0,0 +1,304 @@ +#include +#include +#include + +#include "envoy/config/filter/http/ext_authz/v2/ext_authz.pb.validate.h" + +#include "common/buffer/buffer_impl.h" +#include "common/common/empty_string.h" +#include "common/http/filter/ext_authz.h" +#include "common/http/headers.h" +#include "common/json/json_loader.h" +#include "common/network/address_impl.h" +#include "common/protobuf/utility.h" + +#include "test/mocks/ext_authz/mocks.h" +#include "test/mocks/http/mocks.h" +#include "test/mocks/local_info/mocks.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/runtime/mocks.h" +#include "test/mocks/tracing/mocks.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/printers.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::InSequence; +using testing::Invoke; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; +using testing::TestWithParam; +using testing::Values; +using testing::WithArgs; +using testing::_; + +namespace Envoy { +namespace Http { +namespace ExtAuthz { + +class HttpExtAuthzFilterTestBase { +public: + HttpExtAuthzFilterTestBase() {} + + FilterConfigSharedPtr config_; + Envoy::ExtAuthz::MockClient* client_; + std::unique_ptr filter_; + NiceMock filter_callbacks_; + Envoy::ExtAuthz::RequestCallbacks* request_callbacks_{}; + TestHeaderMapImpl request_headers_; + Buffer::OwnedImpl data_; + Stats::IsolatedStoreImpl stats_store_; + NiceMock runtime_; + NiceMock cm_; + NiceMock local_info_; + Network::Address::InstanceConstSharedPtr addr_; + NiceMock connection_; +}; + +class HttpExtAuthzFilterTest : public testing::Test, public HttpExtAuthzFilterTestBase { +public: + HttpExtAuthzFilterTest() {} + + void initialize(const std::string yaml) { + envoy::config::filter::http::ext_authz::v2::ExtAuthz proto_config{}; + MessageUtil::loadFromYaml(yaml, proto_config); + config_.reset(new FilterConfig(proto_config, local_info_, stats_store_, runtime_, cm_)); + + client_ = new Envoy::ExtAuthz::MockClient(); + filter_.reset(new Filter(config_, Envoy::ExtAuthz::ClientPtr{client_})); + filter_->setDecoderFilterCallbacks(filter_callbacks_); + addr_ = std::make_shared("1.2.3.4", 1111); + } + + const std::string filter_config_ = R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_authz_server" + failure_mode_allow: true + )EOF"; +}; + +typedef envoy::config::filter::http::ext_authz::v2::ExtAuthz CreateFilterConfigFunc(); + +class HttpExtAuthzFilterParamTest : public TestWithParam, + public HttpExtAuthzFilterTestBase { +public: + virtual void SetUp() override { + envoy::config::filter::http::ext_authz::v2::ExtAuthz proto_config = (*GetParam())(); + config_.reset(new FilterConfig(proto_config, local_info_, stats_store_, runtime_, cm_)); + + client_ = new Envoy::ExtAuthz::MockClient(); + filter_.reset(new Filter(config_, Envoy::ExtAuthz::ClientPtr{client_})); + filter_->setDecoderFilterCallbacks(filter_callbacks_); + addr_ = std::make_shared("1.2.3.4", 1111); + } +}; + +template +envoy::config::filter::http::ext_authz::v2::ExtAuthz GetFilterConfig() { + const std::string yaml = R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_authz_server" + )EOF"; + envoy::config::filter::http::ext_authz::v2::ExtAuthz proto_config{}; + MessageUtil::loadFromYaml(yaml, proto_config); + proto_config.set_failure_mode_allow(failure_mode_allow_value); + return proto_config; +} + +INSTANTIATE_TEST_CASE_P(ParameterizedFilterConfig, HttpExtAuthzFilterParamTest, + Values(&GetFilterConfig, &GetFilterConfig)); + +// Test that the request continues when the filter_callbacks has no route. +TEST_P(HttpExtAuthzFilterParamTest, NoRoute) { + + EXPECT_CALL(*filter_callbacks_.route_, routeEntry()).WillOnce(Return(nullptr)); + + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, false)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, false)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_headers_)); +} + +// Test that the request continues when the authorization service cluster is not present. +TEST_P(HttpExtAuthzFilterParamTest, NoCluster) { + + ON_CALL(cm_, get(_)).WillByDefault(Return(nullptr)); + + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, false)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, false)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_headers_)); +} + +// Test that the request is stopped till there is an OK response back after which it continues on. +TEST_P(HttpExtAuthzFilterParamTest, OkResponse) { + InSequence s; + + ON_CALL(filter_callbacks_, connection()).WillByDefault(Return(&connection_)); + EXPECT_CALL(connection_, remoteAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(connection_, localAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(*client_, check(_, _, testing::A())) + .WillOnce(WithArgs<0>(Invoke([&](Envoy::ExtAuthz::RequestCallbacks& callbacks) -> void { + request_callbacks_ = &callbacks; + }))); + + EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->decodeHeaders(request_headers_, false)); + EXPECT_EQ(FilterDataStatus::StopIterationAndWatermark, filter_->decodeData(data_, false)); + EXPECT_EQ(FilterTrailersStatus::StopIteration, filter_->decodeTrailers(request_headers_)); + + EXPECT_CALL(filter_callbacks_, continueDecoding()); + EXPECT_CALL(filter_callbacks_.request_info_, + setResponseFlag(Envoy::RequestInfo::ResponseFlag::Unauthorized)) + .Times(0); + request_callbacks_->onComplete(Envoy::ExtAuthz::CheckStatus::OK); + + EXPECT_EQ(1U, + cm_.thread_local_cluster_.cluster_.info_->stats_store_.counter("ext_authz.ok").value()); +} + +// Test that an synchronous OK response from the authorization service, on the call stack, results +// in request continuing on. +TEST_P(HttpExtAuthzFilterParamTest, ImmediateOkResponse) { + InSequence s; + + ON_CALL(filter_callbacks_, connection()).WillByDefault(Return(&connection_)); + EXPECT_CALL(connection_, remoteAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(connection_, localAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(*client_, check(_, _, _)) + .WillOnce(WithArgs<0>(Invoke([&](Envoy::ExtAuthz::RequestCallbacks& callbacks) -> void { + callbacks.onComplete(Envoy::ExtAuthz::CheckStatus::OK); + }))); + + EXPECT_CALL(filter_callbacks_, continueDecoding()).Times(0); + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, false)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, false)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_headers_)); + + EXPECT_EQ(1U, + cm_.thread_local_cluster_.cluster_.info_->stats_store_.counter("ext_authz.ok").value()); +} + +// Test that a denied response results in the connection closing with a 403 response to the client. +TEST_P(HttpExtAuthzFilterParamTest, DeniedResponse) { + InSequence s; + + ON_CALL(filter_callbacks_, connection()).WillByDefault(Return(&connection_)); + EXPECT_CALL(connection_, remoteAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(connection_, localAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(*client_, check(_, _, _)) + .WillOnce(WithArgs<0>(Invoke([&](Envoy::ExtAuthz::RequestCallbacks& callbacks) -> void { + request_callbacks_ = &callbacks; + }))); + + EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->decodeHeaders(request_headers_, false)); + Http::TestHeaderMapImpl response_headers{{":status", "403"}}; + EXPECT_CALL(filter_callbacks_, encodeHeaders_(HeaderMapEqualRef(&response_headers), true)); + EXPECT_CALL(filter_callbacks_, continueDecoding()).Times(0); + EXPECT_CALL(filter_callbacks_.request_info_, + setResponseFlag(Envoy::RequestInfo::ResponseFlag::Unauthorized)); + request_callbacks_->onComplete(Envoy::ExtAuthz::CheckStatus::Denied); + + EXPECT_EQ( + 1U, + cm_.thread_local_cluster_.cluster_.info_->stats_store_.counter("ext_authz.denied").value()); + EXPECT_EQ( + 1U, + cm_.thread_local_cluster_.cluster_.info_->stats_store_.counter("upstream_rq_4xx").value()); + EXPECT_EQ( + 1U, + cm_.thread_local_cluster_.cluster_.info_->stats_store_.counter("upstream_rq_403").value()); +} + +// Test that when a connection awaiting a authorization response is canceled then the authorization +// call is closed. +TEST_P(HttpExtAuthzFilterParamTest, ResetDuringCall) { + InSequence s; + + ON_CALL(filter_callbacks_, connection()).WillByDefault(Return(&connection_)); + EXPECT_CALL(connection_, remoteAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(connection_, localAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(*client_, check(_, _, _)) + .WillOnce(WithArgs<0>(Invoke([&](Envoy::ExtAuthz::RequestCallbacks& callbacks) -> void { + request_callbacks_ = &callbacks; + }))); + + EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->decodeHeaders(request_headers_, false)); + + EXPECT_CALL(*client_, cancel()); + filter_->onDestroy(); +} + +// Check a bad configuration results in validation exception. +TEST_F(HttpExtAuthzFilterTest, BadConfig) { + const std::string filter_config = R"EOF( + failure_mode_allow: true + grpc_service: {} + )EOF"; + + envoy::config::filter::http::ext_authz::v2::ExtAuthz proto_config{}; + MessageUtil::loadFromYaml(filter_config, proto_config); + + EXPECT_THROW( + MessageUtil::downcastAndValidate( + proto_config), + ProtoValidationException); +} + +// Test when failure_mode_allow is NOT set and the response from the authorization service is Error +// that the request is not allowed to continue. +TEST_F(HttpExtAuthzFilterTest, ErrorFailClose) { + const std::string fail_close_config = R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_authz_server" + failure_mode_allow: false + )EOF"; + initialize(fail_close_config); + InSequence s; + + ON_CALL(filter_callbacks_, connection()).WillByDefault(Return(&connection_)); + EXPECT_CALL(connection_, remoteAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(connection_, localAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(*client_, check(_, _, _)) + .WillOnce(WithArgs<0>(Invoke([&](Envoy::ExtAuthz::RequestCallbacks& callbacks) -> void { + request_callbacks_ = &callbacks; + }))); + + EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->decodeHeaders(request_headers_, false)); + EXPECT_CALL(filter_callbacks_, continueDecoding()).Times(0); + request_callbacks_->onComplete(Envoy::ExtAuthz::CheckStatus::Error); + + EXPECT_EQ( + 1U, + cm_.thread_local_cluster_.cluster_.info_->stats_store_.counter("ext_authz.error").value()); +} + +// Test when failure_mode_allow is set and the response from the authorization service is Error that +// the request is allowed to continue. +TEST_F(HttpExtAuthzFilterTest, ErrorOpen) { + initialize(filter_config_); + InSequence s; + + ON_CALL(filter_callbacks_, connection()).WillByDefault(Return(&connection_)); + EXPECT_CALL(connection_, remoteAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(connection_, localAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(*client_, check(_, _, _)) + .WillOnce(WithArgs<0>(Invoke([&](Envoy::ExtAuthz::RequestCallbacks& callbacks) -> void { + request_callbacks_ = &callbacks; + }))); + + EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->decodeHeaders(request_headers_, false)); + EXPECT_CALL(filter_callbacks_, continueDecoding()); + request_callbacks_->onComplete(Envoy::ExtAuthz::CheckStatus::Error); + + EXPECT_EQ( + 1U, + cm_.thread_local_cluster_.cluster_.info_->stats_store_.counter("ext_authz.error").value()); +} + +} // namespace ExtAuthz +} // namespace Http +} // namespace Envoy diff --git a/test/server/config/http/BUILD b/test/server/config/http/BUILD index 40feea2c80f4f..a93817427737d 100644 --- a/test/server/config/http/BUILD +++ b/test/server/config/http/BUILD @@ -18,6 +18,7 @@ envoy_cc_test( "//source/common/router:router_lib", "//source/server/config/http:buffer_lib", "//source/server/config/http:dynamo_lib", + "//source/server/config/http:ext_authz_lib", "//source/server/config/http:fault_lib", "//source/server/config/http:grpc_http1_bridge_lib", "//source/server/config/http:grpc_json_transcoder_lib", diff --git a/test/server/config/http/config_test.cc b/test/server/config/http/config_test.cc index f35b1ed6c3e06..4660c5474ed41 100644 --- a/test/server/config/http/config_test.cc +++ b/test/server/config/http/config_test.cc @@ -12,6 +12,7 @@ #include "server/config/http/buffer.h" #include "server/config/http/dynamo.h" +#include "server/config/http/ext_authz.h" #include "server/config/http/fault.h" #include "server/config/http/grpc_http1_bridge.h" #include "server/config/http/grpc_json_transcoder.h" @@ -448,6 +449,37 @@ TEST(HttpTracerConfigTest, DoubleRegistrationTest) { "Double registration for name: 'envoy.zipkin'"); } +TEST(HttpExtAuthzConfigTest, ExtAuthzCorrectProto) { + std::string yaml = R"EOF( + grpc_service: + google_grpc: + target_uri: ext_authz_server + stat_prefix: google + failure_mode_allow: false +)EOF"; + + envoy::config::filter::http::ext_authz::v2::ExtAuthz proto_config{}; + MessageUtil::loadFromYaml(yaml, proto_config); + + NiceMock context; + ExtAuthzFilterConfig factory; + + EXPECT_CALL(context.cluster_manager_.async_client_manager_, factoryForGrpcService(_, _)) + .WillOnce(Invoke([](const envoy::api::v2::core::GrpcService&, Stats::Scope&) { + return std::make_unique>(); + })); + HttpFilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamDecoderFilter(_)); + cb(filter_callback); +} + +TEST(HttpExtAuthzConfigTest, DoubleRegistrationTest) { + EXPECT_THROW_WITH_MESSAGE( + (Registry::RegisterFactory()), + EnvoyException, "Double registration for name: 'envoy.ext_authz'"); +} + } // namespace Configuration } // namespace Server } // namespace Envoy diff --git a/test/server/config/network/config_test.cc b/test/server/config/network/config_test.cc index 2a3bad76b5db3..b35fde8e1d4ed 100644 --- a/test/server/config/network/config_test.cc +++ b/test/server/config/network/config_test.cc @@ -540,21 +540,17 @@ TEST(TcpProxyConfigTest, TcpProxyConfigTest) { } TEST(NetworkFilterConfigTest, ExtAuthzCorrectProto) { - std::string json = R"EOF( - { - "grpc_service": { - "google_grpc": { - "target_uri": "ext_authz_server", - "stat_prefix": "google" - } - }, - "failure_mode_allow": false, - "stat_prefix": "name" - } - )EOF"; + std::string yaml = R"EOF( + grpc_service: + google_grpc: + target_uri: ext_authz_server + stat_prefix: google + failure_mode_allow: false + stat_prefix: name +)EOF"; envoy::config::filter::network::ext_authz::v2::ExtAuthz proto_config{}; - MessageUtil::loadFromJson(json, proto_config); + MessageUtil::loadFromYaml(yaml, proto_config); NiceMock context; ExtAuthzConfigFactory factory;