diff --git a/source/common/http/conn_manager_impl.cc b/source/common/http/conn_manager_impl.cc index c1f95fe251b9..c88b9f110be6 100644 --- a/source/common/http/conn_manager_impl.cc +++ b/source/common/http/conn_manager_impl.cc @@ -642,6 +642,16 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(ActiveStreamDecoderFilte state_.filter_call_state_ &= ~FilterCallState::DecodeHeaders; ENVOY_STREAM_LOG(trace, "decode headers called: filter={} status={}", *this, static_cast((*entry).get()), static_cast(status)); +#ifndef NVLOG + headers.iterate( + [](const HeaderEntry& header, void* context) -> HeaderMap::Iterate { + ENVOY_STREAM_LOG(trace, " H'{}':'{}'", *static_cast(context), + header.key().c_str(), header.value().c_str()); + return HeaderMap::Iterate::Continue; + }, + this); +#endif + if (!(*entry)->commonHandleAfterHeadersCallback(status) && std::next(entry) != decoder_filters_.end()) { // Stop iteration IFF this is not the last filter. If it is the last filter, continue with @@ -1254,6 +1264,8 @@ Router::RouteConstSharedPtr ConnectionManagerImpl::ActiveStreamFilterBase::route } void ConnectionManagerImpl::ActiveStreamFilterBase::clearRouteCache() { + ENVOY_STREAM_LOG(trace, "clearing route cache: filter={}", parent_, + static_cast(this)); parent_.cached_route_ = Optional(); } diff --git a/source/common/http/filter/BUILD b/source/common/http/filter/BUILD index 6cd4fc2ae20b..7dc9ba29c7eb 100644 --- a/source/common/http/filter/BUILD +++ b/source/common/http/filter/BUILD @@ -44,6 +44,22 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "extauth_lib", + srcs = ["extauth.cc"], + hdrs = ["extauth.h"], + deps = [ + "//include/envoy/buffer:buffer_interface", + "//include/envoy/http:filter_interface", + "//include/envoy/upstream:cluster_manager_interface", + "//source/common/common:assert_lib", + "//source/common/common:enum_to_int", + "//source/common/common:logger_lib", + "//source/common/http:message_lib", + "//source/common/http:utility_lib", + ], +) + envoy_cc_library( name = "fault_filter_lib", srcs = ["fault_filter.cc"], diff --git a/source/common/http/filter/extauth.cc b/source/common/http/filter/extauth.cc new file mode 100644 index 000000000000..ac2eaadfae9d --- /dev/null +++ b/source/common/http/filter/extauth.cc @@ -0,0 +1,288 @@ +#include "common/http/filter/extauth.h" + +#include "common/common/assert.h" +#include "common/common/enum_to_int.h" +#include "common/http/message_impl.h" +#include "common/http/utility.h" + +#include "absl/strings/str_cat.h" + +namespace Envoy { +namespace Http { + +namespace { + +const LowerCaseString header_to_add() { + CONSTRUCT_ON_FIRST_USE(LowerCaseString, "x-ambassador-calltype"); +} + +const std::string value_to_add() { CONSTRUCT_ON_FIRST_USE(std::string, "extauth-request"); } + +} // namespace + +ExtAuth::ExtAuth(ExtAuthConfigConstSharedPtr config) : config_(std::move(config)) {} + +ExtAuth::~ExtAuth() { ASSERT(!auth_request_); } + +void ExtAuth::dumpHeaders(const char* what, HeaderMap* headers) { +#ifndef NVLOG + ENVOY_STREAM_LOG(trace, "ExtAuth headers ({}):", *callbacks_, what); + if (headers) { + headers->iterate( + [](const HeaderEntry& header, void* context) -> HeaderMap::Iterate { + ENVOY_STREAM_LOG(trace, " '{}':'{}'", + *static_cast(context), + header.key().c_str(), header.value().c_str()); + return HeaderMap::Iterate::Continue; + }, + static_cast(callbacks_)); + } +#endif +} + +// TODO(gsagula): at the end of this PR, most of the comments inside member functions should be +// removed. +FilterHeadersStatus ExtAuth::decodeHeaders(HeaderMap& headers, bool) { + + // decodeHeaders is called at the point that the HTTP machinery handling + // the request has parsed the HTTP headers for this request. + // Our primary job here is to construct the request to the auth service + // and start it executing, but we also have to be sure to save a pointer + // to the incoming request headers in case we need to modify them in + // flight. + + // Remember that we have _not_ finished talking to the auth service... + + auth_complete_ = false; + + // ...and hang onto a pointer to the original request headers. + // + // Note that using a reference here won't work. Move semantics are the + // root of this issue, I think. + + request_headers_ = &headers; + + // Debugging. + dumpHeaders("decodeHeaders", request_headers_); + + // OK, time to get the auth-service request set up. Create a + // RequestMessageImpl to hold all the details, and start it off as a + // copy of the incoming request's headers. + + MessagePtr request_message{new RequestMessageImpl{HeaderMapPtr{new HeaderMapImpl{headers}}}}; + + // We do need to tweak a couple of things. To start with, has a change + // to the path we hand to the auth service been configured? + + if (!config_->path_prefix_.empty()) { + // Yes, it has. Go ahead and prepend it to the request_message path. + std::string path; + absl::StrAppend(&path, config_->path_prefix_, + request_message->headers().insertPath().value().getString()); + request_message->headers().insertPath().value(path); + } + + // https://github.com/datawire/ambassador/issues/154 + // We used to reset the Host: header to match the cluster name we're about + // to send the auth request to. That, however, causes trouble for anyone + // who wants to make auth decisions based on the host to which the client + // started out trying to talk to. + // + // We may need to make this configurable later, so I'm leaving this line + // in for reference. + // request_message->headers().insertHost().value(config_->cluster_); + + // After setting up whatever headers we need to, make sure the body is + // correctly marked as empty. + + request_message->headers().insertContentLength().value(uint64_t(0)); + + // Finally, we mark the request as being an Ambassador auth request. + + request_message->headers().addReference(header_to_add(), value_to_add()); + + // Fire the request up. When it's finished, we'll get a call to + // either onSuccess() or onFailure(). + + ENVOY_STREAM_LOG(trace, "ExtAuth contacting auth server", *callbacks_); + + auth_request_ = config_->cm_.httpAsyncClientForCluster(config_->cluster_) + .send(std::move(request_message), *this, + Optional(config_->timeout_)); + + // It'll take some time for our auth call to complete. Stop + // filtering while we wait for it. + + return FilterHeadersStatus::StopIteration; +} + +FilterDataStatus ExtAuth::decodeData(Buffer::Instance&, bool) { + // decodeHeaders is called at the point that the HTTP machinery handling + // the request has parsed the HTTP body for this request. We don't need + // to do anything special here; we just need to make sure that we don't + // let things proceed until our auth call is done. + if (auth_complete_) { + return FilterDataStatus::Continue; + } + return FilterDataStatus::StopIterationAndBuffer; +} + +FilterTrailersStatus ExtAuth::decodeTrailers(HeaderMap&) { + // decodeTrailers is called at the point that the HTTP machinery handling + // the request has parsed the HTTP trailers for this request. We don't need + // to do anything special here; we just need to make sure that we don't + // let things proceed until our auth call is done. + if (auth_complete_) { + return FilterTrailersStatus::Continue; + } + return FilterTrailersStatus::StopIteration; +} + +ExtAuthStats ExtAuth::generateStats(const std::string& prefix, Stats::Scope& scope) { + std::string final_prefix = prefix + "extauth."; + return {ALL_EXTAUTH_STATS(POOL_COUNTER_PREFIX(scope, final_prefix))}; +} + +void ExtAuth::onSuccess(Http::MessagePtr&& response) { + + // onSuccess is called when our asynch auth request succeeds, meaning + // "the HTTP protocol was successfully followed to completion" -- it + // could still be the case that the auth server gave us a failure + // response. + + // We're done with our auth request, so make sure it gets shredded. + auth_request_ = nullptr; + + dumpHeaders("onSuccess", request_headers_); + + // What did we get back from the auth server? + + uint64_t response_code = Http::Utility::getResponseStatus(response->headers()); + std::string response_body = response->bodyAsString(); + + ENVOY_STREAM_LOG(trace, "ExtAuth Auth responded with code {}", *callbacks_, response_code); + + if (!response->body()->length()) { + ENVOY_STREAM_LOG(trace, "ExtAuth Auth said: {}", *callbacks_, response->bodyAsString()); + } + + // By definition, any response code other than 200, "OK", means we deny + // this request. + + if (response_code != enumToInt(Http::Code::OK)) { + ENVOY_STREAM_LOG(debug, "ExtAuth rejecting request", *callbacks_); + + // Bump the rejection count... + + config_->stats_.rq_rejected_.inc(); + + // ...and ditch our pointer to the request headers. + + request_headers_ = nullptr; + + // Whatever the auth server replied, we're going to hand that back to the + // original requestor. That means both the header and the body; start by + // copying the headers... + + Http::HeaderMapPtr response_headers{new HeaderMapImpl(response->headers())}; + + callbacks_->encodeHeaders(std::move(response_headers), response_body.empty()); + + // ...and then copy the body, as well, if there is one. + + if (!response_body.empty()) { + Buffer::OwnedImpl buffer(response_body); + callbacks_->encodeData(buffer, true); + } + + // ...ahhhhnd we're done. + + return; + } + + ENVOY_STREAM_LOG(debug, "ExtAuth accepting request", *callbacks_); + + // OK, we're going to approve this request, great! Next up: the filter can + // be configured to copy headers from the auth server to the requester. + // If that's configured, we need to take care of that now -- and if we actually + // copy any headers, we'll need to be sure to invalidate the route cache. + // (If we don't copy any headers, we should leave the route cache alone.) + + bool addedHeaders = false; + + // Do we have any headers configured to copy? + + if (!config_->allowed_headers_.empty()) { + // Yup. Let's see if any of them are present. + + for (const std::string& allowed_header : config_->allowed_headers_) { + LowerCaseString key(allowed_header); + + // OK, do we have this header? + + const HeaderEntry* hdr = response->headers().get(key); + + if (hdr) { + // Well, the header exists at all. Does it have a value? + + const HeaderString& value = hdr->value(); + + if (!value.empty()) { + // Not empty! Copy it into our request_headers_. + + std::string valstr{value.c_str()}; + + ENVOY_STREAM_LOG(trace, "ExtAuth allowing response header {}: {}", *callbacks_, + allowed_header, valstr); + addedHeaders = true; + request_headers_->addCopy(key, valstr); + } + } + } + } + + if (addedHeaders) { + // Yup, we added headers. Invalidate the route cache in case any of + // the headers will affect routing decisions. + + dumpHeaders("ExtAuth invalidating route cache", request_headers_); + + callbacks_->clearRouteCache(); + } + + // Finally done. Bump the "passed" stat... + config_->stats_.rq_passed_.inc(); + + // ...remember that auth is done... + auth_complete_ = true; + + // ...clear our request-header pointer now that we're finished with + // this request... + request_headers_ = nullptr; + + // ...and allow everything to continue. + callbacks_->continueDecoding(); +} + +void ExtAuth::onFailure(Http::AsyncClient::FailureReason) { + auth_request_ = nullptr; + request_headers_ = nullptr; + ENVOY_STREAM_LOG(warn, "ExtAuth Auth request failed", *callbacks_); + config_->stats_.rq_failed_.inc(); + Http::Utility::sendLocalReply(*callbacks_, false, Http::Code::ServiceUnavailable, + std::string("Auth request failed.")); +} + +void ExtAuth::onDestroy() { + if (auth_request_) { + auth_request_->cancel(); + auth_request_ = nullptr; + } +} + +void ExtAuth::setDecoderFilterCallbacks(StreamDecoderFilterCallbacks& callbacks) { + callbacks_ = &callbacks; +} + +} // namespace Http +} // namespace Envoy diff --git a/source/common/http/filter/extauth.h b/source/common/http/filter/extauth.h new file mode 100644 index 000000000000..b4c6e82e34f2 --- /dev/null +++ b/source/common/http/filter/extauth.h @@ -0,0 +1,100 @@ +#pragma once + +#include "envoy/http/filter.h" +#include "envoy/upstream/cluster_manager.h" + +#include "common/common/logger.h" + +namespace Envoy { +namespace Http { + +/** + * A pass-through filter that talks to an external authn/authz service. + * + * When Envoy receives a request for which this filter is enabled, an + * asynchronous request with the same HTTP method and headers, but an empty + * body, is made to the configured external auth service. The original + * request is stalled until the auth request completes. + * + * If the auth request returns HTTP 200, the original request is allowed + * to continue. If any headers are listed in the extauth filter's "headers" + * array, those headers will be copied from the auth response into the + * original request (overwriting any duplicate headers). + * + * If the auth request returns anything other than HTTP 200, the original + * request is rejected. The full response from the auth service is returned + * as the response to the rejected request. + * + * Note that at present, a call to the external service is made for _every + * request_ being routed. + */ + +/** + * All stats for the extauth filter. @see stats_macros.h + */ +// clang-format off +#define ALL_EXTAUTH_STATS(COUNTER) \ + COUNTER(rq_failed) \ + COUNTER(rq_passed) \ + COUNTER(rq_rejected) \ + COUNTER(rq_redirected) +// clang-format on + +/** + * Wrapper struct for extauth filter stats. @see stats_macros.h + */ +struct ExtAuthStats { + ALL_EXTAUTH_STATS(GENERATE_COUNTER_STRUCT) +}; + +/** + * Configuration for the extauth filter. + */ +struct ExtAuthConfig { + Upstream::ClusterManager& cm_; + ExtAuthStats stats_; + std::string cluster_; + std::chrono::milliseconds timeout_; + std::vector allowed_headers_; + std::string path_prefix_; +}; + +typedef std::shared_ptr ExtAuthConfigConstSharedPtr; + +/** + * ExtAuth filter itself. + */ +class ExtAuth : public Logger::Loggable, + public StreamDecoderFilter, + public Http::AsyncClient::Callbacks { +public: + ExtAuth(ExtAuthConfigConstSharedPtr config); + ~ExtAuth(); + + static ExtAuthStats generateStats(const std::string& prefix, Stats::Scope& scope); + + // 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; + + // Http::AsyncClient::Callbacks + void onSuccess(Http::MessagePtr&& response) override; + void onFailure(Http::AsyncClient::FailureReason reason) override; + +private: + void dumpHeaders(const char* what, HeaderMap* headers); // dump headers for debugging + + ExtAuthConfigConstSharedPtr config_; + StreamDecoderFilterCallbacks* callbacks_{}; + bool auth_complete_; + Http::AsyncClient::Request* auth_request_{}; + Http::HeaderMap* request_headers_{}; +}; + +} // Http +} // namespace Envoy diff --git a/source/exe/BUILD b/source/exe/BUILD index 7a49d06c46a3..313e456fef36 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:extauth_lib", "//source/server/config/http:fault_lib", "//source/server/config/http:grpc_http1_bridge_lib", "//source/server/config/http:grpc_json_transcoder_lib", diff --git a/source/server/config/http/BUILD b/source/server/config/http/BUILD index 7f68c86f1f5e..2055240639be 100644 --- a/source/server/config/http/BUILD +++ b/source/server/config/http/BUILD @@ -72,6 +72,17 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "extauth_lib", + srcs = ["extauth_config.cc"], + hdrs = ["extauth_config.h"], + deps = [ + "//source/common/http/filter:extauth_lib", + "//source/server:configuration_lib", + "//source/server/config/network:http_connection_manager_lib", + ], +) + envoy_cc_library( name = "fault_lib", srcs = ["fault.cc"], diff --git a/source/server/config/http/extauth_config.cc b/source/server/config/http/extauth_config.cc new file mode 100644 index 000000000000..a713aaa2b742 --- /dev/null +++ b/source/server/config/http/extauth_config.cc @@ -0,0 +1,60 @@ +#include "server/config/http/extauth_config.h" + +#include "envoy/registry/registry.h" + +#include "common/http/filter/extauth.h" +#include "common/json/config_schemas.h" + +namespace Envoy { +namespace Server { +namespace Configuration { + +const std::string EXTAUTH_HTTP_FILTER_SCHEMA(R"EOF( + { + "$schema": "http://json-schema.org/schema#", + "type" : "object", + "properties" : { + "cluster" : {"type" : "string"}, + "timeout_ms": {"type" : "integer"}, + "allowed_headers": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "path_prefix": {"type" : "string"} + }, + "required" : ["cluster", "timeout_ms"], + "additionalProperties" : false + } + )EOF"); + +HttpFilterFactoryCb ExtAuthConfig::createFilterFactory(const Json::Object& json_config, + const std::string& stats_prefix, + FactoryContext& context) { + json_config.validateSchema(EXTAUTH_HTTP_FILTER_SCHEMA); + + std::string prefix = + json_config.hasObject("path_prefix") ? json_config.getString("path_prefix") : ""; + + Http::ExtAuthConfigConstSharedPtr config(new Http::ExtAuthConfig{ + context.clusterManager(), Http::ExtAuth::generateStats(stats_prefix, context.scope()), + json_config.getString("cluster"), + std::chrono::milliseconds(json_config.getInteger("timeout_ms")), + json_config.getStringArray("allowed_headers", true), prefix}); + + return [config](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamDecoderFilter(std::make_shared(config)); + }; +} + +/** + * Static registration for the extauth filter. @see RegisterHttpFilterConfigFactory. + */ +static Registry::RegisterFactory register_; + +} // namespace Configuration +} // namespace Server +} // namespace Envoy diff --git a/source/server/config/http/extauth_config.h b/source/server/config/http/extauth_config.h new file mode 100644 index 000000000000..87a0923e8b58 --- /dev/null +++ b/source/server/config/http/extauth_config.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include "envoy/server/filter_config.h" + +// #include "envoy/server/instance.h" + +// #include "server/config/network/http_connection_manager.h" + +namespace Envoy { +namespace Server { +namespace Configuration { + +/** + * Config registration for the ExtAuth filter. @see HttpFilterConfigFactory. + */ +class ExtAuthConfig : public NamedHttpFilterConfigFactory { +public: + std::string name() override { return "extauth"; } + + HttpFilterFactoryCb createFilterFactory(const Json::Object& json_config, + const std::string& stats_prefix, + FactoryContext& context) override; +}; + +} // namespace Configuration +} // namespace Server +} // namespace Envoy