diff --git a/include/envoy/ext_authz/BUILD b/include/envoy/ext_authz/BUILD new file mode 100644 index 0000000000000..34318465f0e2a --- /dev/null +++ b/include/envoy/ext_authz/BUILD @@ -0,0 +1,18 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_library( + name = "ext_authz_interface", + hdrs = ["ext_authz.h"], + deps = [ + "//include/envoy/tracing:http_tracer_interface", + "@envoy_api//envoy/service/auth/v2:external_auth_cc", + ], +) diff --git a/include/envoy/ext_authz/ext_authz.h b/include/envoy/ext_authz/ext_authz.h new file mode 100644 index 0000000000000..fd11a284ca85a --- /dev/null +++ b/include/envoy/ext_authz/ext_authz.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/common/pure.h" +#include "envoy/service/auth/v2/external_auth.pb.h" +#include "envoy/tracing/http_tracer.h" + +namespace Envoy { +namespace ExtAuthz { + +/** + * 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 onComplete(CheckStatus status) PURE; +}; + +class Client { +public: + // Destructor + virtual ~Client() {} + + /** + * Cancel an inflight Check request. + */ + virtual void cancel() PURE; + + /** + * Request a check call to an external authorization service which can use the + * passed request parameters to make a permit/deny decision. + * @param callback supplies the completion callbacks. + * NOTE: The callback may happen within the calling stack. + * @param request is the proto message with the attributes of the specific payload. + * @param parent_span source for generating an egress child span as part of the trace. + * + */ + virtual void check(RequestCallbacks& callback, + const envoy::service::auth::v2::CheckRequest& request, + Tracing::Span& parent_span) PURE; +}; + +typedef std::unique_ptr ClientPtr; + +} // namespace ExtAuthz +} // namespace Envoy diff --git a/include/envoy/request_info/request_info.h b/include/envoy/request_info/request_info.h index f4750f25f9196..f584e3a6c6229 100644 --- a/include/envoy/request_info/request_info.h +++ b/include/envoy/request_info/request_info.h @@ -43,8 +43,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 b7ff316df0da9..c39bca1adc526 100644 --- a/source/common/access_log/grpc_access_log_impl.cc +++ b/source/common/access_log/grpc_access_log_impl.cc @@ -69,7 +69,7 @@ void HttpGrpcAccessLog::responseFlagsToAccessLogResponseFlags( envoy::config::filter::accesslog::v2::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)) { diff --git a/source/common/config/well_known_names.h b/source/common/config/well_known_names.h index e985e17f9f04f..62030d0e35360 100644 --- a/source/common/config/well_known_names.h +++ b/source/common/config/well_known_names.h @@ -78,13 +78,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; @@ -131,13 +133,16 @@ class HttpFilterNameValues { const std::string LUA = "envoy.lua"; // Squash filter const std::string SQUASH = "envoy.squash"; + // 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..92c148e17a900 --- /dev/null +++ b/source/common/ext_authz/BUILD @@ -0,0 +1,35 @@ +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"], + deps = [ + "//include/envoy/ext_authz:ext_authz_interface", + "//include/envoy/grpc:async_client_interface", + "//include/envoy/grpc:async_client_manager_interface", + "//include/envoy/http:filter_interface", + "//include/envoy/http:header_map_interface", + "//include/envoy/http:protocol_interface", + "//include/envoy/network:address_interface", + "//include/envoy/network:connection_interface", + "//include/envoy/network:filter_interface", + "//include/envoy/ssl:connection_interface", + "//include/envoy/upstream:cluster_manager_interface", + "//source/common/common:assert_lib", + "//source/common/grpc:async_client_lib", + "//source/common/http:headers_lib", + "//source/common/http:utility_lib", + "//source/common/network:utility_lib", + "//source/common/protobuf", + "//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..d2b2a2eef3cc6 --- /dev/null +++ b/source/common/ext_authz/ext_authz_impl.cc @@ -0,0 +1,189 @@ +#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 "common/http/utility.h" +#include "common/network/utility.h" +#include "common/protobuf/protobuf.h" + +#include "fmt/format.h" + +namespace Envoy { +namespace ExtAuthz { + +GrpcClientImpl::GrpcClientImpl(Grpc::AsyncClientPtr&& async_client, + const Optional& timeout) + : service_method_(*Protobuf::DescriptorPool::generated_pool()->FindMethodByName( + "envoy.service.auth.v2.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::service::auth::v2::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_->onComplete(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_->onComplete(CheckStatus::Error); + callbacks_ = nullptr; +} + +void CreateCheckRequest::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(); + if (local) { + Envoy::Network::Utility::addressToProtobufAddress(*connection.localAddress(), *addr); + } else { + Envoy::Network::Utility::addressToProtobufAddress(*connection.remoteAddress(), *addr); + } + + // Set the principal + // Preferably the SAN from the peer's cert or + // Subject from the peer's cert. + Ssl::Connection* ssl = const_cast(connection.ssl()); + if (ssl != nullptr) { + if (local) { + peer.set_principal(ssl->uriSanLocalCertificate()); + + if (peer.principal().empty()) { + peer.set_principal(ssl->subjectLocalCertificate()); + } + } else { + peer.set_principal(ssl->uriSanPeerCertificate()); + + if (peer.principal().empty()) { + peer.set_principal(ssl->subjectPeerCertificate()); + } + } + } + + if (!service.empty()) { + peer.set_service(service); + } +} + +std::string CreateCheckRequest::getHeaderStr(const Envoy::Http::HeaderEntry* entry) { + if (entry) { + return entry->value().getString(); + } + return ""; +} + +void CreateCheckRequest::setHttpRequest( + ::envoy::service::auth::v2::AttributeContext_HttpRequest& httpreq, + const Envoy::Http::StreamDecoderFilterCallbacks* callbacks, + const Envoy::Http::HeaderMap& headers) { + + // 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())); + + // Set method + httpreq.set_method(getHeaderStr(headers.Method())); + // Set path + httpreq.set_path(getHeaderStr(headers.Path())); + // Set host + httpreq.set_host(getHeaderStr(headers.Host())); + // Set scheme + httpreq.set_scheme(getHeaderStr(headers.Scheme())); + + // Set size + // need to convert to google buffer 64t; + httpreq.set_size(sdfc->requestInfo().bytesReceived()); + + // Set protocol + if (sdfc->requestInfo().protocol().valid()) { + httpreq.set_protocol( + Envoy::Http::Utility::getProtocolString(sdfc->requestInfo().protocol().value())); + } + + // Fill in the headers + auto mutable_headers = httpreq.mutable_headers(); + headers.iterate( + [](const Envoy::Http::HeaderEntry& e, void* ctx) { + Envoy::Protobuf::Map<::std::string, ::std::string>* mutable_headers = + static_cast*>(ctx); + (*mutable_headers)[e.key().getString()] = e.value().getString(); + return Envoy::Http::HeaderMap::Iterate::Continue; + }, + mutable_headers); +} + +void CreateCheckRequest::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) { + + auto attrs = request.mutable_attributes(); + + Envoy::Http::StreamDecoderFilterCallbacks* cb = + const_cast(callbacks); + + const std::string service = getHeaderStr(headers.EnvoyDownstreamServiceCluster()); + + setAttrContextPeer(*attrs->mutable_source(), *cb->connection(), service, false); + setAttrContextPeer(*attrs->mutable_destination(), *cb->connection(), "", true); + setAttrContextRequest(*attrs->mutable_request(), callbacks, headers); +} + +void CreateCheckRequest::createTcpCheck(const Network::ReadFilterCallbacks* callbacks, + envoy::service::auth::v2::CheckRequest& request) { + + auto attrs = request.mutable_attributes(); + + Network::ReadFilterCallbacks* cb = const_cast(callbacks); + setAttrContextPeer(*attrs->mutable_source(), cb->connection(), "", false); + setAttrContextPeer(*attrs->mutable_destination(), 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..862e0e61933f8 --- /dev/null +++ b/source/common/ext_authz/ext_authz_impl.h @@ -0,0 +1,112 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/ext_authz/ext_authz.h" +#include "envoy/grpc/async_client.h" +#include "envoy/grpc/async_client_manager.h" +#include "envoy/http/filter.h" +#include "envoy/http/header_map.h" +#include "envoy/http/protocol.h" +#include "envoy/network/address.h" +#include "envoy/network/connection.h" +#include "envoy/network/filter.h" +#include "envoy/tracing/http_tracer.h" +#include "envoy/upstream/cluster_manager.h" + +#include "common/singleton/const_singleton.h" + +namespace Envoy { +namespace ExtAuthz { + +typedef Grpc::TypedAsyncRequestCallbacks + 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; + +// NOTE: We create gRPC client for each filter stack instead of a client per thread. +// That is ok since this is unary RPC and the cost of doing this is minimal. +class GrpcClientImpl : public Client, public ExtAuthzAsyncCallbacks { +public: + GrpcClientImpl(Grpc::AsyncClientPtr&& async_client, + const Optional& timeout); + ~GrpcClientImpl(); + + // ExtAuthz::Client + void cancel() override; + void check(RequestCallbacks& callbacks, const envoy::service::auth::v2::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_; + Grpc::AsyncClientPtr async_client_; + Grpc::AsyncRequest* request_{}; + Optional timeout_; + RequestCallbacks* callbacks_{}; +}; + +/** + * For creating ext_authz.proto (authorization) request. + * CreateCheckRequest 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 { +public: + /** + * createHttpCheck is used to extract the attributes from the stream and the http headers + * and fill them up in the CheckRequest proto message. + * @param callbacks supplies the Http stream context from which data can be extracted. + * @param headers supplies the header map with http headers that will be used to create the + * check request. + * @param request is the reference to the check request that will be filled up. + * + */ + static void createHttpCheck(const Envoy::Http::StreamDecoderFilterCallbacks* callbacks, + const Envoy::Http::HeaderMap& headers, + envoy::service::auth::v2::CheckRequest& request); + + /** + * createTcpCheck is used to extract the attributes from the network layer and fill them up + * in the CheckRequest proto message. + * @param callbacks supplies the network layer context from which data can be extracted. + * @param request is the reference to the check request that will be filled up. + * + */ + static void createTcpCheck(const Network::ReadFilterCallbacks* callbacks, + envoy::service::auth::v2::CheckRequest& request); + +private: + static void setAttrContextPeer(envoy::service::auth::v2::AttributeContext_Peer& peer, + const Network::Connection& connection, const std::string& service, + const bool local); + static void setHttpRequest(::envoy::service::auth::v2::AttributeContext_HttpRequest& httpreq, + const Envoy::Http::StreamDecoderFilterCallbacks* callbacks, + const Envoy::Http::HeaderMap& headers); + static void setAttrContextRequest(::envoy::service::auth::v2::AttributeContext_Request& req, + const Envoy::Http::StreamDecoderFilterCallbacks* callbacks, + const Envoy::Http::HeaderMap& headers); + static std::string getHeaderStr(const Envoy::Http::HeaderEntry* entry); + static Envoy::Http::HeaderMap::Iterate fillHttpHeaders(const Envoy::Http::HeaderEntry&, void*); +}; + +} // namespace ExtAuthz +} // namespace Envoy 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/server/BUILD b/source/server/BUILD index c83baeea051b5..6c830ae70c3cc 100644 --- a/source/server/BUILD +++ b/source/server/BUILD @@ -36,6 +36,7 @@ envoy_cc_library( "//source/common/common:utility_lib", "//source/common/config:lds_json_lib", "//source/common/config:utility_lib", + "//source/common/ext_authz:ext_authz_lib", "//source/common/network:resolver_lib", "//source/common/network:utility_lib", "//source/common/protobuf:utility_lib", diff --git a/test/common/ext_authz/BUILD b/test/common/ext_authz/BUILD new file mode 100644 index 0000000000000..a0cfd0d21f2ff --- /dev/null +++ b/test/common/ext_authz/BUILD @@ -0,0 +1,27 @@ +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/ext_authz:ext_authz_lib", + "//source/common/http:header_map_lib", + "//source/common/http:headers_lib", + "//source/common/network:address_lib", + "//test/mocks/grpc:grpc_mocks", + "//test/mocks/http:http_mocks", + "//test/mocks/network:network_mocks", + "//test/mocks/request_info:request_info_mocks", + "//test/mocks/ssl:ssl_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..77a171c68e7f5 --- /dev/null +++ b/test/common/ext_authz/ext_authz_impl_test.cc @@ -0,0 +1,185 @@ +#include +#include +#include + +#include "common/ext_authz/ext_authz_impl.h" +#include "common/http/header_map_impl.h" +#include "common/http/headers.h" +#include "common/network/address_impl.h" + +#include "test/mocks/grpc/mocks.h" +#include "test/mocks/http/mocks.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/request_info/mocks.h" +#include "test/mocks/ssl/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::ReturnRef; +using testing::WithArg; +using testing::_; + +namespace Envoy { +namespace ExtAuthz { + +class MockRequestCallbacks : public RequestCallbacks { +public: + MOCK_METHOD1(onComplete, void(CheckStatus status)); +}; + +class ExtAuthzGrpcClientTest : public testing::Test { +public: + ExtAuthzGrpcClientTest() + : async_client_(new Grpc::MockAsyncClient()), + client_(Grpc::AsyncClientPtr{async_client_}, Optional()) {} + + Grpc::MockAsyncClient* async_client_; + Grpc::MockAsyncRequest async_request_; + GrpcClientImpl client_; + MockRequestCallbacks request_callbacks_; + Tracing::MockSpan span_; +}; + +TEST_F(ExtAuthzGrpcClientTest, BasicOK) { + envoy::service::auth::v2::CheckRequest request; + std::unique_ptr response; + 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 = std::make_unique(); + auto status = response->mutable_status(); + status->set_code(Grpc::Status::GrpcStatus::Ok); + EXPECT_CALL(span_, setTag("ext_authz_status", "ext_authz_ok")); + EXPECT_CALL(request_callbacks_, onComplete(CheckStatus::OK)); + client_.onSuccess(std::move(response), span_); +} + +TEST_F(ExtAuthzGrpcClientTest, BasicDenied) { + envoy::service::auth::v2::CheckRequest request; + std::unique_ptr response; + Http::HeaderMapImpl headers; + EXPECT_CALL(*async_client_, send(_, ProtoEq(request), Ref(client_), _, _)) + .WillOnce( + Invoke([this](const Protobuf::MethodDescriptor& service_method, const Protobuf::Message&, + Grpc::AsyncRequestCallbacks&, Tracing::Span&, + const Optional&) -> Grpc::AsyncRequest* { + EXPECT_EQ("envoy.service.auth.v2.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 = std::make_unique(); + auto status = response->mutable_status(); + status->set_code(Grpc::Status::GrpcStatus::PermissionDenied); + EXPECT_CALL(span_, setTag("ext_authz_status", "ext_authz_unauthorized")); + EXPECT_CALL(request_callbacks_, onComplete(CheckStatus::Denied)); + client_.onSuccess(std::move(response), span_); +} + +TEST_F(ExtAuthzGrpcClientTest, BasicError) { + envoy::service::auth::v2::CheckRequest request; + EXPECT_CALL(*async_client_, send(_, ProtoEq(request), _, _, _)).WillOnce(Return(&async_request_)); + + client_.check(request_callbacks_, request, Tracing::NullSpan::instance()); + + EXPECT_CALL(request_callbacks_, onComplete(CheckStatus::Error)); + client_.onFailure(Grpc::Status::Unknown, "", span_); +} + +TEST_F(ExtAuthzGrpcClientTest, Cancel) { + envoy::service::auth::v2::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(); +} + +class CreateCheckRequestTest : public testing::Test { +public: + CreateCheckRequestTest() { + addr_ = std::make_shared("1.2.3.4", 1111); + protocol_ = Envoy::Http::Protocol::Http10; + }; + + Network::Address::InstanceConstSharedPtr addr_; + Optional protocol_; + CreateCheckRequest check_request_generator_; + NiceMock callbacks_; + NiceMock net_callbacks_; + NiceMock connection_; + NiceMock ssl_; + NiceMock req_info_; +}; + +TEST_F(CreateCheckRequestTest, BasicTcp) { + + envoy::service::auth::v2::CheckRequest request; + + EXPECT_CALL(net_callbacks_, connection()).Times(2).WillRepeatedly(ReturnRef(connection_)); + EXPECT_CALL(connection_, remoteAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(connection_, localAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(Const(connection_), ssl()).Times(2).WillRepeatedly(Return(&ssl_)); + + CreateCheckRequest::createTcpCheck(&net_callbacks_, request); +} + +TEST_F(CreateCheckRequestTest, BasicHttp) { + + Http::HeaderMapImpl headers; + envoy::service::auth::v2::CheckRequest request; + + EXPECT_CALL(callbacks_, connection()).Times(2).WillRepeatedly(Return(&connection_)); + EXPECT_CALL(connection_, remoteAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(connection_, localAddress()).WillOnce(ReturnRef(addr_)); + EXPECT_CALL(Const(connection_), ssl()).Times(2).WillRepeatedly(Return(&ssl_)); + 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); +} + +TEST_F(CreateCheckRequestTest, CheckAttrContextPeer) { + + Http::TestHeaderMapImpl request_headers{{"x-envoy-downstream-service-cluster", "foo"}, + {":path", "/bar"}}; + envoy::service::auth::v2::CheckRequest request; + + EXPECT_CALL(callbacks_, connection()).WillRepeatedly(Return(&connection_)); + EXPECT_CALL(connection_, remoteAddress()).WillRepeatedly(ReturnRef(addr_)); + EXPECT_CALL(connection_, localAddress()).WillRepeatedly(ReturnRef(addr_)); + EXPECT_CALL(Const(connection_), ssl()).WillRepeatedly(Return(&ssl_)); + EXPECT_CALL(callbacks_, streamId()).WillRepeatedly(Return(0)); + EXPECT_CALL(callbacks_, requestInfo()).WillRepeatedly(ReturnRef(req_info_)); + EXPECT_CALL(req_info_, protocol()).WillRepeatedly(ReturnRef(protocol_)); + + EXPECT_CALL(ssl_, uriSanPeerCertificate()).WillOnce(Return("source")); + EXPECT_CALL(ssl_, uriSanLocalCertificate()).WillOnce(Return("destination")); + CreateCheckRequest::createHttpCheck(&callbacks_, request_headers, request); + + EXPECT_EQ("source", request.attributes().source().principal()); + EXPECT_EQ("destination", request.attributes().destination().principal()); + EXPECT_EQ("foo", request.attributes().source().service()); +} + +} // namespace ExtAuthz +} // namespace Envoy diff --git a/test/common/request_info/utility_test.cc b/test/common/request_info/utility_test.cc index 583d26fe2a336..34191b7226690 100644 --- a/test/common/request_info/utility_test.cc +++ b/test/common/request_info/utility_test.cc @@ -14,7 +14,7 @@ namespace Envoy { namespace RequestInfo { TEST(ResponseFlagUtilsTest, toShortStringConversion) { - 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."); std::vector> expected = { std::make_pair(ResponseFlag::FailedLocalHealthCheck, "LH"), @@ -28,7 +28,9 @@ TEST(ResponseFlagUtilsTest, toShortStringConversion) { std::make_pair(ResponseFlag::NoRouteFound, "NR"), std::make_pair(ResponseFlag::DelayInjected, "DI"), std::make_pair(ResponseFlag::FaultInjected, "FI"), - std::make_pair(ResponseFlag::RateLimited, "RL")}; + std::make_pair(ResponseFlag::RateLimited, "RL"), + std::make_pair(ResponseFlag::Unauthorized, "UA"), + }; for (const auto& test_case : expected) { NiceMock request_info; diff --git a/test/mocks/ext_authz/BUILD b/test/mocks/ext_authz/BUILD new file mode 100644 index 0000000000000..725c43b303014 --- /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..97a2cc6cb5493 --- /dev/null +++ b/test/mocks/ext_authz/mocks.cc @@ -0,0 +1,10 @@ +#include "mocks.h" + +namespace Envoy { +namespace ExtAuthz { + +MockClient::MockClient() {} +MockClient::~MockClient() {} + +} // 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..057e9fc9263e4 --- /dev/null +++ b/test/mocks/ext_authz/mocks.h @@ -0,0 +1,26 @@ +#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 envoy::service::auth::v2::CheckRequest& request, + Tracing::Span& parent_span)); +}; + +} // namespace ExtAuthz +} // namespace Envoy