From fb35a70aaa475ad34b59f359464fcec390c809b6 Mon Sep 17 00:00:00 2001 From: Yuval Kohavi Date: Mon, 23 Sep 2019 15:09:36 -0400 Subject: [PATCH] Aws iam (#25) * update register factory * create path just once * add iam support for AWS; no tests yet * updated code and tests * add missing test file + integration test * update e2e test * test invoking the timer callback * add stats * PR Comments --- .../http/aws_lambda/v2/aws_lambda.proto | 17 +- e2e/extensions/filters/http/aws_lambda/BUILD | 5 +- .../http/aws_lambda/create_config_env.sh | 107 ++++++++++++ .../filters/http/aws_lambda/e2e2e_test.sh | 18 +- .../extensions/filters/http/aws_lambda/BUILD | 5 +- .../http/aws_lambda/aws_authenticator.cc | 2 +- .../http/aws_lambda/aws_lambda_filter.cc | 64 +++++-- .../http/aws_lambda/aws_lambda_filter.h | 9 +- .../aws_lambda_filter_config_factory.cc | 49 +++--- .../aws_lambda_filter_config_factory.h | 31 ++-- .../filters/http/aws_lambda/config.cc | 133 +++++++++++++- .../filters/http/aws_lambda/config.h | 88 ++++++++-- .../nats/streaming/nats_streaming_filter.cc | 2 +- .../nats_streaming_filter_config_factory.cc | 8 +- .../transformation_filter_config_factory.cc | 8 +- test/extensions/filters/http/aws_lambda/BUILD | 14 ++ .../http/aws_lambda/aws_lambda_filter_test.cc | 95 +++++++++- .../filters/http/aws_lambda/config_test.cc | 165 ++++++++++++++++++ .../aws_lambda_filter_integration_test.cc | 60 +++++-- 19 files changed, 771 insertions(+), 109 deletions(-) create mode 100755 e2e/extensions/filters/http/aws_lambda/create_config_env.sh create mode 100644 test/extensions/filters/http/aws_lambda/config_test.cc diff --git a/api/envoy/config/filter/http/aws_lambda/v2/aws_lambda.proto b/api/envoy/config/filter/http/aws_lambda/v2/aws_lambda.proto index 70130ca03..8ce89dc83 100644 --- a/api/envoy/config/filter/http/aws_lambda/v2/aws_lambda.proto +++ b/api/envoy/config/filter/http/aws_lambda/v2/aws_lambda.proto @@ -16,7 +16,7 @@ import "validate/validate.proto"; message AWSLambdaPerRoute { // The name of the function string name = 1 [ (validate.rules).string.min_bytes = 1 ]; - // The qualifier of the function (defualts to $LATEST if not specified) + // The qualifier of the function (defaults to $LATEST if not specified) string qualifier = 2; // Invocation type - async or regular. @@ -33,7 +33,18 @@ message AWSLambdaProtocolExtension { // The region for this cluster string region = 2 [ (validate.rules).string.min_bytes = 1 ]; // The access_key for AWS this cluster - string access_key = 3 [ (validate.rules).string.min_bytes = 1 ]; + string access_key = 3; // The secret_key for AWS this cluster - string secret_key = 4 [ (validate.rules).string.min_bytes = 1 ]; + string secret_key = 4; } + +message AWSLambdaConfig { + // Use AWS default credentials chain to get credentials. + // This will search environment variables, ECS metadata and instance metadata + // to get the credentials. credentials will be rotated automatically. + // + // If credentials are provided on the cluster (using the + // AWSLambdaProtocolExtension), it will override these credentials. This + // defaults to false, but may change in the future to true. + google.protobuf.BoolValue use_default_credentials = 1; +} \ No newline at end of file diff --git a/e2e/extensions/filters/http/aws_lambda/BUILD b/e2e/extensions/filters/http/aws_lambda/BUILD index ea56630a5..a250cd41a 100644 --- a/e2e/extensions/filters/http/aws_lambda/BUILD +++ b/e2e/extensions/filters/http/aws_lambda/BUILD @@ -2,10 +2,8 @@ licenses(["notice"]) # Apache 2 load( "@envoy//bazel:envoy_build_system.bzl", - "envoy_cc_binary", - "envoy_cc_library", - "envoy_cc_test", "envoy_package", + "envoy_sh_test", ) envoy_package() @@ -18,5 +16,6 @@ sh_test( data = [ "//:envoy", "//e2e/extensions/filters/http/aws_lambda:create_config.sh", + "//e2e/extensions/filters/http/aws_lambda:create_config_env.sh", ], ) diff --git a/e2e/extensions/filters/http/aws_lambda/create_config_env.sh b/e2e/extensions/filters/http/aws_lambda/create_config_env.sh new file mode 100755 index 000000000..1fdb94a8e --- /dev/null +++ b/e2e/extensions/filters/http/aws_lambda/create_config_env.sh @@ -0,0 +1,107 @@ +#!/bin/bash +# + +set -e + +# # create function if doesnt exist +# aws lambda create-function --function-name captialize --runtime nodejs +# invoke +# aws lambda invoke --function-name uppercase --payload '"abc"' /dev/stdout + + +# prepare envoy config file. + +cat > envoy_env.yaml << EOF +admin: + access_log_path: /dev/stdout + address: + socket_address: + address: 127.0.0.1 + port_value: 19001 +static_resources: + listeners: + - name: listener_0 + address: + socket_address: { address: 127.0.0.1, port_value: 10001 } + filter_chains: + - filters: + - name: envoy.http_connection_manager + config: + stat_prefix: http + codec_type: AUTO + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: + prefix: /echo + route: + cluster: postman-echo + prefix_rewrite: /post + - match: + prefix: /lambda + route: + cluster: aws-us-east-1-lambda + per_filter_config: + io.solo.aws_lambda: + name: uppercase + qualifier: "1" + - match: + prefix: /latestlambda + route: + cluster: aws-us-east-1-lambda + per_filter_config: + io.solo.aws_lambda: + name: uppercase + qualifier: "%24LATEST" + - match: + prefix: /contact-empty-default + route: + cluster: aws-us-east-1-lambda + per_filter_config: + io.solo.aws_lambda: + name: uppercase + qualifier: "1" + empty_body_override: "\"default-body\"" + - match: + prefix: /contact + route: + cluster: aws-us-east-1-lambda + per_filter_config: + io.solo.aws_lambda: + name: contact-form + qualifier: "3" + http_filters: + - name: io.solo.aws_lambda + config: + use_default_credentials: true + - name: envoy.router + clusters: + - connect_timeout: 5.000s + hosts: + - socket_address: + address: postman-echo.com + port_value: 443 + name: postman-echo + type: LOGICAL_DNS + tls_context: {} + - connect_timeout: 5.000s + hosts: + - socket_address: + address: lambda.us-east-1.amazonaws.com + port_value: 443 + name: aws-us-east-1-lambda + type: LOGICAL_DNS + dns_lookup_family: V4_ONLY + tls_context: {} + extension_protocol_options: + io.solo.aws_lambda: + host: lambda.us-east-1.amazonaws.com + region: us-east-1 +EOF + + +export AWS_ACCESS_KEY_ID=$(grep aws_access_key_id ~/.aws/credentials | head -1 | cut -d= -f2 |tr -d '[:space:]') +export AWS_SECRET_ACCESS_KEY=$(grep aws_secret_access_key ~/.aws/credentials | head -1 | cut -d= -f2 |tr -d '[:space:]') \ No newline at end of file diff --git a/e2e/extensions/filters/http/aws_lambda/e2e2e_test.sh b/e2e/extensions/filters/http/aws_lambda/e2e2e_test.sh index 2fb9bdf96..a6c01196a 100755 --- a/e2e/extensions/filters/http/aws_lambda/e2e2e_test.sh +++ b/e2e/extensions/filters/http/aws_lambda/e2e2e_test.sh @@ -12,7 +12,7 @@ set -e ENVOY=${ENVOY:-envoy} -$ENVOY -c ./envoy.yaml --log-level debug & +$ENVOY --disable-hot-restart -c ./envoy.yaml --log-level debug & sleep 5 @@ -27,4 +27,20 @@ curl localhost:10000/contact |grep '
RcDetails; +} // namespace class AWSLambdaHeaderValues { public: @@ -47,22 +54,13 @@ const HeaderList AWSLambdaFilter::HeadersToSign = Http::Headers::get().ContentType}); AWSLambdaFilter::AWSLambdaFilter(Upstream::ClusterManager &cluster_manager, - TimeSource &time_source) - : aws_authenticator_(time_source), cluster_manager_(cluster_manager) {} + TimeSource &time_source, + AWSLambdaConfigConstSharedPtr filter_config) + : aws_authenticator_(time_source), cluster_manager_(cluster_manager), + filter_config_(filter_config) {} AWSLambdaFilter::~AWSLambdaFilter() {} -std::string AWSLambdaFilter::functionUrlPath(const std::string &name, - const std::string &qualifier) { - - std::stringstream val; - val << "/2015-03-31/functions/" << name << "/invocations"; - if (!qualifier.empty()) { - val << "?Qualifier=" << qualifier; - } - return val.str(); -} - Http::FilterHeadersStatus AWSLambdaFilter::decodeHeaders(Http::HeaderMap &headers, bool end_stream) { @@ -70,10 +68,39 @@ AWSLambdaFilter::decodeHeaders(Http::HeaderMap &headers, bool end_stream) { const AWSLambdaProtocolExtensionConfig>( SoloHttpFilterNames::get().AwsLambda, decoder_callbacks_, cluster_manager_); + if (!protocol_options_) { return Http::FilterHeadersStatus::Continue; } + const std::string *access_key{}; + const std::string *secret_key{}; + if (protocol_options_->accessKey().has_value() && + protocol_options_->secretKey().has_value()) { + access_key = &protocol_options_->accessKey().value(); + secret_key = &protocol_options_->secretKey().value(); + } else if (filter_config_) { + credentials_ = filter_config_->getCredentials(); + if (credentials_) { + const absl::optional &maybeAccessKeyId = + credentials_->accessKeyId(); + const absl::optional &maybeSecretAccessKey = + credentials_->secretAccessKey(); + if (maybeAccessKeyId.has_value() && maybeSecretAccessKey.has_value()) { + access_key = &maybeAccessKeyId.value(); + secret_key = &maybeSecretAccessKey.value(); + } + } + } + + if ((access_key == nullptr) || (secret_key == nullptr)) { + decoder_callbacks_->sendLocalReply(Http::Code::InternalServerError, + RcDetails::get().CredentialsNotFoundBody, + nullptr, absl::nullopt, + RcDetails::get().CredentialsNotFound); + return Http::FilterHeadersStatus::StopIteration; + } + route_ = decoder_callbacks_->route(); // great! this is an aws cluster. get the function information: function_on_route_ = @@ -82,20 +109,19 @@ AWSLambdaFilter::decodeHeaders(Http::HeaderMap &headers, bool end_stream) { if (!function_on_route_) { decoder_callbacks_->sendLocalReply( - Http::Code::NotFound, "no function present for AWS upstream", nullptr, - absl::nullopt, RcDetails::get().FunctionNotFound); + Http::Code::InternalServerError, RcDetails::get().FunctionNotFoundBody, + nullptr, absl::nullopt, RcDetails::get().FunctionNotFound); return Http::FilterHeadersStatus::StopIteration; } - aws_authenticator_.init(&protocol_options_->accessKey(), - &protocol_options_->secretKey()); + aws_authenticator_.init(access_key, secret_key); request_headers_ = &headers; request_headers_->insertMethod().value().setReference( Http::Headers::get().MethodValues.Post); - request_headers_->insertPath().value(functionUrlPath( - function_on_route_->name(), function_on_route_->qualifier())); + request_headers_->insertPath().value().setReference( + function_on_route_->path()); if (end_stream) { lambdafy(); diff --git a/source/extensions/filters/http/aws_lambda/aws_lambda_filter.h b/source/extensions/filters/http/aws_lambda/aws_lambda_filter.h index b5626a142..c12f8eeba 100644 --- a/source/extensions/filters/http/aws_lambda/aws_lambda_filter.h +++ b/source/extensions/filters/http/aws_lambda/aws_lambda_filter.h @@ -23,7 +23,8 @@ namespace AwsLambda { class AWSLambdaFilter : public Http::StreamDecoderFilter { public: AWSLambdaFilter(Upstream::ClusterManager &cluster_manager, - TimeSource &time_source); + TimeSource &time_source, + AWSLambdaConfigConstSharedPtr filter_config); ~AWSLambdaFilter(); // Http::StreamFilterBase @@ -44,8 +45,6 @@ class AWSLambdaFilter : public Http::StreamDecoderFilter { void handleDefaultBody(); void lambdafy(); - static std::string functionUrlPath(const std::string &name, - const std::string &qualifier); void cleanup(); Http::HeaderMap *request_headers_{}; @@ -59,6 +58,10 @@ class AWSLambdaFilter : public Http::StreamDecoderFilter { Router::RouteConstSharedPtr route_; const AWSLambdaRouteConfig *function_on_route_{}; bool has_body_{}; + + AWSLambdaConfigConstSharedPtr filter_config_; + + CredentialsConstSharedPtr credentials_; }; } // namespace AwsLambda diff --git a/source/extensions/filters/http/aws_lambda/aws_lambda_filter_config_factory.cc b/source/extensions/filters/http/aws_lambda/aws_lambda_filter_config_factory.cc index 763eebeec..a65583659 100644 --- a/source/extensions/filters/http/aws_lambda/aws_lambda_filter_config_factory.cc +++ b/source/extensions/filters/http/aws_lambda/aws_lambda_filter_config_factory.cc @@ -3,21 +3,31 @@ #include "envoy/registry/registry.h" #include "extensions/filters/http/aws_lambda/aws_lambda_filter.h" - -#include "api/envoy/config/filter/http/aws_lambda/v2/aws_lambda.pb.validate.h" +#include "extensions/filters/http/common/aws/credentials_provider_impl.h" +#include "extensions/filters/http/common/aws/utility.h" namespace Envoy { namespace Extensions { namespace HttpFilters { namespace AwsLambda { -Http::FilterFactoryCb AWSLambdaFilterConfigFactory::createFilter( - const std::string &, Server::Configuration::FactoryContext &context) { - return [&context](Http::FilterChainFactoryCallbacks &callbacks) -> void { - auto filter = new AWSLambdaFilter(context.clusterManager(), - context.dispatcher().timeSource()); - callbacks.addStreamDecoderFilter( - Http::StreamDecoderFilterSharedPtr{filter}); +Http::FilterFactoryCb +AWSLambdaFilterConfigFactory::createFilterFactoryFromProtoTyped( + const envoy::config::filter::http::aws_lambda::v2::AWSLambdaConfig + &proto_config, + const std::string &stats_prefix, + Server::Configuration::FactoryContext &context) { + + auto config = std::make_shared( + std::make_unique( + context.api(), HttpFilters::Common::Aws::Utility::metadataFetcher), + context.dispatcher(), context.threadLocal(), stats_prefix, + context.scope(), proto_config); + + return [&context, + config](Http::FilterChainFactoryCallbacks &callbacks) -> void { + callbacks.addStreamDecoderFilter(std::make_shared( + context.clusterManager(), context.dispatcher().timeSource(), config)); }; } @@ -36,28 +46,19 @@ AWSLambdaFilterConfigFactory::createEmptyProtocolOptionsProto() { AWSLambdaProtocolExtension>(); } -ProtobufTypes::MessagePtr -AWSLambdaFilterConfigFactory::createEmptyRouteConfigProto() { - return std::make_unique< - envoy::config::filter::http::aws_lambda::v2::AWSLambdaPerRoute>(); -} - Router::RouteSpecificFilterConfigConstSharedPtr -AWSLambdaFilterConfigFactory::createRouteSpecificFilterConfig( - const Protobuf::Message &config, Server::Configuration::FactoryContext &) { - const auto &proto_config = dynamic_cast< - const envoy::config::filter::http::aws_lambda::v2::AWSLambdaPerRoute &>( - config); +AWSLambdaFilterConfigFactory::createRouteSpecificFilterConfigTyped( + const envoy::config::filter::http::aws_lambda::v2::AWSLambdaPerRoute + &proto_config, + Server::Configuration::FactoryContext &) { return std::make_shared(proto_config); } /** * Static registration for the AWS Lambda filter. @see RegisterFactory. */ -static Registry::RegisterFactory< - AWSLambdaFilterConfigFactory, - Server::Configuration::NamedHttpFilterConfigFactory> - register_; +REGISTER_FACTORY(AWSLambdaFilterConfigFactory, + Server::Configuration::NamedHttpFilterConfigFactory); } // namespace AwsLambda } // namespace HttpFilters diff --git a/source/extensions/filters/http/aws_lambda/aws_lambda_filter_config_factory.h b/source/extensions/filters/http/aws_lambda/aws_lambda_filter_config_factory.h index a79f64e5a..95b29a94d 100644 --- a/source/extensions/filters/http/aws_lambda/aws_lambda_filter_config_factory.h +++ b/source/extensions/filters/http/aws_lambda/aws_lambda_filter_config_factory.h @@ -2,37 +2,42 @@ #include "envoy/upstream/upstream.h" -#include "extensions/filters/http/common/empty_http_filter_config.h" +#include "extensions/filters/http/common/factory_base.h" #include "extensions/filters/http/solo_well_known_names.h" +#include "api/envoy/config/filter/http/aws_lambda/v2/aws_lambda.pb.validate.h" + namespace Envoy { namespace Extensions { namespace HttpFilters { namespace AwsLambda { -using Extensions::HttpFilters::Common::EmptyHttpFilterConfig; - /** * Config registration for the AWS Lambda filter. */ -class AWSLambdaFilterConfigFactory : public EmptyHttpFilterConfig { +class AWSLambdaFilterConfigFactory + : public Common::FactoryBase< + envoy::config::filter::http::aws_lambda::v2::AWSLambdaConfig, + envoy::config::filter::http::aws_lambda::v2::AWSLambdaPerRoute> { public: AWSLambdaFilterConfigFactory() - : EmptyHttpFilterConfig(SoloHttpFilterNames::get().AwsLambda) {} + : FactoryBase(SoloHttpFilterNames::get().AwsLambda) {} Upstream::ProtocolOptionsConfigConstSharedPtr createProtocolOptionsConfig(const Protobuf::Message &config) override; ProtobufTypes::MessagePtr createEmptyProtocolOptionsProto() override; - ProtobufTypes::MessagePtr createEmptyRouteConfigProto() override; - Router::RouteSpecificFilterConfigConstSharedPtr - createRouteSpecificFilterConfig( - const Protobuf::Message &, - Server::Configuration::FactoryContext &) override; private: - Http::FilterFactoryCb - createFilter(const std::string &stat_prefix, - Server::Configuration::FactoryContext &context) override; + Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::config::filter::http::aws_lambda::v2::AWSLambdaConfig + &proto_config, + const std::string &stats_prefix, + Server::Configuration::FactoryContext &context) override; + + Router::RouteSpecificFilterConfigConstSharedPtr + createRouteSpecificFilterConfigTyped( + const envoy::config::filter::http::aws_lambda::v2::AWSLambdaPerRoute &, + Server::Configuration::FactoryContext &) override; }; } // namespace AwsLambda diff --git a/source/extensions/filters/http/aws_lambda/config.cc b/source/extensions/filters/http/aws_lambda/config.cc index b5b54ccf7..37f271032 100644 --- a/source/extensions/filters/http/aws_lambda/config.cc +++ b/source/extensions/filters/http/aws_lambda/config.cc @@ -1,14 +1,121 @@ #include "extensions/filters/http/aws_lambda/config.h" +#include "envoy/thread_local/thread_local.h" + namespace Envoy { namespace Extensions { namespace HttpFilters { namespace AwsLambda { +namespace CommonAws = Envoy::Extensions::HttpFilters::Common::Aws; + +namespace { + +// Current AWS implementation will only refresh creds if *more* than an hour has +// passed. In the AWS impl these creds are fetched on the main thread. As we use +// the creds in the filter, we cache the creds in a TLS slot, so they are +// available to the filters in the worker threads. As we have our own cache, we +// have to implement our own refresh loop. To avoid being to tied down the +// timing of the AWS implementation, we'll have a refresh interval of a few +// minutes. refresh itself will only happen when deemed needed by the AWS code. +// +// According to the AWS docs +// (https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html) +// +// >>> it should get a refreshed set of credentials every hour, or at least 15 +// >>> minutes before the current set expires. +// +// Refreshing every 14 minutes should guarantee us fresh credentials. +constexpr std::chrono::milliseconds REFRESH_AWS_CREDS = + std::chrono::minutes(14); + +struct ThreadLocalState : public Envoy::ThreadLocal::ThreadLocalObject { + ThreadLocalState(CredentialsConstSharedPtr credentials) + : credentials_(credentials) {} + CredentialsConstSharedPtr credentials_; +}; + +} // namespace + +AWSLambdaConfigImpl::AWSLambdaConfigImpl( + std::unique_ptr &&provider, + Event::Dispatcher &dispatcher, Envoy::ThreadLocal::SlotAllocator &tls, + const std::string &stats_prefix, Stats::Scope &scope, + const envoy::config::filter::http::aws_lambda::v2::AWSLambdaConfig + &protoconfig) + : stats_(generateStats(stats_prefix, scope)) { + bool use_default_credentials = false; + + if (protoconfig.has_use_default_credentials()) { + use_default_credentials = protoconfig.use_default_credentials().value(); + } + + if (use_default_credentials) { + provider_ = std::move(provider); + + tls_slot_ = tls.allocateSlot(); + auto empty_creds = std::make_shared(); + tls_slot_->set([empty_creds](Event::Dispatcher &) { + return std::make_shared(empty_creds); + }); + + timer_ = dispatcher.createTimer([this] { timerCallback(); }); + // call the time callback to fetch credentials now. + // this will also re-trigger the timer. + timerCallback(); + } +} + +CredentialsConstSharedPtr AWSLambdaConfigImpl::getCredentials() const { + if (!provider_) { + return {}; + } + + // tls_slot_ != nil IFF provider_ != nil + return tls_slot_->getTyped().credentials_; +} + +void AWSLambdaConfigImpl::timerCallback() { + // get new credentials. + auto new_creds = provider_->getCredentials(); + if (new_creds == CommonAws::Credentials()) { + stats_.fetch_failed_.inc(); + stats_.current_state_.set(0); + ENVOY_LOG(warn, "can't get AWS credentials - credentials will not be " + "refreshed and request to AWS may fail"); + } else { + stats_.fetch_success_.inc(); + stats_.current_state_.set(1); + auto currentCreds = getCredentials(); + if (currentCreds == nullptr || !((*currentCreds) == new_creds)) { + stats_.creds_rotated_.inc(); + ENVOY_LOG(debug, "refreshing AWS credentials"); + auto shared_new_creds = + std::make_shared(new_creds); + tls_slot_->set([shared_new_creds](Event::Dispatcher &) { + return std::make_shared(shared_new_creds); + }); + } + } + + if (timer_ != nullptr) { + // re-enable refersh timer + timer_->enableTimer(REFRESH_AWS_CREDS); + } +} + +AwsLambdaFilterStats +AWSLambdaConfigImpl::generateStats(const std::string &prefix, + Stats::Scope &scope) { + const std::string final_prefix = prefix + "aws_lambda."; + return {ALL_AWS_LAMBDA_FILTER_STATS(POOL_COUNTER_PREFIX(scope, final_prefix), + POOL_GAUGE_PREFIX(scope, final_prefix))}; +} + AWSLambdaRouteConfig::AWSLambdaRouteConfig( const envoy::config::filter::http::aws_lambda::v2::AWSLambdaPerRoute &protoconfig) - : name_(protoconfig.name()), qualifier_(protoconfig.qualifier()), + : path_(functionUrlPath(protoconfig.name(), protoconfig.qualifier())), async_(protoconfig.async()) { if (protoconfig.has_empty_body_override()) { @@ -16,6 +123,30 @@ AWSLambdaRouteConfig::AWSLambdaRouteConfig( } } +std::string +AWSLambdaRouteConfig::functionUrlPath(const std::string &name, + const std::string &qualifier) { + + std::stringstream val; + val << "/2015-03-31/functions/" << name << "/invocations"; + if (!qualifier.empty()) { + val << "?Qualifier=" << qualifier; + } + return val.str(); +} + +AWSLambdaProtocolExtensionConfig::AWSLambdaProtocolExtensionConfig( + const envoy::config::filter::http::aws_lambda::v2:: + AWSLambdaProtocolExtension &protoconfig) + : host_(protoconfig.host()), region_(protoconfig.region()) { + if (!protoconfig.access_key().empty()) { + access_key_ = protoconfig.access_key(); + } + if (!protoconfig.secret_key().empty()) { + secret_key_ = protoconfig.secret_key(); + } +} + } // namespace AwsLambda } // namespace HttpFilters } // namespace Extensions diff --git a/source/extensions/filters/http/aws_lambda/config.h b/source/extensions/filters/http/aws_lambda/config.h index f64e5741a..0b629ff07 100644 --- a/source/extensions/filters/http/aws_lambda/config.h +++ b/source/extensions/filters/http/aws_lambda/config.h @@ -4,8 +4,12 @@ #include #include "envoy/http/filter.h" +#include "envoy/stats/scope.h" +#include "envoy/stats/stats_macros.h" #include "envoy/upstream/cluster_manager.h" +#include "extensions/filters/http/common/aws/credentials_provider.h" + #include "absl/types/optional.h" #include "api/envoy/config/filter/http/aws_lambda/v2/aws_lambda.pb.validate.h" @@ -14,24 +18,87 @@ namespace Extensions { namespace HttpFilters { namespace AwsLambda { +/** + * All stats for the fault filter. @see stats_macros.h + */ +#define ALL_AWS_LAMBDA_FILTER_STATS(COUNTER, GAUGE) \ + COUNTER(fetch_failed) \ + COUNTER(fetch_success) \ + COUNTER(creds_rotated) \ + GAUGE(current_state, NeverImport) + +/** + * Wrapper struct for connection manager stats. @see stats_macros.h + */ +struct AwsLambdaFilterStats { + ALL_AWS_LAMBDA_FILTER_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT) +}; + +typedef std::shared_ptr< + Envoy::Extensions::HttpFilters::Common::Aws::Credentials> + CredentialsSharedPtr; +typedef std::shared_ptr< + const Envoy::Extensions::HttpFilters::Common::Aws::Credentials> + CredentialsConstSharedPtr; + +class AWSLambdaConfig { +public: + virtual CredentialsConstSharedPtr getCredentials() const PURE; + virtual ~AWSLambdaConfig() = default; +}; + +class AWSLambdaConfigImpl + : public AWSLambdaConfig, + public Envoy::Logger::Loggable { +public: + AWSLambdaConfigImpl( + std::unique_ptr< + Envoy::Extensions::HttpFilters::Common::Aws::CredentialsProvider> + &&provider, + Event::Dispatcher &dispatcher, Envoy::ThreadLocal::SlotAllocator &, + const std::string &stats_prefix, Stats::Scope &scope, + const envoy::config::filter::http::aws_lambda::v2::AWSLambdaConfig + &protoconfig); + ~AWSLambdaConfigImpl() = default; + + CredentialsConstSharedPtr getCredentials() const override; + +private: + static AwsLambdaFilterStats generateStats(const std::string &prefix, + Stats::Scope &scope); + + void timerCallback(); + + std::unique_ptr provider_; + + ThreadLocal::SlotPtr tls_slot_; + + Event::TimerPtr timer_; + + AwsLambdaFilterStats stats_; +}; + +typedef std::shared_ptr AWSLambdaConfigConstSharedPtr; + class AWSLambdaRouteConfig : public Router::RouteSpecificFilterConfig { public: AWSLambdaRouteConfig( const envoy::config::filter::http::aws_lambda::v2::AWSLambdaPerRoute &protoconfig); - const std::string &name() const { return name_; } - const std::string &qualifier() const { return qualifier_; } + const std::string &path() const { return path_; } bool async() const { return async_; } const absl::optional &defaultBody() const { return default_body_; } private: - std::string name_; - std::string qualifier_; + std::string path_; bool async_; absl::optional default_body_; + + static std::string functionUrlPath(const std::string &name, + const std::string &qualifier); }; class AWSLambdaProtocolExtensionConfig @@ -39,21 +106,18 @@ class AWSLambdaProtocolExtensionConfig public: AWSLambdaProtocolExtensionConfig( const envoy::config::filter::http::aws_lambda::v2:: - AWSLambdaProtocolExtension &protoconfig) - : host_(protoconfig.host()), region_(protoconfig.region()), - access_key_(protoconfig.access_key()), - secret_key_(protoconfig.secret_key()) {} + AWSLambdaProtocolExtension &protoconfig); const std::string &host() const { return host_; } const std::string ®ion() const { return region_; } - const std::string &accessKey() const { return access_key_; } - const std::string &secretKey() const { return secret_key_; } + const absl::optional &accessKey() const { return access_key_; } + const absl::optional &secretKey() const { return secret_key_; } private: std::string host_; std::string region_; - std::string access_key_; - std::string secret_key_; + absl::optional access_key_; + absl::optional secret_key_; }; } // namespace AwsLambda diff --git a/source/extensions/filters/http/nats/streaming/nats_streaming_filter.cc b/source/extensions/filters/http/nats/streaming/nats_streaming_filter.cc index d748591f2..7abbdaa03 100644 --- a/source/extensions/filters/http/nats/streaming/nats_streaming_filter.cc +++ b/source/extensions/filters/http/nats/streaming/nats_streaming_filter.cc @@ -11,8 +11,8 @@ #include "common/common/macros.h" #include "common/common/utility.h" #include "common/grpc/common.h" -#include "common/http/utility.h" #include "common/http/solo_filter_utility.h" +#include "common/http/utility.h" #include "extensions/filters/http/solo_well_known_names.h" diff --git a/source/extensions/filters/http/nats/streaming/nats_streaming_filter_config_factory.cc b/source/extensions/filters/http/nats/streaming/nats_streaming_filter_config_factory.cc index 070f83500..b6cff073a 100644 --- a/source/extensions/filters/http/nats/streaming/nats_streaming_filter_config_factory.cc +++ b/source/extensions/filters/http/nats/streaming/nats_streaming_filter_config_factory.cc @@ -54,12 +54,10 @@ NatsStreamingFilterConfigFactory::createRouteSpecificFilterConfigTyped( } /** - * Static registration for the NATS Streaming filter. @see RegisterFactory. + * Static registration for this filter. @see RegisterFactory. */ -static Registry::RegisterFactory< - NatsStreamingFilterConfigFactory, - Server::Configuration::NamedHttpFilterConfigFactory> - register_; +REGISTER_FACTORY(NatsStreamingFilterConfigFactory, + Server::Configuration::NamedHttpFilterConfigFactory); } // namespace Streaming } // namespace Nats diff --git a/source/extensions/filters/http/transformation/transformation_filter_config_factory.cc b/source/extensions/filters/http/transformation/transformation_filter_config_factory.cc index f7b4ed49a..76eca00ec 100644 --- a/source/extensions/filters/http/transformation/transformation_filter_config_factory.cc +++ b/source/extensions/filters/http/transformation/transformation_filter_config_factory.cc @@ -40,12 +40,10 @@ TransformationFilterConfigFactory::createRouteSpecificFilterConfig( } /** - * Static registration for this sample filter. @see RegisterFactory. + * Static registration for this filter. @see RegisterFactory. */ -static Registry::RegisterFactory< - TransformationFilterConfigFactory, - Server::Configuration::NamedHttpFilterConfigFactory> - register_; +REGISTER_FACTORY(TransformationFilterConfigFactory, + Server::Configuration::NamedHttpFilterConfigFactory); } // namespace Transformation } // namespace HttpFilters diff --git a/test/extensions/filters/http/aws_lambda/BUILD b/test/extensions/filters/http/aws_lambda/BUILD index f48ad9c5e..dcee1b8b5 100644 --- a/test/extensions/filters/http/aws_lambda/BUILD +++ b/test/extensions/filters/http/aws_lambda/BUILD @@ -25,6 +25,20 @@ envoy_gloo_cc_test( ], ) +envoy_gloo_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + repository = "@envoy", + deps = [ + "//source/extensions/filters/http/aws_lambda:aws_lambda_filter_config_lib", + "@envoy//test/extensions/filters/http/common/aws:aws_mocks", + "@envoy//test/mocks/http:http_mocks", + "@envoy//test/mocks/server:server_mocks", + "@envoy//test/mocks/upstream:upstream_mocks", + "@envoy//test/test_common:utility_lib", + ], +) + envoy_gloo_cc_test( name = "aws_authenticator_test", srcs = ["aws_authenticator_test.cc"], diff --git a/test/extensions/filters/http/aws_lambda/aws_lambda_filter_test.cc b/test/extensions/filters/http/aws_lambda/aws_lambda_filter_test.cc index 4df3cd41f..ba243fd61 100644 --- a/test/extensions/filters/http/aws_lambda/aws_lambda_filter_test.cc +++ b/test/extensions/filters/http/aws_lambda/aws_lambda_filter_test.cc @@ -24,12 +24,25 @@ namespace Extensions { namespace HttpFilters { namespace AwsLambda { +class AWSLambdaConfigTestImpl : public AWSLambdaConfig { +public: + CredentialsConstSharedPtr getCredentials() const override { + called_ = true; + return credentials_; + } + + CredentialsConstSharedPtr credentials_; + mutable bool called_{}; +}; + class AWSLambdaFilterTest : public testing::Test { public: AWSLambdaFilterTest() {} protected: - void SetUp() override { + void SetUp() override { setupRoute(true, false); } + + void setupRoute(bool credsOnCluster, bool fetchCredentials) { routeconfig_.set_name("func"); routeconfig_.set_qualifier("v1"); @@ -41,8 +54,15 @@ class AWSLambdaFilterTest : public testing::Test { protoextconfig; protoextconfig.set_host("lambda.us-east-1.amazonaws.com"); protoextconfig.set_region("us-east-1"); - protoextconfig.set_access_key("access key"); - protoextconfig.set_secret_key("secret key"); + if (credsOnCluster) { + protoextconfig.set_access_key("access key"); + protoextconfig.set_secret_key("secret key"); + } else if (fetchCredentials) { + filter_config_ = std::make_shared(); + filter_config_->credentials_ = std::make_shared< + Envoy::Extensions::HttpFilters::Common::Aws::Credentials>( + "access key", "secret key"); + } ON_CALL( *factory_context_.cluster_manager_.thread_local_cluster_.cluster_.info_, @@ -53,7 +73,7 @@ class AWSLambdaFilterTest : public testing::Test { filter_ = std::make_unique( factory_context_.cluster_manager_, - factory_context_.dispatcher().timeSource()); + factory_context_.dispatcher().timeSource(), filter_config_); filter_->setDecoderFilterCallbacks(filter_callbacks_); } @@ -72,11 +92,12 @@ class AWSLambdaFilterTest : public testing::Test { std::unique_ptr filter_; envoy::config::filter::http::aws_lambda::v2::AWSLambdaPerRoute routeconfig_; std::unique_ptr filter_route_config_; + std::shared_ptr filter_config_; }; // see: // https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html -TEST_F(AWSLambdaFilterTest, SingsOnHeadersEndStream) { +TEST_F(AWSLambdaFilterTest, SignsOnHeadersEndStream) { Http::TestHeaderMapImpl headers{{":method", "GET"}, {":authority", "www.solo.io"}, @@ -88,7 +109,37 @@ TEST_F(AWSLambdaFilterTest, SingsOnHeadersEndStream) { EXPECT_TRUE(headers.has("Authorization")); } -TEST_F(AWSLambdaFilterTest, SingsOnDataEndStream) { +TEST_F(AWSLambdaFilterTest, SignsOnHeadersEndStreamWithConfig) { + setupRoute(false, true); + + Http::TestHeaderMapImpl headers{{":method", "GET"}, + {":authority", "www.solo.io"}, + {":path", "/getsomething"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, + filter_->decodeHeaders(headers, true)); + + EXPECT_TRUE(filter_config_->called_); + // Check aws headers. + EXPECT_TRUE(headers.has("Authorization")); +} + +TEST_F(AWSLambdaFilterTest, SignsOnHeadersEndStreamWithBadConfig) { + setupRoute(false, true); + filter_config_->credentials_ = std::make_shared< + Envoy::Extensions::HttpFilters::Common::Aws::Credentials>("access key"); + + Http::TestHeaderMapImpl headers{{":method", "GET"}, + {":authority", "www.solo.io"}, + {":path", "/getsomething"}}; + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(headers, true)); + + // Check no aws headers. + EXPECT_TRUE(filter_config_->called_); + EXPECT_FALSE(headers.has("Authorization")); +} + +TEST_F(AWSLambdaFilterTest, SignsOnDataEndStream) { Http::TestHeaderMapImpl headers{{":method", "GET"}, {":authority", "www.solo.io"}, @@ -268,6 +319,38 @@ TEST_F(AWSLambdaFilterTest, EmptyBodyWithTrailersGetsOverriden) { filter_->decodeTrailers(headers); } +TEST_F(AWSLambdaFilterTest, NoFunctionOnRoute) { + ON_CALL(filter_callbacks_.route_->route_entry_, + perFilterConfig(SoloHttpFilterNames::get().AwsLambda)) + .WillByDefault(Return(nullptr)); + + Http::TestHeaderMapImpl headers{{":method", "GET"}, + {":authority", "www.solo.io"}, + {":path", "/getsomething"}}; + + EXPECT_CALL(filter_callbacks_, + sendLocalReply(Http::Code::InternalServerError, _, _, _, _)) + .Times(1); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(headers, true)); +} + +TEST_F(AWSLambdaFilterTest, NoCredsAvailable) { + setupRoute(false, false); + + Http::TestHeaderMapImpl headers{{":method", "GET"}, + {":authority", "www.solo.io"}, + {":path", "/getsomething"}}; + + EXPECT_CALL(filter_callbacks_, + sendLocalReply(Http::Code::InternalServerError, _, _, _, _)) + .Times(1); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(headers, true)); +} + } // namespace AwsLambda } // namespace HttpFilters } // namespace Extensions diff --git a/test/extensions/filters/http/aws_lambda/config_test.cc b/test/extensions/filters/http/aws_lambda/config_test.cc new file mode 100644 index 000000000..8e6904219 --- /dev/null +++ b/test/extensions/filters/http/aws_lambda/config_test.cc @@ -0,0 +1,165 @@ +#include "extensions/filters/http/aws_lambda/config.h" + +#include "test/extensions/filters/http/common/aws/mocks.h" +#include "test/mocks/common.h" +#include "test/mocks/server/mocks.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/utility.h" + +#include "fmt/format.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::AtLeast; +using testing::Invoke; +using testing::Return; +using testing::ReturnPointee; +using testing::ReturnRef; +using testing::SaveArg; +using testing::WithArg; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AwsLambda { + +class ConfigTest : public testing::Test { +public: + ConfigTest() {} + +protected: + void SetUp() override {} + + NiceMock context_; + Stats::IsolatedStoreImpl stats_; + + envoy::config::filter::http::aws_lambda::v2::AWSLambdaConfig protoconfig; + + NiceMock *prpareTimer() { + NiceMock *timer = + new NiceMock(&context_.dispatcher_); + protoconfig.mutable_use_default_credentials()->set_value(true); + EXPECT_CALL(context_.thread_local_, allocateSlot()).Times(1); + // No need to expect a call createTimer as the mock timer does that. + EXPECT_CALL(*timer, enableTimer(_)).Times(2); + return timer; + } +}; + +TEST_F(ConfigTest, WithUseDefaultCreds) { + auto timer = prpareTimer(); + + const Envoy::Extensions::HttpFilters::Common::Aws::Credentials creds( + "access_key", "secret_key"); + + const Envoy::Extensions::HttpFilters::Common::Aws::Credentials creds2( + "access_key2", "secret_key2"); + + auto cred_provider = std::make_unique>(); + EXPECT_CALL(*cred_provider, getCredentials()) + .WillOnce(Return(creds)) + .WillOnce(Return(creds2)); + + AWSLambdaConfigImpl config(std::move(cred_provider), context_.dispatcher_, + context_.thread_local_, "prefix.", stats_, + protoconfig); + + EXPECT_EQ(creds, *config.getCredentials()); + + timer->invokeCallback(); + EXPECT_EQ(creds2, *config.getCredentials()); + + EXPECT_EQ(2UL, stats_.counter("prefix.aws_lambda.fetch_success").value()); + EXPECT_EQ(2UL, stats_.counter("prefix.aws_lambda.creds_rotated").value()); + EXPECT_EQ(1UL, stats_ + .gauge("prefix.aws_lambda.current_state", + Stats::Gauge::ImportMode::NeverImport) + .value()); + EXPECT_EQ(0UL, stats_.counter("prefix.aws_lambda.fetch_failed").value()); +} + +TEST_F(ConfigTest, FailingToRotate) { + auto timer = prpareTimer(); + + const Envoy::Extensions::HttpFilters::Common::Aws::Credentials creds( + "access_key", "secret_key"); + + auto cred_provider = std::make_unique>(); + EXPECT_CALL(*cred_provider, getCredentials()) + .WillOnce(Return(creds)) + .WillOnce( + Return(Envoy::Extensions::HttpFilters::Common::Aws::Credentials())); + + AWSLambdaConfigImpl config(std::move(cred_provider), context_.dispatcher_, + context_.thread_local_, "prefix.", stats_, + protoconfig); + + EXPECT_EQ(creds, *config.getCredentials()); + + timer->invokeCallback(); + + // When we fail to rotate we latch to the last good credentials + EXPECT_EQ(creds, *config.getCredentials()); + + EXPECT_EQ(1UL, stats_.counter("prefix.aws_lambda.fetch_success").value()); + EXPECT_EQ(1UL, stats_.counter("prefix.aws_lambda.creds_rotated").value()); + EXPECT_EQ(0UL, stats_ + .gauge("prefix.aws_lambda.current_state", + Stats::Gauge::ImportMode::NeverImport) + .value()); + EXPECT_EQ(1UL, stats_.counter("prefix.aws_lambda.fetch_failed").value()); +} + +TEST_F(ConfigTest, SameCredsOnTimer) { + auto timer = prpareTimer(); + + const Envoy::Extensions::HttpFilters::Common::Aws::Credentials creds( + "access_key", "secret_key"); + + auto cred_provider = std::make_unique>(); + EXPECT_CALL(*cred_provider, getCredentials()) + .WillOnce(Return(creds)) + .WillOnce(Return(creds)); + + AWSLambdaConfigImpl config(std::move(cred_provider), context_.dispatcher_, + context_.thread_local_, "prefix.", stats_, + protoconfig); + + EXPECT_EQ(creds, *config.getCredentials()); + + timer->invokeCallback(); + EXPECT_EQ(creds, *config.getCredentials()); + + EXPECT_EQ(2UL, stats_.counter("prefix.aws_lambda.fetch_success").value()); + EXPECT_EQ(1UL, stats_.counter("prefix.aws_lambda.creds_rotated").value()); + EXPECT_EQ(1UL, stats_ + .gauge("prefix.aws_lambda.current_state", + Stats::Gauge::ImportMode::NeverImport) + .value()); + EXPECT_EQ(0UL, stats_.counter("prefix.aws_lambda.fetch_failed").value()); +} + +TEST_F(ConfigTest, WithoutUseDefaultCreds) { + protoconfig.mutable_use_default_credentials()->set_value(false); + EXPECT_CALL(context_.thread_local_, allocateSlot()).Times(0); + EXPECT_CALL(context_.dispatcher_, createTimer_(_)).Times(0); + + auto cred_provider = std::make_unique>(); + EXPECT_CALL(*cred_provider, getCredentials()).Times(0); + + AWSLambdaConfigImpl config(std::move(cred_provider), context_.dispatcher_, + context_.thread_local_, "prefix.", stats_, + protoconfig); + + EXPECT_EQ(nullptr, config.getCredentials()); +} + +} // namespace AwsLambda +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/integration/aws_lambda_filter_integration_test.cc b/test/integration/aws_lambda_filter_integration_test.cc index 2f4ed28b2..ddf56ec73 100644 --- a/test/integration/aws_lambda_filter_integration_test.cc +++ b/test/integration/aws_lambda_filter_integration_test.cc @@ -15,6 +15,13 @@ const std::string DEFAULT_LAMBDA_FILTER = name: io.solo.aws_lambda )EOF"; +const std::string USE_CHAIN_LAMBDA_FILTER = + R"EOF( +name: io.solo.aws_lambda +config: + use_default_credentials: true +)EOF"; + class AWSLambdaFilterIntegrationTest : public HttpIntegrationTest, public testing::TestWithParam { @@ -23,20 +30,35 @@ class AWSLambdaFilterIntegrationTest : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, GetParam(), realTime()) {} + void TearDown() override { + TestEnvironment::unsetEnvVar("AWS_ACCESS_KEY_ID"); + TestEnvironment::unsetEnvVar("AWS_SECRET_ACCESS_KEY"); + } + /** * Initializer for an individual integration test. */ void initialize() override { - config_helper_.addFilter(DEFAULT_LAMBDA_FILTER); - - config_helper_.addConfigModifier([](envoy::config::bootstrap::v2::Bootstrap - &bootstrap) { + if (use_chain_) { + // set env vars for test + TestEnvironment::setEnvVar("AWS_ACCESS_KEY_ID", "access key", 1); + TestEnvironment::setEnvVar("AWS_SECRET_ACCESS_KEY", "access key", 1); + config_helper_.addFilter(USE_CHAIN_LAMBDA_FILTER); + } else { + config_helper_.addFilter(DEFAULT_LAMBDA_FILTER); + } + + config_helper_.addConfigModifier([this]( + envoy::config::bootstrap::v2::Bootstrap + &bootstrap) { envoy::config::filter::http::aws_lambda::v2::AWSLambdaProtocolExtension protoextconfig; protoextconfig.set_host("lambda.us-east-1.amazonaws.com"); protoextconfig.set_region("us-east-1"); - protoextconfig.set_access_key("access key"); - protoextconfig.set_secret_key("secret key"); + if (!use_chain_) { + protoextconfig.set_access_key("access key"); + protoextconfig.set_secret_key("secret key"); + } ProtobufWkt::Struct functionstruct; auto &lambda_cluster = @@ -71,17 +93,29 @@ class AWSLambdaFilterIntegrationTest makeHttpConnection(makeClientConnection((lookupPort("http")))); } - /** - * Initialize before every test. - */ - void SetUp() override { initialize(); } + bool use_chain_{}; }; INSTANTIATE_TEST_SUITE_P( IpVersions, AWSLambdaFilterIntegrationTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest())); -TEST_P(AWSLambdaFilterIntegrationTest, Test1) { +TEST_P(AWSLambdaFilterIntegrationTest, TestWithConfig) { + initialize(); + Http::TestHeaderMapImpl request_headers{ + {":method", "POST"}, {":authority", "www.solo.io"}, {":path", "/"}}; + + sendRequestAndWaitForResponse(request_headers, 10, default_response_headers_, + 10); + + EXPECT_NE(0, upstream_request_->headers() + .get(Http::LowerCaseString("authorization")) + ->value() + .size()); +} +TEST_P(AWSLambdaFilterIntegrationTest, TestWithChain) { + use_chain_ = true; + initialize(); Http::TestHeaderMapImpl request_headers{ {":method", "POST"}, {":authority", "www.solo.io"}, {":path", "/"}}; @@ -92,5 +126,9 @@ TEST_P(AWSLambdaFilterIntegrationTest, Test1) { .get(Http::LowerCaseString("authorization")) ->value() .size()); + EXPECT_EQ(1UL, test_server_->gauge("http.config_test.aws_lambda.current_state")->value()); + EXPECT_EQ(1UL, test_server_->counter("http.config_test.aws_lambda.creds_rotated")->value()); + EXPECT_EQ(1UL, test_server_->counter("http.config_test.aws_lambda.creds_rotated")->value()); + EXPECT_EQ(0UL, test_server_->counter("http.config_test.aws_lambda.fetch_failed")->value()); } } // namespace Envoy