diff --git a/source/common/ext_authz/ext_authz_impl.cc b/source/common/ext_authz/ext_authz_impl.cc index d2b2a2eef3cc6..ac2f987eca224 100644 --- a/source/common/ext_authz/ext_authz_impl.cc +++ b/source/common/ext_authz/ext_authz_impl.cc @@ -66,9 +66,9 @@ void GrpcClientImpl::onFailure(Grpc::Status::GrpcStatus status, const std::strin callbacks_ = nullptr; } -void CreateCheckRequest::setAttrContextPeer(envoy::service::auth::v2::AttributeContext_Peer& peer, - const Network::Connection& connection, - const std::string& service, const bool local) { +void CheckRequestUtils::setAttrContextPeer(envoy::service::auth::v2::AttributeContext_Peer& peer, + const Network::Connection& connection, + const std::string& service, const bool local) { // Set the address auto addr = peer.mutable_address(); @@ -103,14 +103,14 @@ void CreateCheckRequest::setAttrContextPeer(envoy::service::auth::v2::AttributeC } } -std::string CreateCheckRequest::getHeaderStr(const Envoy::Http::HeaderEntry* entry) { +std::string CheckRequestUtils::getHeaderStr(const Envoy::Http::HeaderEntry* entry) { if (entry) { return entry->value().getString(); } return ""; } -void CreateCheckRequest::setHttpRequest( +void CheckRequestUtils::setHttpRequest( ::envoy::service::auth::v2::AttributeContext_HttpRequest& httpreq, const Envoy::Http::StreamDecoderFilterCallbacks* callbacks, const Envoy::Http::HeaderMap& headers) { @@ -152,16 +152,16 @@ void CreateCheckRequest::setHttpRequest( mutable_headers); } -void CreateCheckRequest::setAttrContextRequest( +void CheckRequestUtils::setAttrContextRequest( ::envoy::service::auth::v2::AttributeContext_Request& req, const Envoy::Http::StreamDecoderFilterCallbacks* callbacks, const Envoy::Http::HeaderMap& headers) { setHttpRequest(*req.mutable_http(), callbacks, headers); } -void CreateCheckRequest::createHttpCheck(const Envoy::Http::StreamDecoderFilterCallbacks* callbacks, - const Envoy::Http::HeaderMap& headers, - envoy::service::auth::v2::CheckRequest& request) { +void CheckRequestUtils::createHttpCheck(const Envoy::Http::StreamDecoderFilterCallbacks* callbacks, + const Envoy::Http::HeaderMap& headers, + envoy::service::auth::v2::CheckRequest& request) { auto attrs = request.mutable_attributes(); @@ -175,8 +175,8 @@ void CreateCheckRequest::createHttpCheck(const Envoy::Http::StreamDecoderFilterC setAttrContextRequest(*attrs->mutable_request(), callbacks, headers); } -void CreateCheckRequest::createTcpCheck(const Network::ReadFilterCallbacks* callbacks, - envoy::service::auth::v2::CheckRequest& request) { +void CheckRequestUtils::createTcpCheck(const Network::ReadFilterCallbacks* callbacks, + envoy::service::auth::v2::CheckRequest& request) { auto attrs = request.mutable_attributes(); diff --git a/source/common/ext_authz/ext_authz_impl.h b/source/common/ext_authz/ext_authz_impl.h index 862e0e61933f8..a7b79bf1a1569 100644 --- a/source/common/ext_authz/ext_authz_impl.h +++ b/source/common/ext_authz/ext_authz_impl.h @@ -63,13 +63,13 @@ class GrpcClientImpl : public Client, public ExtAuthzAsyncCallbacks { /** * For creating ext_authz.proto (authorization) request. - * CreateCheckRequest is used to extract attributes from the TCP/HTTP request + * CheckRequestUtils is used to extract attributes from the TCP/HTTP request * and fill out the details in the authorization protobuf that is sent to authorization * service. * The specific information in the request is as per the specification in the * data-plane-api. */ -class CreateCheckRequest { +class CheckRequestUtils { public: /** * createHttpCheck is used to extract the attributes from the stream and the http headers diff --git a/source/common/filter/BUILD b/source/common/filter/BUILD index af8c4723f900e..d32d54782ba47 100644 --- a/source/common/filter/BUILD +++ b/source/common/filter/BUILD @@ -65,3 +65,21 @@ envoy_cc_library( "@envoy_api//envoy/config/filter/network/tcp_proxy/v2:tcp_proxy_cc", ], ) + +envoy_cc_library( + name = "ext_authz_lib", + srcs = ["ext_authz.cc"], + hdrs = ["ext_authz.h"], + deps = [ + "//include/envoy/ext_authz:ext_authz_interface", + "//include/envoy/network:connection_interface", + "//include/envoy/network:filter_interface", + "//include/envoy/runtime:runtime_interface", + "//include/envoy/stats:stats_macros", + "//include/envoy/upstream:cluster_manager_interface", + "//source/common/common:assert_lib", + "//source/common/ext_authz:ext_authz_lib", + "//source/common/tracing:http_tracer_lib", + "@envoy_api//envoy/config/filter/network/ext_authz/v2:ext_authz_cc", + ], +) diff --git a/source/common/filter/ext_authz.cc b/source/common/filter/ext_authz.cc new file mode 100644 index 0000000000000..117e9d7a03ff9 --- /dev/null +++ b/source/common/filter/ext_authz.cc @@ -0,0 +1,90 @@ +#include "common/filter/ext_authz.h" + +#include +#include + +#include "common/common/assert.h" +#include "common/tracing/http_tracer_impl.h" + +#include "fmt/format.h" + +namespace Envoy { +namespace ExtAuthz { +namespace TcpFilter { + +InstanceStats Config::generateStats(const std::string& name, Stats::Scope& scope) { + const std::string final_prefix = fmt::format("ext_authz.{}.", name); + return {ALL_TCP_EXT_AUTHZ_STATS(POOL_COUNTER_PREFIX(scope, final_prefix), + POOL_GAUGE_PREFIX(scope, final_prefix))}; +} + +void Instance::callCheck() { + CheckRequestUtils::createTcpCheck(filter_callbacks_, checkRequest_); + + status_ = Status::Calling; + config_->stats().active_.inc(); + config_->stats().total_.inc(); + + calling_check_ = true; + client_->check(*this, checkRequest_, Tracing::NullSpan::instance()); + calling_check_ = false; +} + +Network::FilterStatus Instance::onData(Buffer::Instance&) { + if (status_ == Status::NotStarted) { + // By waiting to invoke the check at onData() the call to authorization service will have + // sufficient information to fillout the checkRequest_. + callCheck(); + } + return status_ == Status::Calling ? Network::FilterStatus::StopIteration + : Network::FilterStatus::Continue; +} + +Network::FilterStatus Instance::onNewConnection() { + // Wait till onData() happens. + return Network::FilterStatus::Continue; +} + +void Instance::onEvent(Network::ConnectionEvent event) { + if (event == Network::ConnectionEvent::RemoteClose || + event == Network::ConnectionEvent::LocalClose) { + if (status_ == Status::Calling) { + // Make sure that any pending request in the client is cancelled. This will be NOP if the + // request already completed. + client_->cancel(); + config_->stats().active_.dec(); + } + } +} + +void Instance::onComplete(CheckStatus status) { + status_ = Status::Complete; + config_->stats().active_.dec(); + + switch (status) { + case CheckStatus::OK: + config_->stats().ok_.inc(); + break; + case CheckStatus::Error: + config_->stats().error_.inc(); + break; + case CheckStatus::Denied: + config_->stats().denied_.inc(); + break; + } + + // 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())) { + config_->stats().cx_closed_.inc(); + filter_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); + } else { + // We can get completion inline, so only call continue if that isn't happening. + if (!calling_check_) { + filter_callbacks_->continueReading(); + } + } +} + +} // namespace TcpFilter +} // namespace ExtAuthz +} // namespace Envoy diff --git a/source/common/filter/ext_authz.h b/source/common/filter/ext_authz.h new file mode 100644 index 0000000000000..dad3096876d87 --- /dev/null +++ b/source/common/filter/ext_authz.h @@ -0,0 +1,107 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/config/filter/network/ext_authz/v2/ext_authz.pb.h" +#include "envoy/ext_authz/ext_authz.h" +#include "envoy/network/connection.h" +#include "envoy/network/filter.h" +#include "envoy/runtime/runtime.h" +#include "envoy/stats/stats_macros.h" +#include "envoy/upstream/cluster_manager.h" + +#include "common/ext_authz/ext_authz_impl.h" + +namespace Envoy { +namespace ExtAuthz { +namespace TcpFilter { + +/** + * All tcp external authorization stats. @see stats_macros.h + */ +// clang-format off +#define ALL_TCP_EXT_AUTHZ_STATS(COUNTER, GAUGE) \ + COUNTER(total) \ + COUNTER(error) \ + COUNTER(denied) \ + COUNTER(ok) \ + COUNTER(cx_closed) \ + GAUGE (active) +// clang-format on + +/** + * Struct definition for all external authorization stats. @see stats_macros.h + */ +struct InstanceStats { + ALL_TCP_EXT_AUTHZ_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT) +}; + +/** + * Global configuration for ExtAuthz filter. + */ +class Config { +public: + Config(const envoy::config::filter::network::ext_authz::v2::ExtAuthz& config, Stats::Scope& scope) + : stats_(generateStats(config.stat_prefix(), scope)), + failure_mode_allow_(config.failure_mode_allow()) {} + + const InstanceStats& stats() { return stats_; } + bool failOpen() const { return failure_mode_allow_; } + void setFailModeAllow(bool value) { failure_mode_allow_ = value; } + +private: + static InstanceStats generateStats(const std::string& name, Stats::Scope& scope); + const InstanceStats stats_; + bool failure_mode_allow_; +}; + +typedef std::shared_ptr ConfigSharedPtr; + +/** + * ExtAuthz filter instance. This filter will call the Authorization service with the given + * configuration parameters. If the authorization service returns an error or a deny the + * connection will be closed without any further filters being called. Otherwise all buffered + * data will be released to further filters. + */ +class Instance : public Network::ReadFilter, + public Network::ConnectionCallbacks, + public RequestCallbacks { +public: + Instance(ConfigSharedPtr config, ClientPtr&& client) + : config_(config), client_(std::move(client)) {} + ~Instance() {} + + // Network::ReadFilter + Network::FilterStatus onData(Buffer::Instance& data) override; + Network::FilterStatus onNewConnection() override; + void initializeReadFilterCallbacks(Network::ReadFilterCallbacks& callbacks) override { + filter_callbacks_ = &callbacks; + filter_callbacks_->connection().addConnectionCallbacks(*this); + } + + // Network::ConnectionCallbacks + void onEvent(Network::ConnectionEvent event) override; + void onAboveWriteBufferHighWatermark() override {} + void onBelowWriteBufferLowWatermark() override {} + + // ExtAuthz::RequestCallbacks + void onComplete(CheckStatus status) override; + +private: + enum class Status { NotStarted, Calling, Complete }; + void callCheck(); + + ConfigSharedPtr config_; + ClientPtr client_; + Network::ReadFilterCallbacks* filter_callbacks_{}; + Status status_{Status::NotStarted}; + bool calling_check_{}; + envoy::service::auth::v2::CheckRequest checkRequest_{}; +}; + +} // TcpFilter +} // namespace ExtAuthz +} // namespace Envoy diff --git a/source/exe/BUILD b/source/exe/BUILD index ba6db1d96a307..0ef764928f9dc 100644 --- a/source/exe/BUILD +++ b/source/exe/BUILD @@ -49,6 +49,7 @@ envoy_cc_library( "//source/server/config/listener:proxy_protocol_lib", "//source/server/config/network:client_ssl_auth_lib", "//source/server/config/network:echo_lib", + "//source/server/config/network:ext_authz_lib", "//source/server/config/network:http_connection_manager_lib", "//source/server/config/network:ratelimit_lib", "//source/server/config/network:raw_buffer_socket_lib", diff --git a/source/server/config/network/BUILD b/source/server/config/network/BUILD index 23d6918d30d1c..12fff4e293c28 100644 --- a/source/server/config/network/BUILD +++ b/source/server/config/network/BUILD @@ -146,3 +146,17 @@ envoy_cc_library( "@envoy_api//envoy/api/v2/auth:cert_cc", ], ) + +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/filter:ext_authz_lib", + "//source/common/protobuf:utility_lib", + "@envoy_api//envoy/config/filter/network/ext_authz/v2:ext_authz_cc", + ], +) diff --git a/source/server/config/network/ext_authz.cc b/source/server/config/network/ext_authz.cc new file mode 100644 index 0000000000000..b2c7887702c80 --- /dev/null +++ b/source/server/config/network/ext_authz.cc @@ -0,0 +1,68 @@ +#include "server/config/network/ext_authz.h" + +#include +#include + +#include "envoy/config/filter/network/ext_authz/v2/ext_authz.pb.validate.h" +#include "envoy/ext_authz/ext_authz.h" +#include "envoy/network/connection.h" +#include "envoy/registry/registry.h" + +#include "common/ext_authz/ext_authz_impl.h" +#include "common/filter/ext_authz.h" +#include "common/protobuf/utility.h" + +namespace Envoy { +namespace Server { +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); + + return [ grpc_service = proto_config.grpc_service(), &context, ext_authz_config, + timeout_ms ](Network::FilterManager & filter_manager) + ->void { + + 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)); + filter_manager.addReadFilter(Network::ReadFilterSharedPtr{ + new ExtAuthz::TcpFilter::Instance(ext_authz_config, std::move(client))}); + }; +} + +NetworkFilterFactoryCb ExtAuthzConfigFactory::createFilterFactory(const Json::Object&, + FactoryContext&) { + NOT_IMPLEMENTED; +} + +NetworkFilterFactoryCb +ExtAuthzConfigFactory::createFilterFactoryFromProto(const Protobuf::Message& proto_config, + FactoryContext& context) { + return createFilter( + MessageUtil::downcastAndValidate< + const envoy::config::filter::network::ext_authz::v2::ExtAuthz&>(proto_config), + context); +} + +/** + * Static registration for the external authorization filter. @see RegisterFactory. + */ +static Registry::RegisterFactory + registered_; + +} // namespace Configuration +} // namespace Server +} // namespace Envoy diff --git a/source/server/config/network/ext_authz.h b/source/server/config/network/ext_authz.h new file mode 100644 index 0000000000000..42c981ef94e43 --- /dev/null +++ b/source/server/config/network/ext_authz.h @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include "envoy/config/filter/network/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 NamedNetworkFilterConfigFactory. + */ +class ExtAuthzConfigFactory : public NamedNetworkFilterConfigFactory { +public: + // NamedNetworkFilterConfigFactory + NetworkFilterFactoryCb createFilterFactory(const Json::Object& json_config, + FactoryContext& context) override; + + NetworkFilterFactoryCb createFilterFactoryFromProto(const Protobuf::Message& proto_config, + FactoryContext& context) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return ProtobufTypes::MessagePtr{new envoy::config::filter::network::ext_authz::v2::ExtAuthz()}; + } + + std::string name() override { return Config::NetworkFilterNames::get().EXT_AUTHORIZATION; } + +private: + NetworkFilterFactoryCb + createFilter(const envoy::config::filter::network::ext_authz::v2::ExtAuthz& proto_config, + FactoryContext& context); +}; + +} // namespace Configuration +} // namespace Server +} // namespace Envoy diff --git a/test/common/ext_authz/ext_authz_impl_test.cc b/test/common/ext_authz/ext_authz_impl_test.cc index 77a171c68e7f5..2c5ceb501c354 100644 --- a/test/common/ext_authz/ext_authz_impl_test.cc +++ b/test/common/ext_authz/ext_authz_impl_test.cc @@ -114,16 +114,16 @@ TEST_F(ExtAuthzGrpcClientTest, Cancel) { client_.cancel(); } -class CreateCheckRequestTest : public testing::Test { +class CheckRequestUtilsTest : public testing::Test { public: - CreateCheckRequestTest() { + CheckRequestUtilsTest() { addr_ = std::make_shared("1.2.3.4", 1111); protocol_ = Envoy::Http::Protocol::Http10; }; Network::Address::InstanceConstSharedPtr addr_; Optional protocol_; - CreateCheckRequest check_request_generator_; + CheckRequestUtils check_request_generator_; NiceMock callbacks_; NiceMock net_callbacks_; NiceMock connection_; @@ -131,7 +131,7 @@ class CreateCheckRequestTest : public testing::Test { NiceMock req_info_; }; -TEST_F(CreateCheckRequestTest, BasicTcp) { +TEST_F(CheckRequestUtilsTest, BasicTcp) { envoy::service::auth::v2::CheckRequest request; @@ -140,10 +140,10 @@ TEST_F(CreateCheckRequestTest, BasicTcp) { EXPECT_CALL(connection_, localAddress()).WillOnce(ReturnRef(addr_)); EXPECT_CALL(Const(connection_), ssl()).Times(2).WillRepeatedly(Return(&ssl_)); - CreateCheckRequest::createTcpCheck(&net_callbacks_, request); + CheckRequestUtils::createTcpCheck(&net_callbacks_, request); } -TEST_F(CreateCheckRequestTest, BasicHttp) { +TEST_F(CheckRequestUtilsTest, BasicHttp) { Http::HeaderMapImpl headers; envoy::service::auth::v2::CheckRequest request; @@ -155,10 +155,10 @@ TEST_F(CreateCheckRequestTest, BasicHttp) { EXPECT_CALL(callbacks_, streamId()).WillOnce(Return(0)); EXPECT_CALL(callbacks_, requestInfo()).Times(3).WillRepeatedly(ReturnRef(req_info_)); EXPECT_CALL(req_info_, protocol()).Times(2).WillRepeatedly(ReturnRef(protocol_)); - CreateCheckRequest::createHttpCheck(&callbacks_, headers, request); + CheckRequestUtils::createHttpCheck(&callbacks_, headers, request); } -TEST_F(CreateCheckRequestTest, CheckAttrContextPeer) { +TEST_F(CheckRequestUtilsTest, CheckAttrContextPeer) { Http::TestHeaderMapImpl request_headers{{"x-envoy-downstream-service-cluster", "foo"}, {":path", "/bar"}}; @@ -174,7 +174,7 @@ TEST_F(CreateCheckRequestTest, CheckAttrContextPeer) { EXPECT_CALL(ssl_, uriSanPeerCertificate()).WillOnce(Return("source")); EXPECT_CALL(ssl_, uriSanLocalCertificate()).WillOnce(Return("destination")); - CreateCheckRequest::createHttpCheck(&callbacks_, request_headers, request); + CheckRequestUtils::createHttpCheck(&callbacks_, request_headers, request); EXPECT_EQ("source", request.attributes().source().principal()); EXPECT_EQ("destination", request.attributes().destination().principal()); diff --git a/test/common/filter/BUILD b/test/common/filter/BUILD index f850a54f36ab8..66bc5d48cf2e2 100644 --- a/test/common/filter/BUILD +++ b/test/common/filter/BUILD @@ -46,3 +46,24 @@ 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/config:filter_json_lib", + "//source/common/event:dispatcher_lib", + "//source/common/filter:ext_authz_lib", + "//source/common/json:json_loader_lib", + "//source/common/network:address_lib", + "//source/common/protobuf:utility_lib", + "//source/common/stats:stats_lib", + "//test/mocks/ext_authz:ext_authz_mocks", + "//test/mocks/network:network_mocks", + "//test/mocks/runtime:runtime_mocks", + "//test/mocks/tracing:tracing_mocks", + "//test/mocks/upstream:upstream_mocks", + "@envoy_api//envoy/config/filter/network/ext_authz/v2:ext_authz_cc", + ], +) diff --git a/test/common/filter/ext_authz_test.cc b/test/common/filter/ext_authz_test.cc new file mode 100644 index 0000000000000..8484cbffd890b --- /dev/null +++ b/test/common/filter/ext_authz_test.cc @@ -0,0 +1,296 @@ +#include +#include +#include + +#include "envoy/config/filter/network/ext_authz/v2/ext_authz.pb.validate.h" + +#include "common/buffer/buffer_impl.h" +#include "common/filter/ext_authz.h" +#include "common/json/json_loader.h" +#include "common/network/address_impl.h" +#include "common/protobuf/utility.h" +#include "common/stats/stats_impl.h" + +#include "test/mocks/ext_authz/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 "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::InSequence; +using testing::Invoke; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; +using testing::WithArgs; +using testing::_; + +namespace Envoy { +namespace ExtAuthz { +namespace TcpFilter { + +class ExtAuthzFilterTest : public testing::Test { +public: + ExtAuthzFilterTest() { + std::string json = R"EOF( + { + "grpc_service": { + "envoy_grpc": { "cluster_name": "ext_authz_server" } + }, + "failure_mode_allow": true, + "stat_prefix": "name" + } + )EOF"; + + envoy::config::filter::network::ext_authz::v2::ExtAuthz proto_config{}; + MessageUtil::loadFromJson(json, proto_config); + config_.reset(new Config(proto_config, stats_store_)); + client_ = new MockClient(); + filter_.reset(new Instance(config_, ClientPtr{client_})); + filter_->initializeReadFilterCallbacks(filter_callbacks_); + addr_ = std::make_shared("/test/test.sock"); + + // NOP currently. + filter_->onAboveWriteBufferHighWatermark(); + filter_->onBelowWriteBufferLowWatermark(); + } + + ~ExtAuthzFilterTest() { + for (const Stats::GaugeSharedPtr& gauge : stats_store_.gauges()) { + EXPECT_EQ(0U, gauge->value()); + } + } + + Stats::IsolatedStoreImpl stats_store_; + ConfigSharedPtr config_; + MockClient* client_; + std::unique_ptr filter_; + NiceMock filter_callbacks_; + Network::Address::InstanceConstSharedPtr addr_; + RequestCallbacks* request_callbacks_{}; +}; + +TEST_F(ExtAuthzFilterTest, BadExtAuthzConfig) { + std::string json_string = R"EOF( + { + "stat_prefix": "my_stat_prefix", + "grpc_service": {} + } + )EOF"; + + envoy::config::filter::network::ext_authz::v2::ExtAuthz proto_config{}; + MessageUtil::loadFromJson(json_string, proto_config); + + EXPECT_THROW(MessageUtil::downcastAndValidate< + const envoy::config::filter::network::ext_authz::v2::ExtAuthz&>(proto_config), + ProtoValidationException); +} + +TEST_F(ExtAuthzFilterTest, OKWithOnData) { + InSequence s; + + EXPECT_CALL(filter_callbacks_.connection_, remoteAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(filter_callbacks_.connection_, localAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(*client_, check(_, _, testing::A())) + .WillOnce(WithArgs<0>( + Invoke([&](RequestCallbacks& callbacks) -> void { request_callbacks_ = &callbacks; }))); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection()); + // Confirm that the invocation of onNewConnection did NOT increment the active or total count! + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.total").value()); + EXPECT_EQ(0U, stats_store_.gauge("ext_authz.name.active").value()); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data)); + // Confirm that the invocation of onData does increment the active and total count! + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.total").value()); + EXPECT_EQ(1U, stats_store_.gauge("ext_authz.name.active").value()); + + EXPECT_CALL(filter_callbacks_, continueReading()); + request_callbacks_->onComplete(CheckStatus::OK); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data)); + + EXPECT_CALL(*client_, cancel()).Times(0); + filter_callbacks_.connection_.raiseEvent(Network::ConnectionEvent::LocalClose); + + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.total").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.error").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.denied").value()); + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.ok").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.cx_closed").value()); +} + +TEST_F(ExtAuthzFilterTest, DeniedWithOnData) { + InSequence s; + + EXPECT_CALL(filter_callbacks_.connection_, remoteAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(filter_callbacks_.connection_, localAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(*client_, check(_, _, _)) + .WillOnce(WithArgs<0>( + Invoke([&](RequestCallbacks& callbacks) -> void { request_callbacks_ = &callbacks; }))); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection()); + // Confirm that the invocation of onNewConnection did NOT increment the active or total count! + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.total").value()); + EXPECT_EQ(0U, stats_store_.gauge("ext_authz.name.active").value()); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data)); + // Confirm that the invocation of onData does increment the active and total count! + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.total").value()); + EXPECT_EQ(1U, stats_store_.gauge("ext_authz.name.active").value()); + + EXPECT_CALL(filter_callbacks_.connection_, close(Network::ConnectionCloseType::NoFlush)); + EXPECT_CALL(*client_, cancel()).Times(0); + request_callbacks_->onComplete(CheckStatus::Denied); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data)); + + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.total").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.error").value()); + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.denied").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.ok").value()); + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.cx_closed").value()); +} + +TEST_F(ExtAuthzFilterTest, FailOpen) { + InSequence s; + + EXPECT_CALL(filter_callbacks_.connection_, remoteAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(filter_callbacks_.connection_, localAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(*client_, check(_, _, _)) + .WillOnce(WithArgs<0>( + Invoke([&](RequestCallbacks& callbacks) -> void { request_callbacks_ = &callbacks; }))); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection()); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data)); + + EXPECT_CALL(filter_callbacks_.connection_, close(_)).Times(0); + EXPECT_CALL(*client_, cancel()).Times(0); + EXPECT_CALL(filter_callbacks_, continueReading()); + request_callbacks_->onComplete(CheckStatus::Error); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data)); + + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.total").value()); + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.error").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.denied").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.ok").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.cx_closed").value()); +} + +TEST_F(ExtAuthzFilterTest, FailClose) { + InSequence s; + // Explicitily set the failure_mode_allow to false. + config_->setFailModeAllow(false); + + EXPECT_CALL(filter_callbacks_.connection_, remoteAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(filter_callbacks_.connection_, localAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(*client_, check(_, _, _)) + .WillOnce(WithArgs<0>( + Invoke([&](RequestCallbacks& callbacks) -> void { request_callbacks_ = &callbacks; }))); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection()); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data)); + + EXPECT_CALL(filter_callbacks_.connection_, close(_)).Times(1); + EXPECT_CALL(filter_callbacks_, continueReading()).Times(0); + request_callbacks_->onComplete(CheckStatus::Error); + + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.total").value()); + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.error").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.denied").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.ok").value()); + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.cx_closed").value()); +} + +// Test to verify that when callback from the authorization service has completed the filter +// does not invoke Cancel on RemoteClose event. +TEST_F(ExtAuthzFilterTest, DoNotCallCancelonRemoteClose) { + InSequence s; + + EXPECT_CALL(filter_callbacks_.connection_, remoteAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(filter_callbacks_.connection_, localAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(*client_, check(_, _, _)) + .WillOnce(WithArgs<0>( + Invoke([&](RequestCallbacks& callbacks) -> void { request_callbacks_ = &callbacks; }))); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection()); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data)); + + EXPECT_CALL(filter_callbacks_, continueReading()); + request_callbacks_->onComplete(CheckStatus::Error); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data)); + + EXPECT_CALL(*client_, cancel()).Times(0); + filter_callbacks_.connection_.raiseEvent(Network::ConnectionEvent::RemoteClose); + + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.total").value()); + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.error").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.denied").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.ok").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.cx_closed").value()); +} + +// Test to verify that Cancel is invoked when a RemoteClose event occurs while the call +// to the authorization service was in progress. +TEST_F(ExtAuthzFilterTest, VerifyCancelOnRemoteClose) { + InSequence s; + + EXPECT_CALL(filter_callbacks_.connection_, remoteAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(filter_callbacks_.connection_, localAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(*client_, check(_, _, _)) + .WillOnce(WithArgs<0>( + Invoke([&](RequestCallbacks& callbacks) -> void { request_callbacks_ = &callbacks; }))); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection()); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data)); + + EXPECT_CALL(*client_, cancel()); + filter_callbacks_.connection_.raiseEvent(Network::ConnectionEvent::RemoteClose); + + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.total").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.error").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.denied").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.ok").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.cx_closed").value()); +} + +// Test to verify that on stack response from the authorization service does NOT +// result in calling cancel. +TEST_F(ExtAuthzFilterTest, ImmediateOK) { + InSequence s; + + EXPECT_CALL(filter_callbacks_.connection_, remoteAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(filter_callbacks_.connection_, localAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(filter_callbacks_, continueReading()).Times(0); + EXPECT_CALL(*client_, check(_, _, _)) + .WillOnce(WithArgs<0>(Invoke( + [&](RequestCallbacks& callbacks) -> void { callbacks.onComplete(CheckStatus::OK); }))); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection()); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data)); + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data)); + + EXPECT_CALL(*client_, cancel()).Times(0); + filter_callbacks_.connection_.raiseEvent(Network::ConnectionEvent::RemoteClose); + + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.total").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.error").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.denied").value()); + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.ok").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.cx_closed").value()); +} + +} // namespace TcpFilter +} // namespace ExtAuthz +} // namespace Envoy diff --git a/test/server/config/network/BUILD b/test/server/config/network/BUILD index 015b5ee748180..c51d869bb03ae 100644 --- a/test/server/config/network/BUILD +++ b/test/server/config/network/BUILD @@ -15,13 +15,16 @@ envoy_cc_test( "//source/common/access_log:access_log_lib", "//source/common/config:well_known_names", "//source/common/dynamo:dynamo_filter_lib", + "//source/common/protobuf:utility_lib", "//source/server/config/access_log:file_access_log_lib", "//source/server/config/network:client_ssl_auth_lib", + "//source/server/config/network:ext_authz_lib", "//source/server/config/network:http_connection_manager_lib", "//source/server/config/network:mongo_proxy_lib", "//source/server/config/network:ratelimit_lib", "//source/server/config/network:redis_proxy_lib", "//source/server/config/network:tcp_proxy_lib", + "//test/mocks/grpc:grpc_mocks", "//test/mocks/server:server_mocks", "//test/test_common:utility_lib", ], diff --git a/test/server/config/network/config_test.cc b/test/server/config/network/config_test.cc index 4cbba58826b45..2a3bad76b5db3 100644 --- a/test/server/config/network/config_test.cc +++ b/test/server/config/network/config_test.cc @@ -6,21 +6,25 @@ #include "common/config/filter_json.h" #include "common/config/well_known_names.h" #include "common/dynamo/dynamo_filter.h" +#include "common/protobuf/utility.h" #include "server/config/access_log/file_access_log.h" #include "server/config/network/client_ssl_auth.h" +#include "server/config/network/ext_authz.h" #include "server/config/network/http_connection_manager.h" #include "server/config/network/mongo_proxy.h" #include "server/config/network/ratelimit.h" #include "server/config/network/redis_proxy.h" #include "server/config/network/tcp_proxy.h" +#include "test/mocks/grpc/mocks.h" #include "test/mocks/server/mocks.h" #include "test/test_common/utility.h" #include "gmock/gmock.h" #include "gtest/gtest.h" +using testing::Invoke; using testing::NiceMock; using testing::_; @@ -44,9 +48,12 @@ TEST(NetworkFilterConfigTest, ValidateFail) { envoy::config::filter::network::redis_proxy::v2::RedisProxy redis_proto; TcpProxyConfigFactory tcp_proxy_factory; envoy::config::filter::network::tcp_proxy::v2::TcpProxy tcp_proxy_proto; + ExtAuthzConfigFactory ext_authz_factory; + envoy::config::filter::network::ext_authz::v2::ExtAuthz ext_authz_proto; const std::vector> filter_cases = { {client_ssl_auth_factory, client_ssl_auth_proto}, + {ext_authz_factory, ext_authz_proto}, {hcm_factory, hcm_proto}, {mongo_factory, mongo_proto}, {rate_limit_factory, rate_limit_proto}, @@ -532,6 +539,36 @@ TEST(TcpProxyConfigTest, TcpProxyConfigTest) { cb(connection); } +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"; + + envoy::config::filter::network::ext_authz::v2::ExtAuthz proto_config{}; + MessageUtil::loadFromJson(json, proto_config); + + NiceMock context; + ExtAuthzConfigFactory factory; + + EXPECT_CALL(context.cluster_manager_.async_client_manager_, factoryForGrpcService(_, _)) + .WillOnce(Invoke([](const envoy::api::v2::core::GrpcService&, Stats::Scope&) { + return std::make_unique>(); + })); + NetworkFilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, context); + Network::MockConnection connection; + EXPECT_CALL(connection, addReadFilter(_)); + cb(connection); +} + } // namespace Configuration } // namespace Server } // namespace Envoy