diff --git a/bazel/repositories.bzl b/bazel/repositories.bzl index eae8fd4a28448..bc5b863a804da 100644 --- a/bazel/repositories.bzl +++ b/bazel/repositories.bzl @@ -168,6 +168,7 @@ def _envoy_api_deps(): "rate_limit", "router", "transcoder", + "ext_authz", ] for t in http_filter_bind_targets: native.bind( @@ -181,6 +182,7 @@ def _envoy_api_deps(): "redis_proxy", "rate_limit", "client_ssl_auth", + "ext_authz", ] for t in network_filter_bind_targets: native.bind( @@ -195,6 +197,14 @@ def _envoy_api_deps(): name = "http_api_protos", actual = "@googleapis//:http_api_protos", ) + sub_bind_targets = [ + ("auth", "auth"), + ] + for t in sub_bind_targets: + native.bind( + name = "envoy_api_sub_" + t[0], + actual = "@envoy_api//api/" + t[0] + ":" + t[1] + "_cc", + ) def envoy_dependencies(path = "@envoy_deps//", skip_targets = []): envoy_repository = repository_rule( diff --git a/include/envoy/ext_authz/BUILD b/include/envoy/ext_authz/BUILD new file mode 100644 index 0000000000000..ba8c77c594b5e --- /dev/null +++ b/include/envoy/ext_authz/BUILD @@ -0,0 +1,24 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", + "envoy_proto_library", +) + +envoy_package() + +envoy_cc_library( + name = "ext_authz_interface", + hdrs = ["ext_authz.h"], + external_deps = ["envoy_api_sub_auth"], + deps = [ + "//include/envoy/common:optional", + "//include/envoy/http:filter_interface", + "//include/envoy/http:header_map_interface", + "//include/envoy/network:filter_interface", + "//include/envoy/tracing:http_tracer_interface", + ], +) + diff --git a/include/envoy/ext_authz/ext_authz.h b/include/envoy/ext_authz/ext_authz.h new file mode 100644 index 0000000000000..d9671eb3e72a1 --- /dev/null +++ b/include/envoy/ext_authz/ext_authz.h @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/common/optional.h" +#include "envoy/common/pure.h" +#include "envoy/http/filter.h" +#include "envoy/http/header_map.h" +#include "envoy/network/filter.h" +#include "envoy/tracing/http_tracer.h" + +#include "api/auth/external_auth.pb.h" + +namespace Envoy { +namespace ExtAuthz { + +using envoy::api::v2::auth::CheckRequest; + +/** + * Possible async results for a check call. + */ +enum class CheckStatus { + // The request is authorized. + OK, + // The authz service could not be queried. + Error, + // The request is denied. + Denied +}; + +/** + * Async callbacks used during check() calls. + */ +class RequestCallbacks { +public: + virtual ~RequestCallbacks() {} + + /** + * Called when a check request is complete. The resulting status is supplied. + */ + virtual void complete(CheckStatus status) PURE; +}; + + +class Client { + public: + // Destructor + virtual ~Client() {} + + /** + * Cancel an inflight Check request. + */ + virtual void cancel() PURE; + + // A check call. + virtual void check(RequestCallbacks &callback, const CheckRequest& request, + Tracing::Span& parent_span) PURE; + +}; + +typedef std::unique_ptr ClientPtr; + +/** + * An interface for creating a external authorization client. + */ +class ClientFactory { +public: + virtual ~ClientFactory() {} + + /** + * Return a new authz client. + */ + virtual ClientPtr create(const Optional& timeout) PURE; +}; + +typedef std::unique_ptr ClientFactoryPtr; + + +/** + * An interface for creating ext_authz.proto (authorization) request. + */ +class CheckRequestGenIntf { +public: + // Destructor + virtual ~CheckRequestGenIntf() {} + + virtual void createHttpCheck(const Envoy::Http::StreamDecoderFilterCallbacks* callbacks, + const Envoy::Http::HeaderMap &headers, + envoy::api::v2::auth::CheckRequest& request) PURE; + virtual void createTcpCheck(const Network::ReadFilterCallbacks* callbacks, + envoy::api::v2::auth::CheckRequest& request) PURE; +}; + +typedef std::unique_ptr CheckRequestGenIntfPtr; + +} // namespace ExtAuthz +} // namespace Envoy + diff --git a/include/envoy/request_info/request_info.h b/include/envoy/request_info/request_info.h index ad36706ab192a..ccee61596d99d 100644 --- a/include/envoy/request_info/request_info.h +++ b/include/envoy/request_info/request_info.h @@ -38,8 +38,10 @@ enum ResponseFlag { FaultInjected = 0x400, // Request was ratelimited locally by rate limit filter. RateLimited = 0x800, + // Request was unauthorized. + Unauthorized = 0x1000, // ATTENTION: MAKE SURE THIS REMAINS EQUAL TO THE LAST FLAG. - LastFlag = RateLimited + LastFlag = Unauthorized }; /** diff --git a/source/common/access_log/grpc_access_log_impl.cc b/source/common/access_log/grpc_access_log_impl.cc index cbc383850c620..fb3d0d9e46166 100644 --- a/source/common/access_log/grpc_access_log_impl.cc +++ b/source/common/access_log/grpc_access_log_impl.cc @@ -82,7 +82,7 @@ void HttpGrpcAccessLog::responseFlagsToAccessLogResponseFlags( envoy::api::v2::filter::accesslog::AccessLogCommon& common_access_log, const RequestInfo::RequestInfo& request_info) { - static_assert(RequestInfo::ResponseFlag::LastFlag == 0x800, + static_assert(RequestInfo::ResponseFlag::LastFlag == 0x1000, "A flag has been added. Fix this code."); if (request_info.getResponseFlag(RequestInfo::ResponseFlag::FailedLocalHealthCheck)) { @@ -132,6 +132,13 @@ void HttpGrpcAccessLog::responseFlagsToAccessLogResponseFlags( if (request_info.getResponseFlag(RequestInfo::ResponseFlag::RateLimited)) { common_access_log.mutable_response_flags()->set_rate_limited(true); } + + // @saumoh: TBD To the accesslog.proto + // + // if (request_info.getResponseFlag(RequestInfo::ResponseFlag::Unauthorized)) { + // common_access_log.mutable_response_flags()->set_unauthorized(true); + // } + } void HttpGrpcAccessLog::log(const Http::HeaderMap* request_headers, diff --git a/source/common/config/BUILD b/source/common/config/BUILD index 9094cd22ae025..06cfba789299c 100644 --- a/source/common/config/BUILD +++ b/source/common/config/BUILD @@ -91,12 +91,14 @@ envoy_cc_library( external_deps = [ "envoy_filter_network_http_connection_manager", "envoy_filter_http_buffer", + "envoy_filter_http_ext_authz", "envoy_filter_http_lua", "envoy_filter_http_fault", "envoy_filter_http_health_check", "envoy_filter_http_rate_limit", "envoy_filter_http_transcoder", "envoy_filter_http_router", + "envoy_filter_network_ext_authz", "envoy_filter_network_mongo_proxy", "envoy_filter_network_redis_proxy", "envoy_filter_network_tcp_proxy", diff --git a/source/common/config/filter_json.cc b/source/common/config/filter_json.cc index 1ea6196905a88..8c2a5110b9d23 100644 --- a/source/common/config/filter_json.cc +++ b/source/common/config/filter_json.cc @@ -399,5 +399,36 @@ void FilterJson::translateClientSslAuthFilter( *proto_config.mutable_ip_white_list()); } +void FilterJson::translateTcpExtAuthzFilter( + const Json::Object& json_config, envoy::api::v2::filter::network::ExtAuthz& proto_config) { + json_config.validateSchema(Json::Schema::EXT_AUTHZ_NETWORK_FILTER_SCHEMA); + + JSON_UTIL_SET_STRING(json_config, proto_config, stat_prefix); + proto_config.set_failure_mode_allow(json_config.getBoolean("failure_mode_allow", false)); + + const auto &json_grpc_cluster = json_config.getObject("grpc_cluster", false); + auto *grpc_service = proto_config.mutable_grpc_service(); + JSON_UTIL_SET_DURATION(*json_grpc_cluster, *grpc_service, timeout); + + auto *grpc_cluster = grpc_service->mutable_envoy_grpc(); + JSON_UTIL_SET_STRING(*json_grpc_cluster, *grpc_cluster, cluster_name); +} + +void FilterJson::translateHttpExtAuthzFilter( + const Json::Object& json_config, envoy::api::v2::filter::http::ExtAuthz& proto_config) { + json_config.validateSchema(Json::Schema::EXT_AUTHZ_HTTP_FILTER_SCHEMA); + + proto_config.set_failure_mode_allow(json_config.getBoolean("failure_mode_allow", false)); + + + const auto &json_grpc_cluster = json_config.getObject("grpc_cluster", false); + auto *grpc_service = proto_config.mutable_grpc_service(); + JSON_UTIL_SET_DURATION(*json_grpc_cluster, *grpc_service, timeout); + + auto *grpc_cluster = grpc_service->mutable_envoy_grpc(); + JSON_UTIL_SET_STRING(*json_grpc_cluster, *grpc_cluster, cluster_name); +} + + } // namespace Config } // namespace Envoy diff --git a/source/common/config/filter_json.h b/source/common/config/filter_json.h index 6775371d29c68..5dc9387cc3ea1 100644 --- a/source/common/config/filter_json.h +++ b/source/common/config/filter_json.h @@ -3,6 +3,7 @@ #include "envoy/json/json_object.h" #include "api/filter/http/buffer.pb.h" +#include "api/filter/http/ext_authz.pb.h" #include "api/filter/http/fault.pb.h" #include "api/filter/http/health_check.pb.h" #include "api/filter/http/lua.pb.h" @@ -10,6 +11,7 @@ #include "api/filter/http/router.pb.h" #include "api/filter/http/transcoder.pb.h" #include "api/filter/network/client_ssl_auth.pb.h" +#include "api/filter/network/ext_authz.pb.h" #include "api/filter/network/http_connection_manager.pb.h" #include "api/filter/network/mongo_proxy.pb.h" #include "api/filter/network/rate_limit.pb.h" @@ -157,6 +159,24 @@ class FilterJson { static void translateClientSslAuthFilter(const Json::Object& json_config, envoy::api::v2::filter::network::ClientSSLAuth& proto_config); + + /** + * Translate a v1 JSON TCP external Authorization filter object to v2 + * envoy::api::v2::filter::network::ExtAuthz. + * @param json_config source v1 JSON Tc Authorization Filter object. + * @param proto_config destination v2 envoy::api::v2::filter::network::Authz + */ + static void translateTcpExtAuthzFilter(const Json::Object &json_config, + envoy::api::v2::filter::network::ExtAuthz& proto_config); + + /** + * Translate a v1 JSON HTTP external Authorization filter object to v2 + * envoy::api::v2::filter::http::ExtAuthz. + * @param json_config source v1 JSON HTTP Authorization Filter object. + * @param proto_config destination v2 envoy::api::v2::filter::http::Authz. + */ + static void translateHttpExtAuthzFilter(const Json::Object& json_config, + envoy::api::v2::filter::http::ExtAuthz& proto_config); }; } // namespace Config diff --git a/source/common/config/well_known_names.h b/source/common/config/well_known_names.h index 40756b131972c..43fa371c0cf29 100644 --- a/source/common/config/well_known_names.h +++ b/source/common/config/well_known_names.h @@ -66,13 +66,15 @@ class NetworkFilterNameValues { const std::string REDIS_PROXY = "envoy.redis_proxy"; // IP tagging filter const std::string TCP_PROXY = "envoy.tcp_proxy"; + // Authorization filter + const std::string EXT_AUTHORIZATION = "envoy.ext_authz"; // Converts names from v1 to v2 const V1Converter v1_converter_; NetworkFilterNameValues() : v1_converter_({CLIENT_SSL_AUTH, ECHO, HTTP_CONNECTION_MANAGER, MONGO_PROXY, RATE_LIMIT, - REDIS_PROXY, TCP_PROXY}) {} + REDIS_PROXY, TCP_PROXY, EXT_AUTHORIZATION}) {} }; typedef ConstSingleton NetworkFilterNames; @@ -117,13 +119,16 @@ class HttpFilterNameValues { const std::string HEALTH_CHECK = "envoy.health_check"; // Lua filter const std::string LUA = "envoy.lua"; + // External Authorization filter + const std::string EXT_AUTHORIZATION = "envoy.ext_authz"; + // Converts names from v1 to v2 const V1Converter v1_converter_; HttpFilterNameValues() : v1_converter_({BUFFER, CORS, DYNAMO, FAULT, GRPC_HTTP1_BRIDGE, GRPC_JSON_TRANSCODER, - GRPC_WEB, HEALTH_CHECK, IP_TAGGING, RATE_LIMIT, ROUTER, LUA}) {} + GRPC_WEB, HEALTH_CHECK, IP_TAGGING, RATE_LIMIT, ROUTER, LUA, EXT_AUTHORIZATION}) {} }; typedef ConstSingleton HttpFilterNames; diff --git a/source/common/ext_authz/BUILD b/source/common/ext_authz/BUILD new file mode 100644 index 0000000000000..61714cb4a3257 --- /dev/null +++ b/source/common/ext_authz/BUILD @@ -0,0 +1,30 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_library( + name = "ext_authz_lib", + srcs = ["ext_authz_impl.cc"], + hdrs = ["ext_authz_impl.h"], + external_deps = ["envoy_bootstrap"], + deps = [ + "//include/envoy/ext_authz:ext_authz_interface", + "//include/envoy/grpc:async_client_interface", + "//include/envoy/http:protocol_interface", + "//include/envoy/network:address_interface", + "//include/envoy/network:connection_interface", + "//include/envoy/upstream:cluster_manager_interface", + "//include/envoy/ssl:connection_interface", + "//source/common/common:assert_lib", + "//source/common/grpc:async_client_lib", + "//source/common/http:headers_lib", + "//source/common/tracing:http_tracer_lib", + ], +) + diff --git a/source/common/ext_authz/ext_authz_impl.cc b/source/common/ext_authz/ext_authz_impl.cc new file mode 100644 index 0000000000000..f12c8baed88cd --- /dev/null +++ b/source/common/ext_authz/ext_authz_impl.cc @@ -0,0 +1,250 @@ +#include "common/ext_authz/ext_authz_impl.h" + +#include +#include +#include +#include + +#include "envoy/access_log/access_log.h" +#include "envoy/ssl/connection.h" + +#include "common/common/assert.h" +#include "common/grpc/async_client_impl.h" +#include "common/http/headers.h" + +#include "fmt/format.h" + +namespace Envoy { +namespace ExtAuthz { + +GrpcClientImpl::GrpcClientImpl(ExtAuthzAsyncClientPtr&& async_client, + const Optional& timeout) + : service_method_(*Protobuf::DescriptorPool::generated_pool()->FindMethodByName( + "envoy.api.v2.auth.Authorization.Check")), + async_client_(std::move(async_client)), timeout_(timeout) {} + +GrpcClientImpl::~GrpcClientImpl() { ASSERT(!callbacks_); } + +void GrpcClientImpl::cancel() { + ASSERT(callbacks_ != nullptr); + request_->cancel(); + callbacks_ = nullptr; +} + +void GrpcClientImpl::check(RequestCallbacks& callbacks, const envoy::api::v2::auth::CheckRequest& request, + Tracing::Span& parent_span) { + ASSERT(callbacks_ == nullptr); + callbacks_ = &callbacks; + + request_ = async_client_->send(service_method_, request, *this, parent_span, timeout_); +} + +void GrpcClientImpl::onSuccess(std::unique_ptr&& response, + Tracing::Span& span) { + CheckStatus status = CheckStatus::OK; + ASSERT(response->status().code() != Grpc::Status::GrpcStatus::Unknown); + if (response->status().code() != Grpc::Status::GrpcStatus::Ok) { + status = CheckStatus::Denied; + span.setTag(Constants::get().TraceStatus, Constants::get().TraceUnauthz); + } else { + span.setTag(Constants::get().TraceStatus, Constants::get().TraceOk); + } + + callbacks_->complete(status); + callbacks_ = nullptr; +} + +void GrpcClientImpl::onFailure(Grpc::Status::GrpcStatus status, const std::string&, + Tracing::Span&) { + ASSERT(status != Grpc::Status::GrpcStatus::Ok); + UNREFERENCED_PARAMETER(status); + callbacks_->complete(CheckStatus::Error); + callbacks_ = nullptr; +} + +GrpcFactoryImpl::GrpcFactoryImpl(const std::string& cluster_name, + Upstream::ClusterManager& cm) + : cluster_name_(cluster_name), cm_(cm) { + if (!cm_.get(cluster_name_)) { + throw EnvoyException(fmt::format("unknown external authorization service cluster '{}'", cluster_name_)); + } +} + +ClientPtr GrpcFactoryImpl::create(const Optional& timeout) { + return ClientPtr{new GrpcClientImpl( + ExtAuthzAsyncClientPtr{ + new Grpc::AsyncClientImpl(cm_, cluster_name_)}, + timeout)}; +} + +::envoy::api::v2::Address* CheckRequestGen::get_pbuf_address(const Network::Address::InstanceConstSharedPtr& instance) { + using ::envoy::api::v2::Address; + + Address *addr = new Address(); + ASSERT(addr); + + if (instance->type() == Network::Address::Type::Ip) { + addr->mutable_socket_address()->set_address(instance->ip()->addressAsString()); + addr->mutable_socket_address()->set_port_value(instance->ip()->port()); + } else { + ASSERT(instance->type() == Network::Address::Type::Pipe); + addr->mutable_pipe()->set_path(instance->asString()); + } + return addr; +} + +::envoy::api::v2::auth::AttributeContext_Peer* CheckRequestGen::get_connection_peer(const Network::Connection *connection, const std::string& service, const bool local) { + using ::envoy::api::v2::auth::AttributeContext_Peer; + + AttributeContext_Peer *peer = new AttributeContext_Peer(); + ASSERT(peer); + + // Set the address + if (local == false) { + peer->set_allocated_address(get_pbuf_address(connection->remoteAddress())); + } else { + peer->set_allocated_address(get_pbuf_address(connection->localAddress())); + } + + // Set the principal + // Preferably the SAN from the peer's cert or + // Subject from the peer's cert. + std::string principal; + Ssl::Connection* ssl = + const_cast(connection->ssl()); + if (ssl != nullptr) { + if (local == false) { + principal = ssl->uriSanPeerCertificate(); + + if (principal.empty()) { + principal = ssl->subjectPeerCertificate(); + } + } else { + principal = ssl->uriSanLocalCertificate(); + + if (principal.empty()) { + principal = ssl->subjectLocalCertificate(); + } + } + + } + peer->set_principal(principal); + + if (!service.empty()) { + peer->set_service(service); + } + + return peer; +} + +::envoy::api::v2::auth::AttributeContext_Peer* CheckRequestGen::get_connection_peer(const Network::Connection& connection, const std::string& service, const bool local) { + return get_connection_peer(&connection, service, local); +} + + +const std::string CheckRequestGen::proto2str(const Envoy::Http::Protocol& p) { + using Envoy::Http::Protocol; + + switch(p) { + case Protocol::Http10: + return std::string("Http1.0"); + case Protocol::Http11: + return std::string("Http1.1"); + case Protocol::Http2: + return std::string("Http2"); + default: + break; + } + return std::string("unknown"); +} + +Envoy::Http::HeaderMap::Iterate CheckRequestGen::fill_http_headers(const Envoy::Http::HeaderEntry &e, void *ctx) { + ::google::protobuf::Map< ::std::string, ::std::string >* mhdrs = static_cast<::google::protobuf::Map< ::std::string, ::std::string >*>(ctx); + (*mhdrs)[std::string(e.key().c_str(), e.key().size())] = std::string(e.value().c_str(), e.value().size()); + return Envoy::Http::HeaderMap::Iterate::Continue; +} + + +::envoy::api::v2::auth::AttributeContext_Request* CheckRequestGen::get_http_request(const Envoy::Http::StreamDecoderFilterCallbacks* callbacks, const Envoy::Http::HeaderMap &headers) { + using ::envoy::api::v2::auth::AttributeContext_Request; + using ::envoy::api::v2::auth::AttributeContext_HTTPRequest; + + AttributeContext_HTTPRequest *httpreq = new AttributeContext_HTTPRequest(); + ASSERT(httpreq); + + // Set id + // The streamId is not qualified as a const. Although it is as it does not modify the object. + Envoy::Http::StreamDecoderFilterCallbacks *sdfc = const_cast(callbacks); + httpreq->set_id(std::to_string(sdfc->streamId())); + + #define SET_HDR_IN_HTTPREQ(_hq, _api, mname) \ + do { \ + const Envoy::Http::HeaderEntry *_entry = (_api)(); \ + if (_entry != nullptr) { \ + std::string _s(_entry->value().c_str(), _entry->value().size()); \ + (_hq)->set_##mname(_s); \ + } \ + } while(0) + + + // Set method + SET_HDR_IN_HTTPREQ(httpreq, headers.Method, method); + // Set path + SET_HDR_IN_HTTPREQ(httpreq, headers.Path, path); + // Set host + SET_HDR_IN_HTTPREQ(httpreq, headers.Host, host); + // Set scheme + SET_HDR_IN_HTTPREQ(httpreq, headers.Scheme, scheme); + + // Set size + // need to convert to google buffer 64t; + httpreq->set_size(sdfc->requestInfo().bytesReceived()); + + // Set protocol + httpreq->set_protocol(proto2str(sdfc->requestInfo().protocol().value())); + + // Fill in the headers + ::google::protobuf::Map< ::std::string, ::std::string >* mhdrs = httpreq->mutable_headers(); + headers.iterate(fill_http_headers, mhdrs); + + + AttributeContext_Request *req = new AttributeContext_Request(); + ASSERT(req); + req->set_allocated_http(httpreq); + + return req; +} + +void CheckRequestGen::createHttpCheck(const Envoy::Http::StreamDecoderFilterCallbacks* callbacks, const Envoy::Http::HeaderMap &headers, envoy::api::v2::auth::CheckRequest& request) { + using ::envoy::api::v2::auth::AttributeContext; + + AttributeContext* attrs = request.mutable_attributes(); + ASSERT(attrs); + + Envoy::Http::StreamDecoderFilterCallbacks* cb = const_cast(callbacks); + + std::string service; + const Envoy::Http::HeaderEntry *entry = headers.EnvoyDownstreamServiceCluster(); + if (entry != nullptr) { + service = std::string(entry->value().c_str(), entry->value().size()); + } + attrs->set_allocated_source(get_connection_peer(cb->connection(), service, false)); + attrs->set_allocated_destination(get_connection_peer(cb->connection(), "", true)); + attrs->set_allocated_request(get_http_request(callbacks, headers)); +} + +void CheckRequestGen::createTcpCheck(const Network::ReadFilterCallbacks* callbacks, envoy::api::v2::auth::CheckRequest& request) { + using ::envoy::api::v2::auth::AttributeContext; + + AttributeContext* attrs = request.mutable_attributes(); + ASSERT(attrs); + + Network::ReadFilterCallbacks* cb = const_cast(callbacks); + attrs->set_allocated_source(get_connection_peer(cb->connection(), "", false)); + attrs->set_allocated_destination(get_connection_peer(cb->connection(), "", true)); +} + +} // namespace ExtAuthz +} // namespace Envoy + diff --git a/source/common/ext_authz/ext_authz_impl.h b/source/common/ext_authz/ext_authz_impl.h new file mode 100644 index 0000000000000..c67cb426a4e71 --- /dev/null +++ b/source/common/ext_authz/ext_authz_impl.h @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/ext_authz/ext_authz.h" +#include "envoy/grpc/async_client.h" +#include "envoy/http/protocol.h" +#include "envoy/network/address.h" +#include "envoy/network/connection.h" + +#include "envoy/tracing/http_tracer.h" +#include "envoy/upstream/cluster_manager.h" + +#include "common/singleton/const_singleton.h" + +#include "api/bootstrap.pb.h" + +namespace Envoy { +namespace ExtAuthz { + +typedef Grpc::AsyncClient + ExtAuthzAsyncClient; +typedef std::unique_ptr ExtAuthzAsyncClientPtr; + +typedef Grpc::AsyncRequestCallbacks ExtAuthzAsyncCallbacks; + +struct ConstantValues { + const std::string TraceStatus = "ext_authz_status"; + const std::string TraceUnauthz = "ext_authz_unauthorized"; + const std::string TraceOk = "ext_authz_ok"; +}; + +typedef ConstSingleton Constants; + +// TODO(htuch): We should have only one client per thread, but today we create one per filter stack. +// This will require support for more than one outstanding request per client. +class GrpcClientImpl : public Client, public ExtAuthzAsyncCallbacks { +public: + GrpcClientImpl(ExtAuthzAsyncClientPtr&& async_client, + const Optional& timeout); + ~GrpcClientImpl(); + + // ExtAuthz::Client + void cancel() override; + void check(RequestCallbacks& callbacks, const envoy::api::v2::auth::CheckRequest& request, + Tracing::Span& parent_span) override; + + // Grpc::AsyncRequestCallbacks + void onCreateInitialMetadata(Http::HeaderMap&) override {} + void onSuccess(std::unique_ptr&& response, + Tracing::Span& span) override; + void onFailure(Grpc::Status::GrpcStatus status, const std::string& message, + Tracing::Span& span) override; + +private: + const Protobuf::MethodDescriptor& service_method_; + ExtAuthzAsyncClientPtr async_client_; + Grpc::AsyncRequest* request_{}; + Optional timeout_; + RequestCallbacks* callbacks_{}; +}; + +class GrpcFactoryImpl : public ClientFactory { +public: + GrpcFactoryImpl(const std::string& cluster_name, + Upstream::ClusterManager& cm); + + // ExtAuthz::ClientFactory + ClientPtr create(const Optional& timeout) override; + +private: + const std::string cluster_name_; + Upstream::ClusterManager& cm_; +}; + +class NullClientImpl : public Client { +public: + // ExtAuthz::Client + void cancel() override {} + void check(RequestCallbacks& callbacks, const envoy::api::v2::auth::CheckRequest&, + Tracing::Span&) override { + callbacks.complete(CheckStatus::OK); + } +}; + +class NullFactoryImpl : public ClientFactory { +public: + // ExtAuthz::ClientFactory + ClientPtr create(const Optional&) override { + return ClientPtr{new NullClientImpl()}; + } +}; + +class CheckRequestGen: public CheckRequestGenIntf { +public: + CheckRequestGen() {} + ~CheckRequestGen() {} + + // ExtAuthz::CheckRequestGenIntf + void createHttpCheck(const Envoy::Http::StreamDecoderFilterCallbacks* callbacks, const Envoy::Http::HeaderMap &headers, envoy::api::v2::auth::CheckRequest& request); + void createTcpCheck(const Network::ReadFilterCallbacks* callbacks, envoy::api::v2::auth::CheckRequest& request); + +private: + ::envoy::api::v2::Address* get_pbuf_address(const Network::Address::InstanceConstSharedPtr&); + ::envoy::api::v2::auth::AttributeContext_Peer* get_connection_peer(const Network::Connection *, const std::string&, const bool); + ::envoy::api::v2::auth::AttributeContext_Peer* get_connection_peer(const Network::Connection&, const std::string&, const bool); + ::envoy::api::v2::auth::AttributeContext_Request* get_http_request(const Envoy::Http::StreamDecoderFilterCallbacks*, const Envoy::Http::HeaderMap &); + const std::string proto2str(const Envoy::Http::Protocol&); + static Envoy::Http::HeaderMap::Iterate fill_http_headers(const Envoy::Http::HeaderEntry&, void *); +}; + +} // namespace ExtAuthz +} // namespace Envoy diff --git a/source/common/filter/BUILD b/source/common/filter/BUILD index f1eddee693ad9..69d04ee634232 100644 --- a/source/common/filter/BUILD +++ b/source/common/filter/BUILD @@ -67,3 +67,21 @@ envoy_cc_library( "//source/common/request_info:request_info_lib", ], ) + +envoy_cc_library( + name = "ext_authz_lib", + srcs = ["ext_authz.cc"], + hdrs = ["ext_authz.h"], + external_deps = ["envoy_filter_network_ext_authz"], + deps = [ + "//include/envoy/network:connection_interface", + "//include/envoy/network:filter_interface", + "//include/envoy/ext_authz:ext_authz_interface", + "//include/envoy/runtime:runtime_interface", + "//include/envoy/stats:stats_macros", + "//include/envoy/upstream:cluster_manager_interface", + "//source/common/common:assert_lib", + "//source/common/tracing:http_tracer_lib", + "//source/common/ext_authz:ext_authz_lib", + ], +) diff --git a/source/common/filter/ext_authz.cc b/source/common/filter/ext_authz.cc new file mode 100644 index 0000000000000..96e2543ce702b --- /dev/null +++ b/source/common/filter/ext_authz.cc @@ -0,0 +1,109 @@ +#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) { + 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::setCheckReqGen(CheckRequestGenIntf *crg) +{ + ASSERT(crg_ == nullptr); + crg_ = CheckRequestGenIntfPtr{std::move(crg)}; +} + +void Instance::callCheck() { + envoy::api::v2::auth::CheckRequest request; + if (crg_ == nullptr) { + setCheckReqGen(new CheckRequestGen()); + } + crg_->createTcpCheck(filter_callbacks_, request); + + status_ = Status::Calling; + filter_callbacks_->connection().readDisable(true); + config_->stats().active_.inc(); + config_->stats().total_.inc(); + + calling_check_ = true; + client_->check(*this, request, Tracing::NullSpan::instance()); + calling_check_ = false; +} + +Network::FilterStatus Instance::onData(Buffer::Instance&) { + if (status_ == Status::NotStarted) { + // If the ssl handshake was not done and data is the next event! + callCheck(); + } + return status_ == Status::Calling ? Network::FilterStatus::StopIteration + : Network::FilterStatus::Continue; +} + +Network::FilterStatus Instance::onNewConnection() { + // Wait till the next event occurs. + return Network::FilterStatus::Continue; +} + +void Instance::onEvent(Network::ConnectionEvent event) { + // Make sure that any pending request in the client is cancelled. This will be NOP if the + // request already completed. + if (event == Network::ConnectionEvent::RemoteClose || + event == Network::ConnectionEvent::LocalClose) { + if (status_ == Status::Calling) { + client_->cancel(); + config_->stats().active_.dec(); + } + } else { + // SSL connection is post TCP newConnection. Therefore the ext_authz check in onEvent. + // if the ssl handshake was successful then it will invoke the + // Network::ConnectionEvent::Connected. + if (status_ == Status::NotStarted) { + callCheck(); + } + } +} + +void Instance::complete(CheckStatus status) { + status_ = Status::Complete; + filter_callbacks_->connection().readDisable(false); + 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().unauthz_.inc(); + break; + } + + // We fail open if there is an error contacting the service. + 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..a2b05cd08098c --- /dev/null +++ b/source/common/filter/ext_authz.h @@ -0,0 +1,118 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/network/connection.h" +#include "envoy/network/filter.h" +#include "envoy/ext_authz/ext_authz.h" +#include "envoy/runtime/runtime.h" +#include "envoy/stats/stats_macros.h" +#include "envoy/upstream/cluster_manager.h" + +#include "api/filter/network/ext_authz.pb.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(unauthz) \ + 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: + // @saumoh: TBD: Take care of grpc service != envoy_grpc() + Config(const envoy::api::v2::filter::network::ExtAuthz& config, Stats::Scope& scope, + Runtime::Loader& runtime, Upstream::ClusterManager& cm) + : stats_(generateStats(config.stat_prefix(), scope)), + runtime_(runtime), cm_(cm), cluster_name_(config.grpc_service().envoy_grpc().cluster_name()), failure_mode_allow_(config.failure_mode_allow()) {} + + Runtime::Loader& runtime() { return runtime_; } + std::string cluster() { return cluster_name_; } + Upstream::ClusterManager& cm() { return cm_; } + const InstanceStats& stats() { return stats_; } + bool failOpen() const { return failure_mode_allow_; } + +private: + static InstanceStats generateStats(const std::string& name, Stats::Scope& scope); + const InstanceStats stats_; + Runtime::Loader& runtime_; + Upstream::ClusterManager& cm_; + std::string cluster_name_; + 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 complete(CheckStatus status) override; + + void setCheckReqGen(CheckRequestGenIntf* crg); + +private: + enum class Status { NotStarted, Calling, Complete }; + void callCheck(); + + ConfigSharedPtr config_; + ClientPtr client_; + Network::ReadFilterCallbacks* filter_callbacks_{}; + Status status_{Status::NotStarted}; + bool calling_check_{}; + CheckRequestGenIntfPtr crg_{}; +}; + +} // TcpFilter +} // namespace ExtAuthz +} // namespace Envoy + diff --git a/source/common/http/filter/BUILD b/source/common/http/filter/BUILD index 5b381e8c4442e..93d8e93cec8f0 100644 --- a/source/common/http/filter/BUILD +++ b/source/common/http/filter/BUILD @@ -111,3 +111,37 @@ envoy_cc_library( "//source/common/json:json_validator_lib", ], ) + +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/http:codes_lib", + "//source/common/router:config_lib", + "//source/common/ext_authz:ext_authz_lib", + ], +) + +envoy_cc_library( + name = "ext_authz_includes", + hdrs = ["ext_authz.h"], + external_deps = ["envoy_filter_http_ext_authz"], + 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", + ], +) diff --git a/source/common/http/filter/ext_authz.cc b/source/common/http/filter/ext_authz.cc new file mode 100644 index 0000000000000..2c0ee1b2cf783 --- /dev/null +++ b/source/common/http/filter/ext_authz.cc @@ -0,0 +1,143 @@ +#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/http/codes.h" +#include "common/router/config_impl.h" +#include "common/ext_authz/ext_authz_impl.h" + +#include "fmt/format.h" + +namespace Envoy { +namespace Http { +namespace ExtAuthz { + +namespace { + +static 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::setCheckReqGen(Envoy::ExtAuthz::CheckRequestGenIntf *crg) +{ + ASSERT(crg_ == nullptr); + crg_ = Envoy::ExtAuthz::CheckRequestGenIntfPtr{std::move(crg)}; +} + +void Filter::initiateCall(const HeaderMap& headers) { + + Router::RouteConstSharedPtr route = callbacks_->route(); + if (!route || !route->routeEntry()) { + return; + } + + const Router::RouteEntry* route_entry = route->routeEntry(); + Upstream::ThreadLocalCluster* cluster = config_->cm().get(route_entry->clusterName()); + if (!cluster) { + return; + } + cluster_ = cluster->info(); + + if (crg_ == nullptr) { + setCheckReqGen(new Envoy::ExtAuthz::CheckRequestGen()); + } + envoy::api::v2::auth::CheckRequest request; + crg_->createHttpCheck(callbacks_, headers, request); + + state_ = State::Calling; + initiating_call_ = true; + client_->check(*this, request, callbacks_->activeSpan()); + initiating_call_ = false; +} + +FilterHeadersStatus Filter::decodeHeaders(HeaderMap& headers, bool) { + initiateCall(headers); + return (state_ == State::Calling || state_ == State::Responded) + ? FilterHeadersStatus::StopIteration + : FilterHeadersStatus::Continue; +} + +FilterDataStatus Filter::decodeData(Buffer::Instance&, bool) { + ASSERT(state_ != State::Responded); + if (state_ != State::Calling) { + return FilterDataStatus::Continue; + } + // If the request is too large, stop reading new data until the buffer drains. + return FilterDataStatus::StopIterationAndWatermark; +} + +FilterTrailersStatus Filter::decodeTrailers(HeaderMap&) { + ASSERT(state_ != State::Responded); + 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::complete(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.unauthz").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_->failOpen())) { + state_ = State::Responded; + 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..33d5399810d51 --- /dev/null +++ b/source/common/http/filter/ext_authz.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/http/filter.h" +#include "envoy/local_info/local_info.h" +#include "envoy/ext_authz/ext_authz.h" +#include "envoy/runtime/runtime.h" +#include "envoy/upstream/cluster_manager.h" + +#include "common/common/assert.h" +#include "common/http/header_map_impl.h" + +#include "api/filter/http/ext_authz.pb.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: + // @saumoh: TBD : Take care of grpc service != envoy_grpc() + FilterConfig(const envoy::api::v2::filter::http::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 failOpen() 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)) {} + + ~Filter() {} + + // 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 complete(Envoy::ExtAuthz::CheckStatus status) override; + + void setCheckReqGen(Envoy::ExtAuthz::CheckRequestGenIntf* crg); + +private: + enum class State { NotStarted, Calling, Complete, Responded }; + void initiateCall(const HeaderMap& headers); + + FilterConfigSharedPtr config_; + Envoy::ExtAuthz::ClientPtr client_; + StreamDecoderFilterCallbacks* callbacks_{}; + State state_{State::NotStarted}; + Upstream::ClusterInfoConstSharedPtr cluster_; + bool initiating_call_{}; + Envoy::ExtAuthz::CheckRequestGenIntfPtr crg_{}; +}; + +} // namespace ExtAuthz +} // namespace Http +} // namespace Envoy diff --git a/source/common/json/config_schemas.cc b/source/common/json/config_schemas.cc index f887d8e121d6d..0691ada812b1c 100644 --- a/source/common/json/config_schemas.cc +++ b/source/common/json/config_schemas.cc @@ -518,6 +518,31 @@ const std::string Json::Schema::TCP_PROXY_NETWORK_FILTER_SCHEMA(R"EOF( } )EOF"); +const std::string Json::Schema::EXT_AUTHZ_NETWORK_FILTER_SCHEMA(R"EOF( + { + "$schema": "http://json-schema.org/schema#", + "definitions" : { + "grpc_cluster" : { + "type" : "object", + "properties" : { + "cluster_name" : {"type": "string"}, + "timeout": {"type": "integer"} + }, + "required" : ["cluster_name"], + "additionalProperties" : false + } + }, + "type" : "object", + "properties" : { + "stat_prefix" : {"type" : "string"}, + "grpc_cluster" : {"$ref" : "#/definitions/grpc_cluster"}, + "failure_mode_allow" : {"type" : "boolean"} + }, + "required" : ["stat_prefix", "grpc_cluster"], + "additionalProperties" : false + } + )EOF"); + const std::string Json::Schema::ROUTE_CONFIGURATION_SCHEMA(R"EOF( { "$schema": "http://json-schema.org/schema#", @@ -1127,6 +1152,31 @@ const std::string Json::Schema::ROUTER_HTTP_FILTER_SCHEMA(R"EOF( } )EOF"); +const std::string Json::Schema::EXT_AUTHZ_HTTP_FILTER_SCHEMA(R"EOF( + { + "$schema": "http://json-schema.org/schema#", + "definitions" : { + "grpc_cluster" : { + "type" : "object", + "properties" : { + "cluster_name" : {"type": "string"}, + "timeout": {"type": "integer"} + }, + "required" : ["cluster_name"], + "additionalProperties" : false + } + }, + "type" : "object", + "properties" : { + "grpc_cluster" : {"$ref" : "#/definitions/grpc_cluster"}, + "failure_mode_allow" : {"type" : "boolean"} + }, + "required" : ["grpc_cluster"], + "additionalProperties" : false + } + )EOF"); + + const std::string Json::Schema::CLUSTER_MANAGER_SCHEMA(R"EOF( { "$schema": "http://json-schema.org/schema#", diff --git a/source/common/json/config_schemas.h b/source/common/json/config_schemas.h index 7fe902319a397..6ffc8b161994f 100644 --- a/source/common/json/config_schemas.h +++ b/source/common/json/config_schemas.h @@ -25,6 +25,7 @@ class Schema { static const std::string RATELIMIT_NETWORK_FILTER_SCHEMA; static const std::string REDIS_PROXY_NETWORK_FILTER_SCHEMA; static const std::string TCP_PROXY_NETWORK_FILTER_SCHEMA; + static const std::string EXT_AUTHZ_NETWORK_FILTER_SCHEMA; // HTTP Connection Manager Schemas static const std::string ROUTE_CONFIGURATION_SCHEMA; @@ -44,6 +45,7 @@ class Schema { static const std::string RATE_LIMIT_HTTP_FILTER_SCHEMA; static const std::string ROUTER_HTTP_FILTER_SCHEMA; static const std::string LUA_HTTP_FILTER_SCHEMA; + static const std::string EXT_AUTHZ_HTTP_FILTER_SCHEMA; // Cluster Schemas static const std::string CLUSTER_MANAGER_SCHEMA; diff --git a/source/common/request_info/utility.cc b/source/common/request_info/utility.cc index 433c6f1966b70..fb2858017c52a 100644 --- a/source/common/request_info/utility.cc +++ b/source/common/request_info/utility.cc @@ -20,6 +20,7 @@ const std::string ResponseFlagUtils::NO_ROUTE_FOUND = "NR"; const std::string ResponseFlagUtils::DELAY_INJECTED = "DI"; const std::string ResponseFlagUtils::FAULT_INJECTED = "FI"; const std::string ResponseFlagUtils::RATE_LIMITED = "RL"; +const std::string ResponseFlagUtils::UNAUTHORIZED = "UA"; void ResponseFlagUtils::appendString(std::string& result, const std::string& append) { if (result.empty()) { @@ -32,7 +33,7 @@ void ResponseFlagUtils::appendString(std::string& result, const std::string& app const std::string ResponseFlagUtils::toShortString(const RequestInfo& request_info) { std::string result; - static_assert(ResponseFlag::LastFlag == 0x800, "A flag has been added. Fix this code."); + static_assert(ResponseFlag::LastFlag == 0x1000, "A flag has been added. Fix this code."); if (request_info.getResponseFlag(ResponseFlag::FailedLocalHealthCheck)) { appendString(result, FAILED_LOCAL_HEALTH_CHECK); @@ -82,6 +83,10 @@ const std::string ResponseFlagUtils::toShortString(const RequestInfo& request_in appendString(result, RATE_LIMITED); } + if (request_info.getResponseFlag(ResponseFlag::Unauthorized)) { + appendString(result, UNAUTHORIZED); + } + return result.empty() ? NONE : result; } diff --git a/source/common/request_info/utility.h b/source/common/request_info/utility.h index 1f4614a297659..a9288151b1baa 100644 --- a/source/common/request_info/utility.h +++ b/source/common/request_info/utility.h @@ -32,6 +32,7 @@ class ResponseFlagUtils { const static std::string DELAY_INJECTED; const static std::string FAULT_INJECTED; const static std::string RATE_LIMITED; + const static std::string UNAUTHORIZED; }; /** diff --git a/source/exe/BUILD b/source/exe/BUILD index 0fba8d2f9c690..0b7d4465b1052 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", @@ -45,6 +46,7 @@ envoy_cc_library( "//source/server/config/http:lua_lib", "//source/server/config/http:ratelimit_lib", "//source/server/config/http:router_lib", + "//source/server/config/network:ext_authz_lib", "//source/server/config/network:client_ssl_auth_lib", "//source/server/config/network:echo_lib", "//source/server/config/network:http_connection_manager_lib", diff --git a/source/server/BUILD b/source/server/BUILD index d2bea25d768aa..2c6b8c2e01f93 100644 --- a/source/server/BUILD +++ b/source/server/BUILD @@ -36,6 +36,7 @@ envoy_cc_library( "//include/envoy/server:filter_config_interface", "//include/envoy/server:instance_interface", "//include/envoy/ssl:context_manager_interface", + "//source/common/ext_authz:ext_authz_lib", "//source/common/common:assert_lib", "//source/common/common:logger_lib", "//source/common/common:utility_lib", diff --git a/source/server/config/http/BUILD b/source/server/config/http/BUILD index 842bba28b0e2e..d51fcf508fdcc 100644 --- a/source/server/config/http/BUILD +++ b/source/server/config/http/BUILD @@ -189,3 +189,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:filter_json_lib", + "//source/common/config:well_known_names", + "//source/common/http/filter:ext_authz_lib", + "//source/common/protobuf:utility_lib", + ], +) diff --git a/source/server/config/http/ext_authz.cc b/source/server/config/http/ext_authz.cc new file mode 100644 index 0000000000000..d89eb01aad9bb --- /dev/null +++ b/source/server/config/http/ext_authz.cc @@ -0,0 +1,65 @@ +#include "server/config/http/ext_authz.h" + +#include +#include + +#include "envoy/registry/registry.h" + +#include "common/config/filter_json.h" +#include "common/ext_authz/ext_authz_impl.h" +#include "common/http/filter/ext_authz.h" +#include "common/protobuf/utility.h" + +#include "api/filter/http/ext_authz.pb.validate.h" + +namespace Envoy { +namespace Server { +namespace Configuration { + +HttpFilterFactoryCb +ExtAuthzFilterConfig::createFilter(const envoy::api::v2::filter::http::ExtAuthz& proto_config, + const std::string&, FactoryContext& context) { + + ASSERT(proto_config.grpc_service().has_envoy_grpc()); + ASSERT(!proto_config.grpc_service().envoy_grpc().cluster_name().empty()); + + Http::ExtAuthz::FilterConfigSharedPtr filter_config( + new Http::ExtAuthz::FilterConfig(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, 20); + + return [filter_config, timeout_ms](Http::FilterChainFactoryCallbacks& callbacks) -> void { + + ExtAuthz::GrpcFactoryImpl client_factory(filter_config->cluster(), filter_config->cm()); + + callbacks.addStreamDecoderFilter(Http::StreamDecoderFilterSharedPtr{new Http::ExtAuthz::Filter( + filter_config, client_factory.create(std::chrono::milliseconds(timeout_ms)))}); + }; +} + +HttpFilterFactoryCb ExtAuthzFilterConfig::createFilterFactory(const Json::Object& json_config, + const std::string& stats_prefix, + FactoryContext& context) { + envoy::api::v2::filter::http::ExtAuthz proto_config; + Config::FilterJson::translateHttpExtAuthzFilter(json_config, proto_config); + return createFilter(proto_config, stats_prefix, context); +} + +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..790b264fc430d --- /dev/null +++ b/source/server/config/http/ext_authz.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "envoy/server/filter_config.h" + +#include "common/config/well_known_names.h" + +#include "api/filter/http/ext_authz.pb.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::api::v2::filter::http::ExtAuthz()}; + } + + std::string name() override { return Config::HttpFilterNames::get().EXT_AUTHORIZATION; } + +private: + HttpFilterFactoryCb createFilter(const envoy::api::v2::filter::http::ExtAuthz& proto_config, + const std::string& stats_prefix, FactoryContext& context); +}; + +} // namespace Configuration +} // namespace Server +} // namespace Envoy + diff --git a/source/server/config/network/BUILD b/source/server/config/network/BUILD index 949b589c4262b..e2d543595ef0f 100644 --- a/source/server/config/network/BUILD +++ b/source/server/config/network/BUILD @@ -148,3 +148,17 @@ envoy_cc_library( "//source/common/ssl:connection_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:filter_json_lib", + "//source/common/config:well_known_names", + "//source/common/filter:ext_authz_lib", + "//source/common/protobuf:utility_lib", + ], +) diff --git a/source/server/config/network/ext_authz.cc b/source/server/config/network/ext_authz.cc new file mode 100644 index 0000000000000..bbd792d7d6a95 --- /dev/null +++ b/source/server/config/network/ext_authz.cc @@ -0,0 +1,70 @@ +#include "server/config/network/ext_authz.h" + +#include +#include + +#include "envoy/network/connection.h" +#include "envoy/registry/registry.h" +#include "envoy/ext_authz/ext_authz.h" + +#include "common/config/filter_json.h" +#include "common/ext_authz/ext_authz_impl.h" +#include "common/filter/ext_authz.h" +#include "common/protobuf/utility.h" + +#include "api/filter/network/ext_authz.pb.validate.h" + +namespace Envoy { +namespace Server { +namespace Configuration { + +NetworkFilterFactoryCb +ExtAuthzConfigFactory::createFilter(const envoy::api::v2::filter::network::ExtAuthz& proto_config, + FactoryContext& context) { + + ASSERT(!proto_config.stat_prefix().empty()); + ASSERT(proto_config.grpc_service().has_envoy_grpc()); + ASSERT(!proto_config.grpc_service().envoy_grpc().cluster_name().empty()); + + ExtAuthz::TcpFilter::ConfigSharedPtr ext_authz_config( + new ExtAuthz::TcpFilter::Config(proto_config, context.scope(), context.runtime(), context.clusterManager())); + const uint32_t timeout_ms = PROTOBUF_GET_MS_OR_DEFAULT(proto_config.grpc_service(), timeout, 20); + +/* @saumoh: could we just create one client_factory and use it the lambda? + ExtAuthz::ClientFactoryPtr client_factory( + new ExtAuthz::GrpcFactoryImpl(ext_authz_config->cluster(), ext_authz_config->cm())); +*/ + + return [ext_authz_config, timeout_ms](Network::FilterManager& filter_manager) -> void { + ExtAuthz::GrpcFactoryImpl client_factory(ext_authz_config->cluster(), ext_authz_config->cm()); + + filter_manager.addReadFilter(Network::ReadFilterSharedPtr{ + new ExtAuthz::TcpFilter::Instance(ext_authz_config, client_factory.create(std::chrono::milliseconds(timeout_ms)))}); + }; +} + +NetworkFilterFactoryCb ExtAuthzConfigFactory::createFilterFactory(const Json::Object& json_config, + FactoryContext& context) { + envoy::api::v2::filter::network::ExtAuthz proto_config; + Config::FilterJson::translateTcpExtAuthzFilter(json_config, proto_config); + return createFilter(proto_config, context); +} + +NetworkFilterFactoryCb +ExtAuthzConfigFactory::createFilterFactoryFromProto(const Protobuf::Message& proto_config, + FactoryContext& context) { + return createFilter( + MessageUtil::downcastAndValidate( + 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..3419bc42b9a85 --- /dev/null +++ b/source/server/config/network/ext_authz.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include "envoy/server/filter_config.h" + +#include "common/config/well_known_names.h" + +#include "api/filter/network/ext_authz.pb.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::api::v2::filter::network::ExtAuthz()}; + } + + std::string name() override { return Config::NetworkFilterNames::get().EXT_AUTHORIZATION; } + +private: + NetworkFilterFactoryCb + createFilter(const envoy::api::v2::filter::network::ExtAuthz& proto_config, + FactoryContext& context); +}; + +} // namespace Configuration +} // namespace Server +} // namespace Envoy + diff --git a/test/common/ext_authz/BUILD b/test/common/ext_authz/BUILD new file mode 100644 index 0000000000000..7486369149a29 --- /dev/null +++ b/test/common/ext_authz/BUILD @@ -0,0 +1,22 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +envoy_package() + +envoy_cc_test( + name = "ext_authz_impl_test", + srcs = ["ext_authz_impl_test.cc"], + deps = [ + "//source/common/http:header_map_lib", + "//source/common/http:headers_lib", + "//source/common/ext_authz:ext_authz_lib", + "//test/mocks/grpc:grpc_mocks", + "//test/mocks/upstream:upstream_mocks", + "//test/test_common:utility_lib", + ], +) diff --git a/test/common/ext_authz/ext_authz_impl_test.cc b/test/common/ext_authz/ext_authz_impl_test.cc new file mode 100644 index 0000000000000..8eed85a38939c --- /dev/null +++ b/test/common/ext_authz/ext_authz_impl_test.cc @@ -0,0 +1,154 @@ +#include +#include +#include + +#include "common/http/header_map_impl.h" +#include "common/http/headers.h" +#include "common/ext_authz/ext_authz_impl.h" + +#include "test/mocks/grpc/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::AtLeast; +using testing::Invoke; +using testing::Ref; +using testing::Return; +using testing::WithArg; +using testing::_; + +namespace Envoy { +namespace ExtAuthz { + +class MockRequestCallbacks : public RequestCallbacks { +public: + MOCK_METHOD1(complete, void(CheckStatus status)); +}; + +class ExtAuthzGrpcClientTest : public testing::Test { +public: + ExtAuthzGrpcClientTest() + : async_client_(new Grpc::MockAsyncClient()), + client_(ExtAuthzAsyncClientPtr{async_client_}, Optional()) {} + + Grpc::MockAsyncClient* async_client_; + Grpc::MockAsyncRequest async_request_; + GrpcClientImpl client_; + MockRequestCallbacks request_callbacks_; + Tracing::MockSpan span_; +}; + +TEST_F(ExtAuthzGrpcClientTest, Basic) { + std::unique_ptr response; + + { + envoy::api::v2::auth::CheckRequest request; + Http::HeaderMapImpl headers; + EXPECT_CALL(*async_client_, send(_, ProtoEq(request), Ref(client_), _, _)) + .WillOnce(Invoke([this]( + const Protobuf::MethodDescriptor& service_method, + const envoy::api::v2::auth::CheckRequest&, + Grpc::AsyncRequestCallbacks&, + Tracing::Span&, + const Optional&) -> Grpc::AsyncRequest* { + EXPECT_EQ("envoy.api.v2.auth.Authorization", service_method.service()->full_name()); + EXPECT_EQ("Check", service_method.name()); + return &async_request_; + })); + + client_.check(request_callbacks_, request, Tracing::NullSpan::instance()); + + client_.onCreateInitialMetadata(headers); + EXPECT_EQ(nullptr, headers.RequestId()); + + response.reset(new envoy::api::v2::auth::CheckResponse()); + ::google::rpc::Status *status = new ::google::rpc::Status(); + status->set_code(Grpc::Status::GrpcStatus::PermissionDenied); + response->set_allocated_status(status); + EXPECT_CALL(span_, setTag("ext_authz_status", "ext_authz_unauthorized")); + EXPECT_CALL(request_callbacks_, complete(CheckStatus::Denied)); + client_.onSuccess(std::move(response), span_); + } + + { + envoy::api::v2::auth::CheckRequest request; + Http::HeaderMapImpl headers; + EXPECT_CALL(*async_client_, send(_, ProtoEq(request), _, _, _)) + .WillOnce(Return(&async_request_)); + + client_.check(request_callbacks_, request, + Tracing::NullSpan::instance()); + + client_.onCreateInitialMetadata(headers); + + response.reset(new envoy::api::v2::auth::CheckResponse()); + ::google::rpc::Status *status = new ::google::rpc::Status(); + status->set_code(Grpc::Status::GrpcStatus::Ok); + response->set_allocated_status(status); + EXPECT_CALL(span_, setTag("ext_authz_status", "ext_authz_ok")); + EXPECT_CALL(request_callbacks_, complete(CheckStatus::OK)); + client_.onSuccess(std::move(response), span_); + } + + + { + envoy::api::v2::auth::CheckRequest request; + EXPECT_CALL(*async_client_, send(_, ProtoEq(request), _, _, _)) + .WillOnce(Return(&async_request_)); + + client_.check(request_callbacks_, request, + Tracing::NullSpan::instance()); + + response.reset(new envoy::api::v2::auth::CheckResponse()); + EXPECT_CALL(request_callbacks_, complete(CheckStatus::Error)); + client_.onFailure(Grpc::Status::Unknown, "", span_); + } +} + +TEST_F(ExtAuthzGrpcClientTest, Cancel) { + std::unique_ptr response; + envoy::api::v2::auth::CheckRequest request; + + EXPECT_CALL(*async_client_, send(_, _, _, _, _)).WillOnce(Return(&async_request_)); + + client_.check(request_callbacks_, request, Tracing::NullSpan::instance()); + + EXPECT_CALL(async_request_, cancel()); + client_.cancel(); +} + +TEST(ExtAuthzGrpcFactoryTest, NoCluster) { + Upstream::MockClusterManager cm; + + EXPECT_CALL(cm, get("foo")).WillOnce(Return(nullptr)); + EXPECT_THROW(GrpcFactoryImpl("foo", cm), EnvoyException); +} + +TEST(ExtAuthzGrpcFactoryTest, Create) { + Upstream::MockClusterManager cm; + + EXPECT_CALL(cm, get("foo")).Times(AtLeast(1)); + GrpcFactoryImpl factory("foo", cm); + factory.create(Optional()); +} + +TEST(ExtAuthzNullFactoryTest, Basic) { + NullFactoryImpl factory; + ClientPtr client = factory.create(Optional()); + MockRequestCallbacks request_callbacks; + envoy::api::v2::auth::CheckRequest request; + EXPECT_CALL(request_callbacks, complete(CheckStatus::OK)); + client->check(request_callbacks, request, Tracing::NullSpan::instance()); + client->cancel(); +} + +// @SM: TODO Add TEST for createHttpCheck and createTcpCheck + +} // namespace ExtAuthz +} // namespace Envoy diff --git a/test/common/filter/BUILD b/test/common/filter/BUILD index f850a54f36ab8..1eddaf79b44ab 100644 --- a/test/common/filter/BUILD +++ b/test/common/filter/BUILD @@ -46,3 +46,20 @@ 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/stats:stats_lib", + "//test/mocks/network:network_mocks", + "//test/mocks/ext_authz:ext_authz_mocks", + "//test/mocks/runtime:runtime_mocks", + "//test/mocks/upstream:upstream_mocks", + "//test/mocks/tracing:tracing_mocks", + ], +) diff --git a/test/common/filter/ext_authz_test.cc b/test/common/filter/ext_authz_test.cc new file mode 100644 index 0000000000000..98d82e4d18320 --- /dev/null +++ b/test/common/filter/ext_authz_test.cc @@ -0,0 +1,286 @@ +#include +#include +#include + +#include "common/buffer/buffer_impl.h" +#include "common/config/filter_json.h" +#include "common/filter/ext_authz.h" +#include "common/json/json_loader.h" +#include "common/stats/stats_impl.h" + +#include "test/mocks/network/mocks.h" +#include "test/mocks/ext_authz/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::WithArgs; +using testing::_; + +namespace Envoy { +namespace ExtAuthz { +namespace TcpFilter { + +class ExtAuthzFilterTest : public testing::Test { +public: + ExtAuthzFilterTest() { + std::string json = R"EOF( + { + "grpc_cluster": { "cluster_name": "ext_authz_server" }, + "failure_mode_allow": true, + "stat_prefix": "name" + } + )EOF"; + + Json::ObjectSharedPtr json_config = Json::Factory::loadFromString(json); + envoy::api::v2::filter::network::ExtAuthz proto_config{}; + Envoy::Config::FilterJson::translateTcpExtAuthzFilter(*json_config, proto_config); + config_.reset(new Config(proto_config, stats_store_, runtime_, cm_)); + client_ = new MockClient(); + filter_.reset(new Instance(config_, ClientPtr{client_})); + filter_->initializeReadFilterCallbacks(filter_callbacks_); + crg_ = new NiceMock(); + filter_->setCheckReqGen(crg_); + + // NOP currently. + filter_->onAboveWriteBufferHighWatermark(); + filter_->onBelowWriteBufferLowWatermark(); + } + + ~ExtAuthzFilterTest() { + for (const Stats::GaugeSharedPtr& gauge : stats_store_.gauges()) { + EXPECT_EQ(0U, gauge->value()); + } + } + + + Stats::IsolatedStoreImpl stats_store_; + NiceMock runtime_; + NiceMock cm_; + NiceMock* crg_; + ConfigSharedPtr config_; + MockClient* client_; + std::unique_ptr filter_; + NiceMock filter_callbacks_; + RequestCallbacks* request_callbacks_{}; +}; + +TEST_F(ExtAuthzFilterTest, BadExtAuthzConfig) { + std::string json_string = R"EOF( + { + "stat_prefix": "my_stat_prefix" + } + )EOF"; + + Json::ObjectSharedPtr json_config = Json::Factory::loadFromString(json_string); + envoy::api::v2::filter::network::ExtAuthz proto_config{}; + + EXPECT_THROW(Envoy::Config::FilterJson::translateTcpExtAuthzFilter(*json_config, proto_config), + Json::Exception); +} + +TEST_F(ExtAuthzFilterTest, OK) { + InSequence s; + + EXPECT_CALL(*crg_, createTcpCheck(_, _)); + EXPECT_CALL(filter_callbacks_.connection_, readDisable(true)); + EXPECT_CALL(*client_, check(_, _, + testing::A())) + .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_EQ(Network::FilterStatus::StopIteration, filter_->onData(data)); + + EXPECT_CALL(filter_callbacks_.connection_, readDisable(false)); + EXPECT_CALL(filter_callbacks_, continueReading()); + request_callbacks_->complete(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(1U, stats_store_.counter("ext_authz.name.ok").value()); +} + +TEST_F(ExtAuthzFilterTest, Denied) { + InSequence s; + + EXPECT_CALL(*crg_, createTcpCheck(_, _)); + EXPECT_CALL(filter_callbacks_.connection_, readDisable(true)); + 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_, readDisable(false)); + EXPECT_CALL(filter_callbacks_.connection_, close(Network::ConnectionCloseType::NoFlush)); + EXPECT_CALL(*client_, cancel()).Times(0); + request_callbacks_->complete(CheckStatus::Denied); + + 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.unauthz").value()); + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.cx_closed").value()); +} + +TEST_F(ExtAuthzFilterTest, OKWithSSLConnect) { + InSequence s; + + EXPECT_CALL(*crg_, createTcpCheck(_, _)); + EXPECT_CALL(filter_callbacks_.connection_, readDisable(true)); + EXPECT_CALL(*client_, check(_, _, + testing::A())) + .WillOnce(WithArgs<0>( + Invoke([&](RequestCallbacks& callbacks) -> void { request_callbacks_ = &callbacks; }))); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection()); + // Called by SSL when the handshake is done. + filter_callbacks_.connection_.raiseEvent(Network::ConnectionEvent::Connected); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data)); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data)); + + EXPECT_CALL(filter_callbacks_.connection_, readDisable(false)); + EXPECT_CALL(filter_callbacks_, continueReading()); + request_callbacks_->complete(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(1U, stats_store_.counter("ext_authz.name.ok").value()); +} + +TEST_F(ExtAuthzFilterTest, DeniedWithSSLConnect) { + InSequence s; + + EXPECT_CALL(*crg_, createTcpCheck(_, _)); + EXPECT_CALL(filter_callbacks_.connection_, readDisable(true)); + EXPECT_CALL(*client_, check(_, _, _)) + .WillOnce(WithArgs<0>( + Invoke([&](RequestCallbacks& callbacks) -> void { request_callbacks_ = &callbacks; }))); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection()); + // Called by SSL when the handshake is done. + filter_callbacks_.connection_.raiseEvent(Network::ConnectionEvent::Connected); + Buffer::OwnedImpl data("hello"); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data)); + + EXPECT_CALL(filter_callbacks_.connection_, readDisable(false)); + EXPECT_CALL(filter_callbacks_.connection_, close(Network::ConnectionCloseType::NoFlush)); + EXPECT_CALL(*client_, cancel()).Times(0); + request_callbacks_->complete(CheckStatus::Denied); + + 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.unauthz").value()); + EXPECT_EQ(1U, stats_store_.counter("ext_authz.name.cx_closed").value()); +} + +TEST_F(ExtAuthzFilterTest, FailOpen) { + InSequence s; + + 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_->complete(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.unauthz").value()); + EXPECT_EQ(0U, stats_store_.counter("ext_authz.name.cx_closed").value()); +} + +TEST_F(ExtAuthzFilterTest, Error) { + InSequence s; + + 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_->complete(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()); +} + +TEST_F(ExtAuthzFilterTest, Disconnect) { + InSequence s; + + 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()); +} + +TEST_F(ExtAuthzFilterTest, ImmediateOK) { + InSequence s; + + EXPECT_CALL(filter_callbacks_, continueReading()).Times(0); + EXPECT_CALL(*client_, check(_, _, _)) + .WillOnce(WithArgs<0>(Invoke( + [&](RequestCallbacks& callbacks) -> void { callbacks.complete(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(1U, stats_store_.counter("ext_authz.name.ok").value()); +} + +} // namespace TcpFilter +} // namespace ExtAuthz +} // namespace Envoy diff --git a/test/common/http/filter/BUILD b/test/common/http/filter/BUILD index 2d5dcaa896965..80d995cd52e19 100644 --- a/test/common/http/filter/BUILD +++ b/test/common/http/filter/BUILD @@ -89,3 +89,24 @@ envoy_cc_test( "//test/test_common:utility_lib", ], ) + +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/http:headers_lib", + "//source/common/http/filter:ext_authz_includes", + "//source/common/http/filter:ext_authz_lib", + "//source/common/ext_authz:ext_authz_lib", + "//test/mocks/http:http_mocks", + "//test/mocks/local_info:local_info_mocks", + "//test/mocks/ext_authz:ext_authz_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..e84d4e6d1c7fe --- /dev/null +++ b/test/common/http/filter/ext_authz_test.cc @@ -0,0 +1,240 @@ +#include +#include +#include + +#include "common/buffer/buffer_impl.h" +#include "common/common/empty_string.h" +#include "common/config/filter_json.h" +#include "common/http/filter/ext_authz.h" +#include "common/http/headers.h" + +#include "test/mocks/http/mocks.h" +#include "test/mocks/local_info/mocks.h" +#include "test/mocks/ext_authz/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::SetArgReferee; +using testing::WithArgs; +using testing::_; + +namespace Envoy { +namespace Http { +namespace ExtAuthz { + +class HttpExtAuthzFilterTest : public testing::Test { +public: + HttpExtAuthzFilterTest() {} + + void SetUpTest(const std::string json) { + Json::ObjectSharedPtr json_config = Json::Factory::loadFromString(json); + envoy::api::v2::filter::http::ExtAuthz proto_config{}; + Config::FilterJson::translateHttpExtAuthzFilter(*json_config, 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_); + crg_ = new NiceMock(); + filter_->setCheckReqGen(crg_); + } + + const std::string filter_config_ = R"EOF( + { + "grpc_cluster": { "cluster_name": "ext_authz_server" }, + "failure_mode_allow": true + } + )EOF"; + + 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_; + NiceMock* crg_; +}; + +TEST_F(HttpExtAuthzFilterTest, BadConfig) { + const std::string filter_config = R"EOF( + { + "failure_mode_allow": true + } + )EOF"; + + Json::ObjectSharedPtr json_config = Json::Factory::loadFromString(filter_config); + envoy::api::v2::filter::http::ExtAuthz proto_config{}; + EXPECT_THROW(Config::FilterJson::translateHttpExtAuthzFilter(*json_config, proto_config), + Json::Exception); +} + +TEST_F(HttpExtAuthzFilterTest, NoRoute) { + SetUpTest(filter_config_); + + 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_F(HttpExtAuthzFilterTest, NoCluster) { + SetUpTest(filter_config_); + + 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_F(HttpExtAuthzFilterTest, OkResponse) { + SetUpTest(filter_config_); + InSequence s; + + EXPECT_CALL(*crg_, createHttpCheck(_, _, _)); + 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_->complete(Envoy::ExtAuthz::CheckStatus::OK); + + EXPECT_EQ(1U, + cm_.thread_local_cluster_.cluster_.info_->stats_store_.counter("ext_authz.ok").value()); +} + +TEST_F(HttpExtAuthzFilterTest, ImmediateOkResponse) { + SetUpTest(filter_config_); + InSequence s; + + EXPECT_CALL(*client_, check(_, _, _)) + .WillOnce(WithArgs<0>(Invoke([&](Envoy::ExtAuthz::RequestCallbacks& callbacks) -> void { + callbacks.complete(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_F(HttpExtAuthzFilterTest, DeniedResponse) { + SetUpTest(filter_config_); + InSequence s; + + EXPECT_CALL(*crg_, createHttpCheck(_, _, _)); + 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_->complete(Envoy::ExtAuthz::CheckStatus::Denied); + + EXPECT_EQ(1U, + cm_.thread_local_cluster_.cluster_.info_->stats_store_.counter("ext_authz.unauthz") + .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_F(HttpExtAuthzFilterTest, ErrorFailClose) { + const std::string fail_close_config = R"EOF( + { + "grpc_cluster": { "cluster_name": "ext_authz_server" }, + "failure_mode_allow": false + } + )EOF"; + SetUpTest(fail_close_config); + InSequence s; + + 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_->complete(Envoy::ExtAuthz::CheckStatus::Error); + + + EXPECT_EQ( + 1U, + cm_.thread_local_cluster_.cluster_.info_->stats_store_.counter("ext_authz.error").value()); +} + +TEST_F(HttpExtAuthzFilterTest, ErrorOpen) { + SetUpTest(filter_config_); + InSequence s; + + 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_->complete(Envoy::ExtAuthz::CheckStatus::Error); + + + EXPECT_EQ( + 1U, + cm_.thread_local_cluster_.cluster_.info_->stats_store_.counter("ext_authz.error").value()); +} + +TEST_F(HttpExtAuthzFilterTest, ResetDuringCall) { + SetUpTest(filter_config_); + InSequence s; + + 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(); +} + +} // namespace ExtAuthz +} // namespace Http +} // namespace Envoy diff --git a/test/mocks/ext_authz/BUILD b/test/mocks/ext_authz/BUILD new file mode 100644 index 0000000000000..0a1c65bd37e2d --- /dev/null +++ b/test/mocks/ext_authz/BUILD @@ -0,0 +1,18 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_mock", + "envoy_package", +) + +envoy_package() + +envoy_cc_mock( + name = "ext_authz_mocks", + srcs = ["mocks.cc"], + hdrs = ["mocks.h"], + deps = [ + "//include/envoy/ext_authz:ext_authz_interface" + ], +) diff --git a/test/mocks/ext_authz/mocks.cc b/test/mocks/ext_authz/mocks.cc new file mode 100644 index 0000000000000..6365eb1b90dac --- /dev/null +++ b/test/mocks/ext_authz/mocks.cc @@ -0,0 +1,13 @@ +#include "mocks.h" + +namespace Envoy { +namespace ExtAuthz { + +MockClient::MockClient() {} +MockClient::~MockClient() {} + +MockCheckRequestGen::MockCheckRequestGen() {} +MockCheckRequestGen::~MockCheckRequestGen() {} + +} // namespace ExtAuthz +} // namespace Envoy diff --git a/test/mocks/ext_authz/mocks.h b/test/mocks/ext_authz/mocks.h new file mode 100644 index 0000000000000..6199498d966b0 --- /dev/null +++ b/test/mocks/ext_authz/mocks.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include "envoy/ext_authz/ext_authz.h" + +#include "gmock/gmock.h" + +namespace Envoy { +namespace ExtAuthz { + +class MockClient : public Client { +public: + MockClient(); + ~MockClient(); + + // ExtAuthz::Client + MOCK_METHOD0(cancel, void()); + MOCK_METHOD3(check, void(RequestCallbacks& callbacks, const CheckRequest& request, + Tracing::Span& parent_span)); +}; + +class MockCheckRequestGen : public CheckRequestGenIntf { +public: + MockCheckRequestGen(); + ~MockCheckRequestGen(); + + // ExtAuthz::CheckRequestGen + MOCK_METHOD3(createHttpCheck, void(const Envoy::Http::StreamDecoderFilterCallbacks* callbacks, const Envoy::Http::HeaderMap &headers, + envoy::api::v2::auth::CheckRequest& request)); + MOCK_METHOD2(createTcpCheck, void(const Network::ReadFilterCallbacks* callbacks, + envoy::api::v2::auth::CheckRequest& request)); +}; + +} // namespace ExtAuthz +} // namespace Envoy