diff --git a/CODEOWNERS b/CODEOWNERS index 7b5b0090b9ab..3580d19b7843 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -101,3 +101,4 @@ extensions/filters/common/original_src @snowp @klarose /*/extensions/filters/http/on_demand @dmitri-d @htuch @lambdai /*/extensions/filters/network/local_ratelimit @mattklein123 @junr03 /*/extensions/filters/http/aws_request_signing @rgs1 @derekargueta @mattklein123 @marcomagdy +/*/extensions/filters/http/aws_lambda @mattklein123 @marcomagdy @lavignes diff --git a/api/BUILD b/api/BUILD index 3a16b3e30685..c8d845c6455c 100644 --- a/api/BUILD +++ b/api/BUILD @@ -28,6 +28,7 @@ proto_library( "//envoy/config/filter/dubbo/router/v2alpha1:pkg", "//envoy/config/filter/fault/v2:pkg", "//envoy/config/filter/http/adaptive_concurrency/v2alpha:pkg", + "//envoy/config/filter/http/aws_lambda/v2alpha:pkg", "//envoy/config/filter/http/aws_request_signing/v2alpha:pkg", "//envoy/config/filter/http/buffer/v2:pkg", "//envoy/config/filter/http/cache/v2alpha:pkg", @@ -160,6 +161,7 @@ proto_library( "//envoy/extensions/common/tap/v3:pkg", "//envoy/extensions/filters/common/fault/v3:pkg", "//envoy/extensions/filters/http/adaptive_concurrency/v3:pkg", + "//envoy/extensions/filters/http/aws_lambda/v3:pkg", "//envoy/extensions/filters/http/aws_request_signing/v3:pkg", "//envoy/extensions/filters/http/buffer/v3:pkg", "//envoy/extensions/filters/http/cache/v3alpha:pkg", diff --git a/api/docs/BUILD b/api/docs/BUILD index 222890e8b8f0..1a791f147502 100644 --- a/api/docs/BUILD +++ b/api/docs/BUILD @@ -34,6 +34,7 @@ proto_library( "//envoy/config/filter/dubbo/router/v2alpha1:pkg", "//envoy/config/filter/fault/v2:pkg", "//envoy/config/filter/http/adaptive_concurrency/v2alpha:pkg", + "//envoy/config/filter/http/aws_lambda/v2alpha:pkg", "//envoy/config/filter/http/aws_request_signing/v2alpha:pkg", "//envoy/config/filter/http/buffer/v2:pkg", "//envoy/config/filter/http/cache/v2alpha:pkg", diff --git a/api/envoy/config/filter/http/aws_lambda/v2alpha/BUILD b/api/envoy/config/filter/http/aws_lambda/v2alpha/BUILD new file mode 100644 index 000000000000..ef3541ebcb1d --- /dev/null +++ b/api/envoy/config/filter/http/aws_lambda/v2alpha/BUILD @@ -0,0 +1,9 @@ +# DO NOT EDIT. This file is generated by tools/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = ["@com_github_cncf_udpa//udpa/annotations:pkg"], +) diff --git a/api/envoy/config/filter/http/aws_lambda/v2alpha/aws_lambda.proto b/api/envoy/config/filter/http/aws_lambda/v2alpha/aws_lambda.proto new file mode 100644 index 000000000000..cd9e1d30e887 --- /dev/null +++ b/api/envoy/config/filter/http/aws_lambda/v2alpha/aws_lambda.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package envoy.config.filter.http.aws_lambda.v2alpha; + +import "udpa/annotations/status.proto"; + +import "udpa/annotations/migrate.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.config.filter.http.aws_lambda.v2alpha"; +option java_outer_classname = "AwsLambdaProto"; +option java_multiple_files = true; +option (udpa.annotations.file_migrate).move_to_package = + "envoy.extensions.filters.http.aws_lambda.v3"; +option (udpa.annotations.file_status).work_in_progress = true; + +// [#protodoc-title: AWS Lambda] +// AWS Lambda :ref:`configuration overview `. +// [#extension: envoy.filters.http.aws_lambda] + +// AWS Lambda filter config +message Config { + // The ARN of the AWS Lambda to invoke when the filter is engaged + // Must be in the following format: + // arn::lambda:::function: + string arn = 1 [(validate.rules).string = {min_len: 1}]; + + // Whether to transform the request (headers and body) to a JSON payload or pass it as is. + bool payload_passthrough = 2; +} + +// Per-route configuration for AWS Lambda. This can be useful when invoking a different Lambda function or a different +// version of the same Lambda depending on the route. +message PerRouteConfig { + Config invoke_config = 1; +} diff --git a/api/envoy/extensions/filters/http/aws_lambda/v3/BUILD b/api/envoy/extensions/filters/http/aws_lambda/v3/BUILD new file mode 100644 index 000000000000..00c1431346c2 --- /dev/null +++ b/api/envoy/extensions/filters/http/aws_lambda/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/filter/http/aws_lambda/v2alpha:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/filters/http/aws_lambda/v3/aws_lambda.proto b/api/envoy/extensions/filters/http/aws_lambda/v3/aws_lambda.proto new file mode 100644 index 000000000000..f41639d9b5b1 --- /dev/null +++ b/api/envoy/extensions/filters/http/aws_lambda/v3/aws_lambda.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.aws_lambda.v3; + +import "udpa/annotations/versioning.proto"; + +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.aws_lambda.v3"; +option java_outer_classname = "AwsLambdaProto"; +option java_multiple_files = true; + +// [#protodoc-title: AWS Lambda] +// AWS Lambda :ref:`configuration overview `. +// [#extension: envoy.filters.http.aws_lambda] + +// AWS Lambda filter config +message Config { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.http.aws_lambda.v2alpha.Config"; + + // The ARN of the AWS Lambda to invoke when the filter is engaged + // Must be in the following format: + // arn::lambda:::function: + string arn = 1 [(validate.rules).string = {min_len: 1}]; + + // Whether to transform the request (headers and body) to a JSON payload or pass it as is. + bool payload_passthrough = 2; +} + +// Per-route configuration for AWS Lambda. This can be useful when invoking a different Lambda function or a different +// version of the same Lambda depending on the route. +message PerRouteConfig { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.http.aws_lambda.v2alpha.PerRouteConfig"; + + Config invoke_config = 1; +} diff --git a/docs/root/configuration/http/http_filters/aws_lambda_filter.rst b/docs/root/configuration/http/http_filters/aws_lambda_filter.rst new file mode 100644 index 000000000000..1e59f96a4e82 --- /dev/null +++ b/docs/root/configuration/http/http_filters/aws_lambda_filter.rst @@ -0,0 +1,116 @@ + +.. _config_http_filters_aws_lambda: + +AWS Lambda +========== + +* :ref:`v2 API reference ` +* This filter should be configured with the name *envoy.filters.http.aws_lambda*. + +.. attention:: + + The AWS Lambda filter is currently under active development. + +The HTTP AWS Lambda filter is used to trigger an AWS Lambda function from a standard HTTP/1.x or HTTP/2 request. +It supports a few options to control whether to pass through the HTTP request payload as is or to wrap it in a JSON +schema. + +If :ref:`payload_passthrough ` is set to +``true``, then the payload is sent to Lambda without any transformations. +*Note*: This means you lose access to all the HTTP headers in the Lambda function. + +However, if :ref:`payload_passthrough ` +is set to ``false``, then the HTTP request is transformed to a JSON (the details of the JSON transformation will be +documented once that feature is implemented). + +The filter supports :ref:`per-filter configuration +`. +Below are some examples the show how the filter can be used in different deployment scenarios. + +Example configuration +--------------------- + +In this configuration, the filter applies to all routes in the filter chain of the http connection manager: + +.. code-block:: yaml + + http_filters: + - name: envoy.filters.http.aws_lambda + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.aws_lambda.v3.Config + arn: "arn:aws:lambda:us-west-2:987654321:function:hello_envoy" + payload_passthrough: true + +The corresponding regional endpoint must be specified in the target cluster. So, for example if the Lambda function is +in us-west-2: + +.. code-block:: yaml + + clusters: + - name: lambda_egress_gateway + connect_timeout: 0.25s + type: LOGICAL_DNS + dns_lookup_family: V4_ONLY + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: lambda_egress_gateway + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: lambda.us-west-2.amazonaws.com + port_value: 443 + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext + sni: "*.amazonaws.com" + + +The filter can also be configured per virtual-host, route or weighted-cluster. In that case, the target cluster *must* +have specific Lambda metadata. + +.. code-block:: yaml + + weighted_clusters: + clusters: + - name: lambda_egress_gateway + weight: 42 + typed_per_filter_config: + envoy.filters.http.aws_lambda: + "@type": type.googleapis.com/envoy.extensions.filters.http.aws_lambda.v3.PerRouteConfig + invoke_config: + arn: "arn:aws:lambda:us-west-2:987654321:function:hello_envoy" + payload_passthrough: false + + +An example with the Lambda metadata applied to a weighted-cluster: + +.. code-block:: yaml + + clusters: + - name: lambda_egress_gateway + connect_timeout: 0.25s + type: LOGICAL_DNS + dns_lookup_family: V4_ONLY + lb_policy: ROUND_ROBIN + metadata: + filter_metadata: + com.amazonaws.lambda: + egress_gateway: true + load_assignment: + cluster_name: lambda_egress_gateway # does this have to match? seems redundant + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: lambda.us-west-2.amazonaws.com + port_value: 443 + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext + sni: "*.amazonaws.com" + diff --git a/docs/root/configuration/http/http_filters/http_filters.rst b/docs/root/configuration/http/http_filters/http_filters.rst index f70580ee4e3d..aa435d6d5a9f 100644 --- a/docs/root/configuration/http/http_filters/http_filters.rst +++ b/docs/root/configuration/http/http_filters/http_filters.rst @@ -7,6 +7,7 @@ HTTP filters :maxdepth: 2 adaptive_concurrency_filter + aws_lambda_filter aws_request_signing_filter buffer_filter cors_filter diff --git a/generated_api_shadow/envoy/config/filter/http/aws_lambda/v2alpha/BUILD b/generated_api_shadow/envoy/config/filter/http/aws_lambda/v2alpha/BUILD new file mode 100644 index 000000000000..ef3541ebcb1d --- /dev/null +++ b/generated_api_shadow/envoy/config/filter/http/aws_lambda/v2alpha/BUILD @@ -0,0 +1,9 @@ +# DO NOT EDIT. This file is generated by tools/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = ["@com_github_cncf_udpa//udpa/annotations:pkg"], +) diff --git a/generated_api_shadow/envoy/config/filter/http/aws_lambda/v2alpha/aws_lambda.proto b/generated_api_shadow/envoy/config/filter/http/aws_lambda/v2alpha/aws_lambda.proto new file mode 100644 index 000000000000..cd9e1d30e887 --- /dev/null +++ b/generated_api_shadow/envoy/config/filter/http/aws_lambda/v2alpha/aws_lambda.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package envoy.config.filter.http.aws_lambda.v2alpha; + +import "udpa/annotations/status.proto"; + +import "udpa/annotations/migrate.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.config.filter.http.aws_lambda.v2alpha"; +option java_outer_classname = "AwsLambdaProto"; +option java_multiple_files = true; +option (udpa.annotations.file_migrate).move_to_package = + "envoy.extensions.filters.http.aws_lambda.v3"; +option (udpa.annotations.file_status).work_in_progress = true; + +// [#protodoc-title: AWS Lambda] +// AWS Lambda :ref:`configuration overview `. +// [#extension: envoy.filters.http.aws_lambda] + +// AWS Lambda filter config +message Config { + // The ARN of the AWS Lambda to invoke when the filter is engaged + // Must be in the following format: + // arn::lambda:::function: + string arn = 1 [(validate.rules).string = {min_len: 1}]; + + // Whether to transform the request (headers and body) to a JSON payload or pass it as is. + bool payload_passthrough = 2; +} + +// Per-route configuration for AWS Lambda. This can be useful when invoking a different Lambda function or a different +// version of the same Lambda depending on the route. +message PerRouteConfig { + Config invoke_config = 1; +} diff --git a/generated_api_shadow/envoy/extensions/filters/http/aws_lambda/v3/BUILD b/generated_api_shadow/envoy/extensions/filters/http/aws_lambda/v3/BUILD new file mode 100644 index 000000000000..00c1431346c2 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/http/aws_lambda/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/filter/http/aws_lambda/v2alpha:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/extensions/filters/http/aws_lambda/v3/aws_lambda.proto b/generated_api_shadow/envoy/extensions/filters/http/aws_lambda/v3/aws_lambda.proto new file mode 100644 index 000000000000..f41639d9b5b1 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/http/aws_lambda/v3/aws_lambda.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.aws_lambda.v3; + +import "udpa/annotations/versioning.proto"; + +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.aws_lambda.v3"; +option java_outer_classname = "AwsLambdaProto"; +option java_multiple_files = true; + +// [#protodoc-title: AWS Lambda] +// AWS Lambda :ref:`configuration overview `. +// [#extension: envoy.filters.http.aws_lambda] + +// AWS Lambda filter config +message Config { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.http.aws_lambda.v2alpha.Config"; + + // The ARN of the AWS Lambda to invoke when the filter is engaged + // Must be in the following format: + // arn::lambda:::function: + string arn = 1 [(validate.rules).string = {min_len: 1}]; + + // Whether to transform the request (headers and body) to a JSON payload or pass it as is. + bool payload_passthrough = 2; +} + +// Per-route configuration for AWS Lambda. This can be useful when invoking a different Lambda function or a different +// version of the same Lambda depending on the route. +message PerRouteConfig { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.http.aws_lambda.v2alpha.PerRouteConfig"; + + Config invoke_config = 1; +} diff --git a/source/extensions/common/aws/signer.h b/source/extensions/common/aws/signer.h index 02514833cd05..34a892c9efb2 100644 --- a/source/extensions/common/aws/signer.h +++ b/source/extensions/common/aws/signer.h @@ -26,6 +26,14 @@ class Signer { * @throws EnvoyException if the request cannot be signed. */ virtual void sign(Http::RequestHeaderMap& headers) PURE; + + /** + * Sign an AWS request. + * @param headers AWS API request headers. + * @param content_hash The Hex encoded SHA-256 of the body of the AWS API request. + * @throws EnvoyException if the request cannot be signed. + */ + virtual void sign(Http::RequestHeaderMap& headers, const std::string& content_hash) PURE; }; using SignerPtr = std::unique_ptr; diff --git a/source/extensions/common/aws/signer_impl.cc b/source/extensions/common/aws/signer_impl.cc index 4992ddee8707..da5256e25ed1 100644 --- a/source/extensions/common/aws/signer_impl.cc +++ b/source/extensions/common/aws/signer_impl.cc @@ -37,6 +37,7 @@ void SignerImpl::sign(Http::RequestHeaderMap& headers) { } void SignerImpl::sign(Http::RequestHeaderMap& headers, const std::string& content_hash) { + headers.setReferenceKey(SignatureHeaders::get().ContentSha256, content_hash); const auto& credentials = credentials_provider_->getCredentials(); if (!credentials.accessKeyId() || !credentials.secretAccessKey()) { // Empty or "anonymous" credentials are a valid use-case for non-production environments. @@ -85,7 +86,6 @@ std::string SignerImpl::createContentHash(Http::RequestMessage& message, bool si const auto content_hash = message.body() ? Hex::encode(crypto_util.getSha256Digest(*message.body())) : SignatureConstants::get().HashedEmptyString; - message.headers().addCopy(SignatureHeaders::get().ContentSha256, content_hash); return content_hash; } diff --git a/source/extensions/common/aws/signer_impl.h b/source/extensions/common/aws/signer_impl.h index 9fe3be041480..f925b6046b9d 100644 --- a/source/extensions/common/aws/signer_impl.h +++ b/source/extensions/common/aws/signer_impl.h @@ -70,7 +70,7 @@ class SignerImpl : public Signer, public Logger::Loggable { const std::map& canonical_headers, absl::string_view signature) const; - void sign(Http::RequestHeaderMap& headers, const std::string& content_hash); + void sign(Http::RequestHeaderMap& headers, const std::string& content_hash) override; const std::string service_name_; const std::string region_; diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 89beac404cb8..f6ae6599079e 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -34,6 +34,7 @@ EXTENSIONS = { # "envoy.filters.http.adaptive_concurrency": "//source/extensions/filters/http/adaptive_concurrency:config", + "envoy.filters.http.aws_lambda": "//source/extensions/filters/http/aws_lambda:config", "envoy.filters.http.aws_request_signing": "//source/extensions/filters/http/aws_request_signing:config", "envoy.filters.http.buffer": "//source/extensions/filters/http/buffer:config", "envoy.filters.http.cache": "//source/extensions/filters/http/cache:config", diff --git a/source/extensions/filters/http/aws_lambda/BUILD b/source/extensions/filters/http/aws_lambda/BUILD new file mode 100644 index 000000000000..b7c67308ce2d --- /dev/null +++ b/source/extensions/filters/http/aws_lambda/BUILD @@ -0,0 +1,41 @@ +licenses(["notice"]) # Apache 2 + +# L7 HTTP AWS Lambda filter +# Public docs: docs/root/configuration/http_filters/aws_lambda_filter.rst + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_library( + name = "aws_lambda_filter_lib", + srcs = ["aws_lambda_filter.cc"], + hdrs = ["aws_lambda_filter.h"], + deps = [ + "//include/envoy/http:filter_interface", + "//source/extensions/common/aws:credentials_provider_impl_lib", + "//source/extensions/common/aws:signer_impl_lib", + "//source/extensions/filters/http:well_known_names", + "//source/extensions/filters/http/common:pass_through_filter_lib", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + security_posture = "requires_trusted_downstream_and_upstream", + status = "alpha", + deps = [ + ":aws_lambda_filter_lib", + "//include/envoy/registry", + "//source/extensions/filters/http:well_known_names", + "//source/extensions/filters/http/common:factory_base_lib", + "@envoy_api//envoy/extensions/filters/http/aws_lambda/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/http/aws_lambda/aws_lambda_filter.cc b/source/extensions/filters/http/aws_lambda/aws_lambda_filter.cc new file mode 100644 index 000000000000..d90e5a410567 --- /dev/null +++ b/source/extensions/filters/http/aws_lambda/aws_lambda_filter.cc @@ -0,0 +1,162 @@ +#include "extensions/filters/http/aws_lambda/aws_lambda_filter.h" + +#include +#include + +#include "envoy/upstream/upstream.h" + +#include "common/common/fmt.h" +#include "common/common/hex.h" +#include "common/crypto/utility.h" +#include "common/http/headers.h" +#include "common/protobuf/utility.h" + +#include "extensions/filters/http/well_known_names.h" + +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AwsLambdaFilter { + +namespace { + +constexpr auto filter_metadata_key = "com.amazonaws.lambda"; +constexpr auto egress_gateway_metadata_key = "egress_gateway"; + +void setHeaders(Http::RequestHeaderMap& headers, absl::string_view function_name) { + headers.setMethod(Http::Headers::get().MethodValues.Post); + headers.setPath(fmt::format("/2015-03-31/functions/{}/invocations", function_name)); + headers.setCopy(Http::LowerCaseString{"x-amz-invocation-type"}, "RequestResponse"); +} + +/** + * Determines if the target cluster has the AWS Lambda metadata on it. + */ +bool isTargetClusterLambdaGateway(Upstream::ClusterInfo const& cluster_info) { + using ProtobufWkt::Value; + const auto& filter_metadata_map = cluster_info.metadata().filter_metadata(); + auto metadata_it = filter_metadata_map.find(filter_metadata_key); + if (metadata_it == filter_metadata_map.end()) { + return false; + } + + auto egress_gateway_it = metadata_it->second.fields().find(egress_gateway_metadata_key); + if (egress_gateway_it == metadata_it->second.fields().end()) { + return false; + } + + if (egress_gateway_it->second.kind_case() != Value::KindCase::kBoolValue) { + return false; + } + + return egress_gateway_it->second.bool_value(); +} + +} // namespace + +Filter::Filter(const FilterSettings& settings, + const std::shared_ptr& sigv4_signer) + : settings_(settings), sigv4_signer_(sigv4_signer) {} + +absl::optional Filter::calculateRouteArn() { + if (!decoder_callbacks_->route() || !decoder_callbacks_->route()->routeEntry()) { + return absl::nullopt; + } + const auto* route_entry = decoder_callbacks_->route()->routeEntry(); + const auto* settings = route_entry->mostSpecificPerFilterConfigTyped( + HttpFilterNames::get().AwsLambda); + if (!settings) { + return absl::nullopt; + } + + return parseArn(settings->arn()); +} + +Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, bool end_stream) { + if (!settings_.payloadPassthrough()) { + skip_ = true; + return Http::FilterHeadersStatus::Continue; + } + + auto route_arn = calculateRouteArn(); + if (route_arn.has_value()) { + auto cluster_info_ptr = decoder_callbacks_->clusterInfo(); + ASSERT(cluster_info_ptr); + if (!isTargetClusterLambdaGateway(*cluster_info_ptr)) { + skip_ = true; + return Http::FilterHeadersStatus::Continue; + } + arn_.swap(route_arn); + } else { + arn_ = parseArn(settings_.arn()); + if (!arn_.has_value()) { + ENVOY_LOG(error, "Unable to parse Lambda ARN {}.", settings_.arn()); + skip_ = true; + return Http::FilterHeadersStatus::Continue; + } + } + + if (end_stream) { + setHeaders(headers, arn_->functionName()); + sigv4_signer_->sign(headers); + return Http::FilterHeadersStatus::Continue; + } + + headers_ = &headers; + return Http::FilterHeadersStatus::StopIteration; +} + +Http::FilterDataStatus Filter::decodeData(Buffer::Instance& data, bool end_stream) { + UNREFERENCED_PARAMETER(data); + if (skip_) { + return Http::FilterDataStatus::Continue; + } + + if (end_stream) { + setHeaders(*headers_, arn_->functionName()); + auto& hashing_util = Envoy::Common::Crypto::UtilitySingleton::get(); + const Buffer::Instance& decoding_buffer = *decoder_callbacks_->decodingBuffer(); + const auto hash = Hex::encode(hashing_util.getSha256Digest(decoding_buffer)); + sigv4_signer_->sign(*headers_, hash); + return Http::FilterDataStatus::Continue; + } + return Http::FilterDataStatus::StopIterationAndBuffer; +} + +absl::optional parseArn(absl::string_view arn) { + const std::vector parts = absl::StrSplit(arn, ':'); + constexpr auto min_arn_size = 7; + if (parts.size() < min_arn_size) { + return absl::nullopt; + } + + if (parts[0] != "arn") { + return absl::nullopt; + } + + auto partition = parts[1]; + auto service = parts[2]; + auto region = parts[3]; + auto account_id = parts[4]; + auto resource_type = parts[5]; + auto function_name = parts[6]; + + // If the ARN contains a function version/alias, then we want it to be part of the function name. + // For example: + // arn:aws:lambda:us-west-2:987654321:function:hello_envoy:v1 + if (parts.size() > min_arn_size) { + std::string versioned_function_name = std::string(function_name); + versioned_function_name.push_back(':'); + versioned_function_name += std::string(parts[7]); + return Arn{partition, service, region, account_id, resource_type, versioned_function_name}; + } + + return Arn{partition, service, region, account_id, resource_type, function_name}; +} + +} // namespace AwsLambdaFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/aws_lambda/aws_lambda_filter.h b/source/extensions/filters/http/aws_lambda/aws_lambda_filter.h new file mode 100644 index 000000000000..c59016480bd0 --- /dev/null +++ b/source/extensions/filters/http/aws_lambda/aws_lambda_filter.h @@ -0,0 +1,92 @@ +#pragma once + +#include + +#include "envoy/http/filter.h" + +#include "extensions/common/aws/signer.h" +#include "extensions/filters/http/common/pass_through_filter.h" + +#include "absl/strings/str_split.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AwsLambdaFilter { + +class Arn { +public: + Arn(absl::string_view partition, absl::string_view service, absl::string_view region, + absl::string_view account_id, absl::string_view resource_type, + absl::string_view function_name) + : partition_(partition), service_(service), region_(region), account_id_(account_id), + resource_type_(resource_type), function_name_(function_name) {} + + const std::string& partition() const { return partition_; } + const std::string& service() const { return service_; } + const std::string& region() const { return region_; } + const std::string& accountId() const { return account_id_; } + const std::string& resourceType() const { return resource_type_; } + const std::string& functionName() const { return function_name_; } + +private: + std::string partition_; + std::string service_; + std::string region_; + std::string account_id_; + std::string resource_type_; + std::string function_name_; // resource_id +}; + +/** + * Parses the input string into a structured ARN. + * + * The format is expected to be as such: + * arn:partition:service:region:account-id:resource-type:resource-id + * + * Lambda ARN Example: + * arn:aws:lambda:us-west-2:987654321:function:hello_envoy + */ +absl::optional parseArn(absl::string_view arn); + +class FilterSettings : public Router::RouteSpecificFilterConfig { +public: + FilterSettings(const std::string& arn, bool payload_passthrough) + : arn_(arn), payload_passthrough_(payload_passthrough) {} + + const std::string& arn() const { return arn_; } + bool payloadPassthrough() const { return payload_passthrough_; } + +private: + std::string arn_; + bool payload_passthrough_; +}; + +class Filter : public Http::PassThroughFilter, Logger::Loggable { + +public: + Filter(const FilterSettings& config, + const std::shared_ptr& sigv4_signer); + + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, + bool end_stream) override; + Http::FilterDataStatus decodeData(Buffer::Instance& data, bool end_stream) override; + +private: + /** + * Calculates the route specific Lambda ARN if any. + */ + absl::optional calculateRouteArn(); + const FilterSettings settings_; + Http::RequestHeaderMap* headers_ = nullptr; + std::shared_ptr sigv4_signer_; + absl::optional arn_; + bool skip_ = false; +}; + +} // namespace AwsLambdaFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/aws_lambda/config.cc b/source/extensions/filters/http/aws_lambda/config.cc new file mode 100644 index 000000000000..058fe9f1bede --- /dev/null +++ b/source/extensions/filters/http/aws_lambda/config.cc @@ -0,0 +1,63 @@ +#include "extensions/filters/http/aws_lambda/config.h" + +#include "envoy/extensions/filters/http/aws_lambda/v3/aws_lambda.pb.validate.h" +#include "envoy/registry/registry.h" + +#include "common/common/fmt.h" + +#include "extensions/common/aws/credentials_provider_impl.h" +#include "extensions/common/aws/signer_impl.h" +#include "extensions/common/aws/utility.h" +#include "extensions/filters/http/aws_lambda/aws_lambda_filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AwsLambdaFilter { +constexpr auto service_name = "lambda"; +namespace { +std::string extractRegionFromArn(absl::string_view arn) { + auto parsed_arn = parseArn(arn); + if (parsed_arn.has_value()) { + return parsed_arn->region(); + } + throw EnvoyException(fmt::format("Invalid ARN: {}", arn)); +} +} // namespace + +Http::FilterFactoryCb AwsLambdaFilterFactory::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::aws_lambda::v3::Config& proto_config, + const std::string&, Server::Configuration::FactoryContext& context) { + + auto credentials_provider = + std::make_shared( + context.api(), Extensions::Common::Aws::Utility::metadataFetcher); + + const std::string region = extractRegionFromArn(proto_config.arn()); + auto signer = std::make_shared( + service_name, region, std::move(credentials_provider), context.dispatcher().timeSource()); + + FilterSettings filter_settings{proto_config.arn(), proto_config.payload_passthrough()}; + + return [signer, filter_settings](Http::FilterChainFactoryCallbacks& cb) { + auto filter = std::make_shared(filter_settings, signer); + cb.addStreamFilter(filter); + }; +} + +Router::RouteSpecificFilterConfigConstSharedPtr +AwsLambdaFilterFactory::createRouteSpecificFilterConfigTyped( + const envoy::extensions::filters::http::aws_lambda::v3::PerRouteConfig& proto_config, + Server::Configuration::ServerFactoryContext&, ProtobufMessage::ValidationVisitor&) { + return std::make_shared(FilterSettings{ + proto_config.invoke_config().arn(), proto_config.invoke_config().payload_passthrough()}); +} +/* + * Static registration for the AWS Lambda filter. @see RegisterFactory. + */ +REGISTER_FACTORY(AwsLambdaFilterFactory, Server::Configuration::NamedHttpFilterConfigFactory); + +} // namespace AwsLambdaFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/aws_lambda/config.h b/source/extensions/filters/http/aws_lambda/config.h new file mode 100644 index 000000000000..fbdfb26ff9e7 --- /dev/null +++ b/source/extensions/filters/http/aws_lambda/config.h @@ -0,0 +1,33 @@ +#pragma once + +#include "envoy/extensions/filters/http/aws_lambda/v3/aws_lambda.pb.h" +#include "envoy/extensions/filters/http/aws_lambda/v3/aws_lambda.pb.validate.h" + +#include "extensions/filters/http/common/factory_base.h" +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AwsLambdaFilter { + +class AwsLambdaFilterFactory + : public Common::FactoryBase { +public: + AwsLambdaFilterFactory() : FactoryBase(HttpFilterNames::get().AwsLambda) {} + +private: + Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::aws_lambda::v3::Config& proto_config, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override; + + Router::RouteSpecificFilterConfigConstSharedPtr createRouteSpecificFilterConfigTyped( + const envoy::extensions::filters::http::aws_lambda::v3::PerRouteConfig&, + Server::Configuration::ServerFactoryContext&, ProtobufMessage::ValidationVisitor&) override; +}; + +} // namespace AwsLambdaFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/well_known_names.h b/source/extensions/filters/http/well_known_names.h index e21b0698b7dc..68bc5c361be4 100644 --- a/source/extensions/filters/http/well_known_names.h +++ b/source/extensions/filters/http/well_known_names.h @@ -68,6 +68,8 @@ class HttpFilterNameValues { const std::string DynamicForwardProxy = "envoy.filters.http.dynamic_forward_proxy"; // AWS request signing filter const std::string AwsRequestSigning = "envoy.filters.http.aws_request_signing"; + // AWS Lambda filter + const std::string AwsLambda = "envoy.filters.http.aws_lambda"; }; using HttpFilterNames = ConstSingleton; diff --git a/test/extensions/common/aws/mocks.h b/test/extensions/common/aws/mocks.h index ecdcd8827bff..d89e1934c6d9 100644 --- a/test/extensions/common/aws/mocks.h +++ b/test/extensions/common/aws/mocks.h @@ -25,6 +25,7 @@ class MockSigner : public Signer { MOCK_METHOD(void, sign, (Http::RequestMessage&, bool)); MOCK_METHOD(void, sign, (Http::RequestHeaderMap&)); + MOCK_METHOD(void, sign, (Http::RequestHeaderMap&, const std::string&)); }; class MockMetadataFetcher { diff --git a/test/extensions/common/aws/signer_impl_test.cc b/test/extensions/common/aws/signer_impl_test.cc index 4b5f4c0d56a8..31ed9f7cbd9d 100644 --- a/test/extensions/common/aws/signer_impl_test.cc +++ b/test/extensions/common/aws/signer_impl_test.cc @@ -80,12 +80,12 @@ TEST_F(SignerImplTest, SignDateHeader) { addMethod("GET"); addPath("/"); signer_.sign(*message_); - EXPECT_EQ(nullptr, message_->headers().get(SignatureHeaders::get().ContentSha256)); + EXPECT_NE(nullptr, message_->headers().get(SignatureHeaders::get().ContentSha256)); EXPECT_EQ("20180102T030400Z", message_->headers().get(SignatureHeaders::get().Date)->value().getStringView()); EXPECT_EQ("AWS4-HMAC-SHA256 Credential=akid/20180102/region/service/aws4_request, " - "SignedHeaders=x-amz-date, " - "Signature=1310784f67248cab70d98b9404d601f30d8fe20bd1820560cce224f4131dc1cc", + "SignedHeaders=x-amz-content-sha256;x-amz-date, " + "Signature=4ee6aa9355259c18133f150b139ea9aeb7969c9408ad361b2151f50a516afe42", message_->headers().Authorization()->value().getStringView()); } @@ -99,8 +99,8 @@ TEST_F(SignerImplTest, SignSecurityTokenHeader) { "token", message_->headers().get(SignatureHeaders::get().SecurityToken)->value().getStringView()); EXPECT_EQ("AWS4-HMAC-SHA256 Credential=akid/20180102/region/service/aws4_request, " - "SignedHeaders=x-amz-date;x-amz-security-token, " - "Signature=ff1d9fa7e54a72677b5336df047bb1f1493f86b92099973bf62da3af852d1679", + "SignedHeaders=x-amz-content-sha256;x-amz-date;x-amz-security-token, " + "Signature=1d42526aabf7d8b6d7d33d9db43b03537300cc7e6bb2817e349749e0a08f5b5e", message_->headers().Authorization()->value().getStringView()); } @@ -145,8 +145,8 @@ TEST_F(SignerImplTest, SignExtraHeaders) { addHeader("c", "c_value"); signer_.sign(*message_); EXPECT_EQ("AWS4-HMAC-SHA256 Credential=akid/20180102/region/service/aws4_request, " - "SignedHeaders=a;b;c;x-amz-date, " - "Signature=d5e025e1cf0d5af0d83110bc2ef1cafd2d9dca1dea9d7767f58308da64aa6558", + "SignedHeaders=a;b;c;x-amz-content-sha256;x-amz-date, " + "Signature=0940025fcecfef5d7ee30e0a26a0957e116560e374878cd86ef4316c53ae9e81", message_->headers().Authorization()->value().getStringView()); } @@ -158,8 +158,8 @@ TEST_F(SignerImplTest, SignHostHeader) { addHeader("host", "www.example.com"); signer_.sign(*message_); EXPECT_EQ("AWS4-HMAC-SHA256 Credential=akid/20180102/region/service/aws4_request, " - "SignedHeaders=host;x-amz-date, " - "Signature=60216ee44dd651322ea10cc6747308dd30e582aaa773f6c1b1354e486385c021", + "SignedHeaders=host;x-amz-content-sha256;x-amz-date, " + "Signature=d9fd9be575a254c924d843964b063d770181d938ae818f5b603ef0575a5ce2cd", message_->headers().Authorization()->value().getStringView()); } diff --git a/test/extensions/filters/http/aws_lambda/BUILD b/test/extensions/filters/http/aws_lambda/BUILD new file mode 100644 index 000000000000..fdcc88cbe5ef --- /dev/null +++ b/test/extensions/filters/http/aws_lambda/BUILD @@ -0,0 +1,45 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +envoy_package() + +envoy_extension_cc_test( + name = "aws_lambda_filter_test", + srcs = ["aws_lambda_filter_test.cc"], + extension_name = "envoy.filters.http.aws_lambda", + deps = [ + "//source/extensions/filters/http/aws_lambda:aws_lambda_filter_lib", + "//test/extensions/common/aws:aws_mocks", + "//test/mocks/http:http_mocks", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "arn_test", + srcs = ["arn_test.cc"], + extension_name = "envoy.filters.http.aws_lambda", + deps = [ + "//source/extensions/filters/http/aws_lambda:aws_lambda_filter_lib", + "//test/mocks/http:http_mocks", + ], +) + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_name = "envoy.filters.http.aws_lambda", + deps = [ + "//source/extensions/filters/http/aws_lambda:config", + "//test/mocks/server:server_mocks", + "@envoy_api//envoy/extensions/filters/http/aws_lambda/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/filters/http/aws_lambda/arn_test.cc b/test/extensions/filters/http/aws_lambda/arn_test.cc new file mode 100644 index 000000000000..8f20f9af7767 --- /dev/null +++ b/test/extensions/filters/http/aws_lambda/arn_test.cc @@ -0,0 +1,48 @@ +#include "extensions/filters/http/aws_lambda/aws_lambda_filter.h" + +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AwsLambdaFilter { + +namespace { + +TEST(AwsArn, ValidArn) { + constexpr auto input_arn = "arn:aws:lambda:us-west-2:1337:function:fun"; + const absl::optional arn = parseArn(input_arn); + ASSERT_TRUE(arn.has_value()); + EXPECT_STREQ("aws", arn->partition().c_str()); + EXPECT_STREQ("lambda", arn->service().c_str()); + EXPECT_STREQ("us-west-2", arn->region().c_str()); + EXPECT_STREQ("1337", arn->accountId().c_str()); + EXPECT_STREQ("function", arn->resourceType().c_str()); + EXPECT_STREQ("fun", arn->functionName().c_str()); +} + +TEST(AwsArn, ValidArnWithVersion) { + constexpr auto input_arn = "arn:aws:lambda:us-west-2:1337:function:fun:v2"; + const absl::optional arn = parseArn(input_arn); + ASSERT_TRUE(arn.has_value()); + EXPECT_STREQ("aws", arn->partition().c_str()); + EXPECT_STREQ("lambda", arn->service().c_str()); + EXPECT_STREQ("us-west-2", arn->region().c_str()); + EXPECT_STREQ("1337", arn->accountId().c_str()); + EXPECT_STREQ("function", arn->resourceType().c_str()); + EXPECT_STREQ("fun:v2", arn->functionName().c_str()); +} + +TEST(AwsArn, InvalidArn) { + constexpr auto input_arn = "arn:aws:lambda:us-west-2:1337:function"; + const absl::optional arn = parseArn(input_arn); + EXPECT_EQ(absl::nullopt, arn); +} + +} // namespace +} // namespace AwsLambdaFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy 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 new file mode 100644 index 000000000000..fc390631b742 --- /dev/null +++ b/test/extensions/filters/http/aws_lambda/aws_lambda_filter_test.cc @@ -0,0 +1,151 @@ +#include "envoy/config/core/v3/base.pb.h" + +#include "extensions/filters/http/aws_lambda/aws_lambda_filter.h" +#include "extensions/filters/http/well_known_names.h" + +#include "test/extensions/common/aws/mocks.h" +#include "test/mocks/http/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AwsLambdaFilter { + +namespace { + +using Common::Aws::MockSigner; +using ::testing::Invoke; +using ::testing::Return; +using ::testing::ReturnRef; + +constexpr auto Arn = "arn:aws:lambda:us-west-2:1337:function:fun"; +class AwsLambdaFilterTest : public ::testing::Test { +public: + void setupFilter(const FilterSettings& settings) { + signer_ = std::make_shared>(); + filter_ = std::make_unique(settings, signer_); + filter_->setDecoderFilterCallbacks(decoder_callbacks_); + } + + std::unique_ptr filter_; + std::shared_ptr> signer_; + NiceMock decoder_callbacks_; +}; + +/** + * Requests that are _not_ header only, should result in StopIteration. + */ +TEST_F(AwsLambdaFilterTest, DecodingHeaderStopIteration) { + setupFilter({Arn, true /*passthrough*/}); + Http::TestRequestHeaderMapImpl headers; + const auto result = filter_->decodeHeaders(headers, false /*end_stream*/); + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, result); +} + +/** + * Header only requests should be signed and Continue iteration. + * Also, if x-forwarded-proto header is found, it should be removed when signing. + */ +TEST_F(AwsLambdaFilterTest, HeaderOnlyShouldContinue) { + setupFilter({Arn, true /*passthrough*/}); + Http::TestRequestHeaderMapImpl input_headers{{":method", "GET"}, {"x-forwarded-proto", "http"}}; + const auto result = filter_->decodeHeaders(input_headers, true /*end_stream*/); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, result); +} + +/** + * If there's a per-route configuration and the target cluster does not have the AWS Lambda + * metadata, then we should skip the filter. + */ +TEST_F(AwsLambdaFilterTest, PerRouteConfigNoClusterMetadata) { + setupFilter({Arn, true /*passthrough*/}); + FilterSettings route_settings{Arn, true /*passthrough*/}; + ON_CALL(decoder_callbacks_.route_->route_entry_, + perFilterConfig(HttpFilterNames::get().AwsLambda)) + .WillByDefault(Return(&route_settings)); + Http::TestRequestHeaderMapImpl headers; + const auto result = filter_->decodeHeaders(headers, true /*end_stream*/); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, result); +} + +/** + * If there's a per route config and the target cluster has the _wrong_ metadata, then skip the + * filter. + */ +TEST_F(AwsLambdaFilterTest, PerRouteConfigWrongClusterMetadata) { + const std::string metadata_yaml = R"EOF( + egress_gateway: true + )EOF"; + + ProtobufWkt::Struct cluster_metadata; + envoy::config::core::v3::Metadata metadata; // What should this type be? + TestUtility::loadFromYaml(metadata_yaml, cluster_metadata); + metadata.mutable_filter_metadata()->insert({"WrongMetadataKey", cluster_metadata}); + + setupFilter({Arn, true /*passthrough*/}); + FilterSettings route_settings{Arn, true /*passthrough*/}; + ON_CALL(decoder_callbacks_.route_->route_entry_, + perFilterConfig(HttpFilterNames::get().AwsLambda)) + .WillByDefault(Return(&route_settings)); + + ON_CALL(*decoder_callbacks_.cluster_info_, metadata()).WillByDefault(ReturnRef(metadata)); + Http::TestRequestHeaderMapImpl headers; + const auto result = filter_->decodeHeaders(headers, false /*end_stream*/); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, result); +} + +/** + * If there's a per route config and the target cluster has the _correct_ metadata, then we should + * process the request (i.e. StopIteration if end_stream is false) + */ +TEST_F(AwsLambdaFilterTest, PerRouteConfigCorrectClusterMetadata) { + const std::string metadata_yaml = R"EOF( + egress_gateway: true + )EOF"; + + ProtobufWkt::Struct cluster_metadata; + envoy::config::core::v3::Metadata metadata; // What should this type be? + TestUtility::loadFromYaml(metadata_yaml, cluster_metadata); + metadata.mutable_filter_metadata()->insert({"com.amazonaws.lambda", cluster_metadata}); + + setupFilter({Arn, true /*passthrough*/}); + FilterSettings route_settings{Arn, true /*passthrough*/}; + ON_CALL(decoder_callbacks_.route_->route_entry_, + perFilterConfig(HttpFilterNames::get().AwsLambda)) + .WillByDefault(Return(&route_settings)); + + ON_CALL(*decoder_callbacks_.cluster_info_, metadata()).WillByDefault(ReturnRef(metadata)); + Http::TestRequestHeaderMapImpl headers; + const auto result = filter_->decodeHeaders(headers, false /*end_stream*/); + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, result); +} + +TEST_F(AwsLambdaFilterTest, DecodeDataShouldBuffer) { + setupFilter({Arn, true /*passthrough*/}); + Http::TestRequestHeaderMapImpl headers; + const auto header_result = filter_->decodeHeaders(headers, false /*end_stream*/); + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, header_result); + Buffer::OwnedImpl buffer; + const auto data_result = filter_->decodeData(buffer, false); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, data_result); +} + +TEST_F(AwsLambdaFilterTest, DecodeDataShouldSign) { + setupFilter({Arn, true /*passthrough*/}); + Http::TestRequestHeaderMapImpl headers; + const auto header_result = filter_->decodeHeaders(headers, false /*end_stream*/); + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, header_result); + Buffer::OwnedImpl buffer; + ON_CALL(decoder_callbacks_, decodingBuffer()).WillByDefault(Return(&buffer)); + const auto data_result = filter_->decodeData(buffer, true /*end_stream*/); + EXPECT_EQ(Http::FilterDataStatus::Continue, data_result); +} + +} // namespace +} // namespace AwsLambdaFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy 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 000000000000..9b8eb6098332 --- /dev/null +++ b/test/extensions/filters/http/aws_lambda/config_test.cc @@ -0,0 +1,62 @@ +#include "envoy/extensions/filters/http/aws_lambda/v3/aws_lambda.pb.h" +#include "envoy/extensions/filters/http/aws_lambda/v3/aws_lambda.pb.validate.h" + +#include "extensions/filters/http/aws_lambda/config.h" + +#include "test/mocks/server/mocks.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace AwsLambdaFilter { +namespace { + +using LambdaConfig = envoy::extensions::filters::http::aws_lambda::v3::Config; +using LambdaPerRouteConfig = envoy::extensions::filters::http::aws_lambda::v3::PerRouteConfig; + +TEST(AwsLambdaFilterConfigTest, ValidConfigCreatesFilter) { + const std::string yaml = R"EOF( +arn: "arn:aws:lambda:region:424242:function:fun" +payload_passthrough: true + )EOF"; + + LambdaConfig proto_config; + TestUtility::loadFromYamlAndValidate(yaml, proto_config); + + testing::NiceMock context; + AwsLambdaFilterFactory factory; + + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context); + Http::MockFilterChainFactoryCallbacks filter_callbacks; + EXPECT_CALL(filter_callbacks, addStreamFilter(_)); + cb(filter_callbacks); +} + +TEST(AwsLambdaFilterConfigTest, ValidPerRouteConfigCreatesFilter) { + const std::string yaml = R"EOF( + invoke_config: + arn: "arn:aws:lambda:region:424242:function:fun" + payload_passthrough: true + )EOF"; + + LambdaPerRouteConfig proto_config; + TestUtility::loadFromYamlAndValidate(yaml, proto_config); + + testing::NiceMock context; + AwsLambdaFilterFactory factory; + + auto route_specific_config_ptr = factory.createRouteSpecificFilterConfig( + proto_config, context, ProtobufMessage::getStrictValidationVisitor()); + Http::MockFilterChainFactoryCallbacks filter_callbacks; + EXPECT_NE(route_specific_config_ptr, nullptr); +} + +} // namespace +} // namespace AwsLambdaFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/grpc_credentials/aws_iam/aws_iam_grpc_credentials_test.cc b/test/extensions/grpc_credentials/aws_iam/aws_iam_grpc_credentials_test.cc index d700c8487084..8dcfd96b25f0 100644 --- a/test/extensions/grpc_credentials/aws_iam/aws_iam_grpc_credentials_test.cc +++ b/test/extensions/grpc_credentials/aws_iam/aws_iam_grpc_credentials_test.cc @@ -44,7 +44,7 @@ class GrpcAwsIamClientIntegrationTest : public GrpcSslClientIntegrationTest { EXPECT_TRUE(absl::StartsWith(auth_parts[1], "Credential=test_akid/")); EXPECT_TRUE(absl::EndsWith(auth_parts[1], fmt::format("{}/{}/aws4_request", region_name_, service_name_))); - EXPECT_EQ("SignedHeaders=host;x-amz-date", auth_parts[2]); + EXPECT_EQ("SignedHeaders=host;x-amz-content-sha256;x-amz-date", auth_parts[2]); // We don't verify correctness off the signature here, as this is part of the signer unit tests. EXPECT_TRUE(absl::StartsWith(auth_parts[3], "Signature=")); } diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index ff9096d07a07..24ba7e79890c 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -11,6 +11,7 @@ ALS AMZ APC API +ARN ASAN ASCII ASSERTs