From 3816b1a4788db91ab65d3c815af52108eed02943 Mon Sep 17 00:00:00 2001 From: Sunil Narasimhamurthy Date: Sat, 4 Nov 2023 07:38:11 -0700 Subject: [PATCH] aws: use http async client to fetch metadata credentials (#30626) Signed-off-by: Sunil Narasimhamurthy --- changelogs/current.yaml | 6 +- .../http_filters/_include/aws_credentials.rst | 10 +- source/common/runtime/runtime_features.cc | 3 + source/extensions/common/aws/BUILD | 5 + .../common/aws/credentials_provider.h | 2 + .../common/aws/credentials_provider_impl.cc | 313 +++- .../common/aws/credentials_provider_impl.h | 174 +- .../filters/http/aws_lambda/config.cc | 4 +- .../http/aws_request_signing/config.cc | 6 +- .../grpc_credentials/aws_iam/config.cc | 12 +- test/extensions/common/aws/BUILD | 2 + .../aws/credentials_provider_impl_test.cc | 1500 ++++++++++++++++- .../aws_lambda_filter_integration_test.cc | 1 + tools/spelling/spelling_dictionary.txt | 1 + 14 files changed, 1882 insertions(+), 157 deletions(-) diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 75d150ee9f81..235ff8a658cc 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -15,13 +15,17 @@ behavior_changes: ` extension becomes stable. minor_behavior_changes: +# *Changes that may cause incompatibilities for some users, but should not for most* +- area: aws + change: | + uses http async client to fetch the credentials from EC2 instance metadata and ECS task metadata providers instead of libcurl + which is deprecated. To revert this behavior set ``envoy.reloadable_features.use_libcurl_to_fetch_aws_credentials`` to true. - area: upstream change: | Fixed a reported issue (https://github.com/envoyproxy/envoy/issues/11004) that causes the Least Request load balancer policy to be unfair when the number of hosts are very small, when the number of hosts is smaller than the choice_count, instead of randomly selection hosts from the list, we perform a full scan on it to choose the host with least requests. -# *Changes that may cause incompatibilities for some users, but should not for most* - area: local_rate_limit change: | Added new configuration field :ref:`rate_limited_as_resource_exhausted diff --git a/docs/root/configuration/http/http_filters/_include/aws_credentials.rst b/docs/root/configuration/http/http_filters/_include/aws_credentials.rst index 83fd1aea5bd0..4821760d6308 100644 --- a/docs/root/configuration/http/http_filters/_include/aws_credentials.rst +++ b/docs/root/configuration/http/http_filters/_include/aws_credentials.rst @@ -13,4 +13,12 @@ secret access key (the session token is optional). 3. Either EC2 instance metadata or ECS task metadata. For EC2 instance metadata, the fields ``AccessKeyId``, ``SecretAccessKey``, and ``Token`` are used, and credentials are cached for 1 hour. For ECS task metadata, the fields ``AccessKeyId``, ``SecretAccessKey``, and - ``Token`` are used, and credentials are cached for 1 hour or until they expire (according to the field ``Expiration``). + ``Token`` are used, and credentials are cached for 1 hour or until they expire (according to the field ``Expiration``). Note that the + latest update on AWS credentials provider utility uses http async client functionality by default instead of libcurl to fetch the + credentials. The usage of libcurl is on the deprecation path and will be removed soon. This behavior can be changed by setting + ``envoy.reloadable_features.use_libcurl_to_fetch_aws_credentials`` to ``true``. To fetch the credentials from either EC2 instance + metadata or ECS task metadata a static cluster is required pointing towards the credentials provider. The static cluster name has to be + ``ec2_instance_metadata_server_internal`` for fetching from EC2 instance metadata or ``ecs_task_metadata_server_internal`` for fetching + from ECS task metadata. If these clusters are not provided in the bootstrap configuration then either of these will be added by default. + The static internal cluster will still be added even if initially ``envoy.reloadable_features.use_libcurl_to_fetch_aws_credentials`` is + set to ``true`` so that in future if the reloadable feature is set to ``false`` the cluster config is available to fetch the credentials. diff --git a/source/common/runtime/runtime_features.cc b/source/common/runtime/runtime_features.cc index c572b3d2820a..ea7a92e65f54 100644 --- a/source/common/runtime/runtime_features.cc +++ b/source/common/runtime/runtime_features.cc @@ -113,6 +113,9 @@ FALSE_RUNTIME_GUARD(envoy_reloadable_features_enable_include_histograms); FALSE_RUNTIME_GUARD(envoy_reloadable_features_refresh_rtt_after_request); // TODO(danzh) false deprecate it once QUICHE has its own enable/disable flag. FALSE_RUNTIME_GUARD(envoy_reloadable_features_quic_reject_all); +// TODO(suniltheta): Once the newly added http async technique proves effective and +// is stabilized get rid of this feature flag and code path that relies on libcurl. +FALSE_RUNTIME_GUARD(envoy_reloadable_features_use_libcurl_to_fetch_aws_credentials); // TODO(adisuissa): enable by default once this is tested in prod. FALSE_RUNTIME_GUARD(envoy_restart_features_use_eds_cache_for_ads); // TODO(#10646) change to true when UHV is sufficiently tested diff --git a/source/extensions/common/aws/BUILD b/source/extensions/common/aws/BUILD index b5d884069500..b2cc5cd4e1d2 100644 --- a/source/extensions/common/aws/BUILD +++ b/source/extensions/common/aws/BUILD @@ -59,12 +59,16 @@ envoy_cc_library( external_deps = ["abseil_time"], deps = [ ":credentials_provider_interface", + ":metadata_fetcher_lib", ":utility_lib", "//envoy/api:api_interface", "//source/common/common:logger_lib", "//source/common/common:thread_lib", "//source/common/http:utility_lib", + "//source/common/init:target_lib", "//source/common/json:json_loader_lib", + "//source/common/runtime:runtime_features_lib", + "//source/common/tracing:http_tracer_lib", ], ) @@ -81,6 +85,7 @@ envoy_cc_library( "//source/common/common:utility_lib", "//source/common/http:headers_lib", "//source/common/http:utility_lib", + "//source/common/runtime:runtime_features_lib", "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/upstreams/http/v3:pkg_cc_proto", ], diff --git a/source/extensions/common/aws/credentials_provider.h b/source/extensions/common/aws/credentials_provider.h index 9de0fe8b7a4d..dc06c0c77988 100644 --- a/source/extensions/common/aws/credentials_provider.h +++ b/source/extensions/common/aws/credentials_provider.h @@ -68,6 +68,8 @@ class CredentialsProvider { virtual Credentials getCredentials() PURE; }; +using CredentialsConstSharedPtr = std::shared_ptr; +using CredentialsConstUniquePtr = std::unique_ptr; using CredentialsProviderSharedPtr = std::shared_ptr; } // namespace Aws diff --git a/source/extensions/common/aws/credentials_provider_impl.cc b/source/extensions/common/aws/credentials_provider_impl.cc index 9cba9e82c265..785144227a81 100644 --- a/source/extensions/common/aws/credentials_provider_impl.cc +++ b/source/extensions/common/aws/credentials_provider_impl.cc @@ -9,6 +9,7 @@ #include "source/common/http/utility.h" #include "source/common/json/json_loader.h" #include "source/common/runtime/runtime_features.h" +#include "source/common/tracing/http_tracer_impl.h" #include "source/extensions/common/aws/utility.h" #include "absl/strings/str_format.h" @@ -52,6 +53,9 @@ constexpr char EC2_IMDS_TOKEN_TTL_HEADER[] = "X-aws-ec2-metadata-token-ttl-secon constexpr char EC2_IMDS_TOKEN_TTL_DEFAULT_VALUE[] = "21600"; constexpr char SECURITY_CREDENTIALS_PATH[] = "/latest/meta-data/iam/security-credentials"; +constexpr char EC2_METADATA_CLUSTER[] = "ec2_instance_metadata_server_internal"; +constexpr char CONTAINER_METADATA_CLUSTER[] = "ecs_task_metadata_server_internal"; + } // namespace Credentials EnvironmentCredentialsProvider::getCredentials() { @@ -80,6 +84,95 @@ void CachedCredentialsProviderBase::refreshIfNeeded() { } } +// TODO(suniltheta): The field context is of type ServerFactoryContextOptRef so that an +// optional empty value can be set. Especially in aws iam plugin the cluster manager +// obtained from server factory context object is not fully initialized due to the +// reasons explained in https://github.com/envoyproxy/envoy/issues/27586 which cannot +// utilize http async client here to fetch AWS credentials. For time being if context +// is empty then will use libcurl to fetch the credentials. +MetadataCredentialsProviderBase::MetadataCredentialsProviderBase( + Api::Api& api, ServerFactoryContextOptRef context, + const CurlMetadataFetcher& fetch_metadata_using_curl, + CreateMetadataFetcherCb create_metadata_fetcher_cb, absl::string_view cluster_name, + absl::string_view uri) + : api_(api), context_(context), fetch_metadata_using_curl_(fetch_metadata_using_curl), + create_metadata_fetcher_cb_(create_metadata_fetcher_cb), + cluster_name_(std::string(cluster_name)), cache_duration_(getCacheDuration()), + debug_name_(absl::StrCat("Fetching aws credentials from cluster=", cluster_name)) { + if (context_) { + context_->mainThreadDispatcher().post([this, uri]() { + if (!Utility::addInternalClusterStatic(context_->clusterManager(), cluster_name_, + envoy::config::cluster::v3::Cluster::STATIC, uri)) { + ENVOY_LOG(critical, + "Failed to add [STATIC cluster = {} with address = {}] or cluster not found", + cluster_name_, uri); + return; + } + }); + + tls_ = ThreadLocal::TypedSlot::makeUnique(context_->threadLocal()); + tls_->set( + [](Envoy::Event::Dispatcher&) { return std::make_shared(); }); + + cache_duration_timer_ = context_->mainThreadDispatcher().createTimer([this]() -> void { + if (useHttpAsyncClient()) { + const Thread::LockGuard lock(lock_); + refresh(); + } + }); + + if (useHttpAsyncClient()) { + // Register with init_manager, force the listener to wait for fetching (refresh). + init_target_ = + std::make_unique(debug_name_, [this]() -> void { refresh(); }); + context_->initManager().add(*init_target_); + } + } +} + +Credentials MetadataCredentialsProviderBase::getCredentials() { + refreshIfNeeded(); + if (useHttpAsyncClient() && context_ && tls_) { + // If server factor context was supplied then we would have thread local slot initialized. + return *(*tls_)->credentials_.get(); + } else { + return cached_credentials_; + } +} + +std::chrono::seconds MetadataCredentialsProviderBase::getCacheDuration() { + return std::chrono::seconds( + REFRESH_INTERVAL * 60 * 60 - + REFRESH_GRACE_PERIOD /*TODO: Add jitter from context.api().randomGenerator()*/); +} + +void MetadataCredentialsProviderBase::handleFetchDone() { + if (useHttpAsyncClient() && context_) { + if (init_target_) { + init_target_->ready(); + init_target_.reset(); + } + if (cache_duration_timer_ && !cache_duration_timer_->enabled()) { + cache_duration_timer_->enableTimer(cache_duration_); + } + } +} + +void MetadataCredentialsProviderBase::setCredentialsToAllThreads( + CredentialsConstUniquePtr&& creds) { + CredentialsConstSharedPtr shared_credentials = std::move(creds); + if (tls_) { + tls_->runOnAllThreads([shared_credentials](OptRef obj) { + obj->credentials_ = shared_credentials; + }); + } +} + +bool MetadataCredentialsProviderBase::useHttpAsyncClient() { + return !Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.use_libcurl_to_fetch_aws_credentials"); +} + bool CredentialsFileCredentialsProvider::needsRefresh() { return api_.timeSource().systemTime() - last_updated_ > REFRESH_INTERVAL; } @@ -161,6 +254,14 @@ void CredentialsFileCredentialsProvider::extractCredentials(const std::string& c last_updated_ = api_.timeSource().systemTime(); } +InstanceProfileCredentialsProvider::InstanceProfileCredentialsProvider( + Api::Api& api, ServerFactoryContextOptRef context, + const CurlMetadataFetcher& fetch_metadata_using_curl, + CreateMetadataFetcherCb create_metadata_fetcher_cb, absl::string_view cluster_name) + : MetadataCredentialsProviderBase(api, context, fetch_metadata_using_curl, + create_metadata_fetcher_cb, cluster_name, EC2_METADATA_HOST) { +} + bool InstanceProfileCredentialsProvider::needsRefresh() { return api_.timeSource().systemTime() - last_updated_ > REFRESH_INTERVAL; } @@ -176,17 +277,40 @@ void InstanceProfileCredentialsProvider::refresh() { token_req_message.headers().setPath(EC2_IMDS_TOKEN_RESOURCE); token_req_message.headers().setCopy(Http::LowerCaseString(EC2_IMDS_TOKEN_TTL_HEADER), EC2_IMDS_TOKEN_TTL_DEFAULT_VALUE); - const auto token_string = metadata_fetcher_(token_req_message); - if (token_string) { - ENVOY_LOG(debug, "Obtained token to make secure call to EC2MetadataService"); - fetchInstanceRole(token_string.value()); + + if (!useHttpAsyncClient() || !context_) { + // Using curl to fetch the AWS credentials where we first get the token. + const auto token_string = fetch_metadata_using_curl_(token_req_message); + if (token_string) { + ENVOY_LOG(debug, "Obtained token to make secure call to EC2MetadataService"); + fetchInstanceRole(std::move(token_string.value())); + } else { + ENVOY_LOG(warn, + "Failed to get token from EC2MetadataService, falling back to less secure way"); + fetchInstanceRole(std::move("")); + } } else { - ENVOY_LOG(warn, "Failed to get token from EC2MetadataService, falling back to less secure way"); - fetchInstanceRole(""); + // Stop any existing timer. + if (cache_duration_timer_ && cache_duration_timer_->enabled()) { + cache_duration_timer_->disableTimer(); + } + // Using Http async client to fetch the AWS credentials where we first get the token. + if (!metadata_fetcher_) { + metadata_fetcher_ = create_metadata_fetcher_cb_(context_->clusterManager(), clusterName()); + } else { + metadata_fetcher_->cancel(); // Cancel if there is any inflight request. + } + on_async_fetch_cb_ = [this](const std::string&& arg) { + return this->fetchInstanceRoleAsync(std::move(arg)); + }; + continue_on_async_fetch_failure_ = true; + continue_on_async_fetch_failure_reason_ = "Token fetch failed so fall back to less secure way"; + metadata_fetcher_->fetch(token_req_message, Tracing::NullSpan::instance(), *this); } } -void InstanceProfileCredentialsProvider::fetchInstanceRole(const std::string& token_string) { +void InstanceProfileCredentialsProvider::fetchInstanceRole(const std::string&& token_string, + bool async /*default = false*/) { // Discover the Role of this instance. Http::RequestMessageImpl message; message.headers().setMethod(Http::Headers::get().MethodValues.Get); @@ -196,22 +320,43 @@ void InstanceProfileCredentialsProvider::fetchInstanceRole(const std::string& to message.headers().setCopy(Http::LowerCaseString(EC2_IMDS_TOKEN_HEADER), StringUtil::trim(token_string)); } - const auto instance_role_string = metadata_fetcher_(message); - if (!instance_role_string) { - ENVOY_LOG(error, "Could not retrieve credentials listing from the EC2MetadataService"); - return; + + if (!async) { + // Using curl to fetch the Instance Role. + const auto instance_role_string = fetch_metadata_using_curl_(message); + if (!instance_role_string) { + ENVOY_LOG(error, "Could not retrieve credentials listing from the EC2MetadataService"); + return; + } + fetchCredentialFromInstanceRole(std::move(instance_role_string.value()), + std::move(token_string)); + } else { + // Using Http async client to fetch the Instance Role. + metadata_fetcher_->cancel(); // Cancel if there is any inflight request. + on_async_fetch_cb_ = [this, token_string = std::move(token_string)](const std::string&& arg) { + return this->fetchCredentialFromInstanceRoleAsync(std::move(arg), std::move(token_string)); + }; + metadata_fetcher_->fetch(message, Tracing::NullSpan::instance(), *this); } - fetchCredentialFromInstanceRole(instance_role_string.value(), token_string); } void InstanceProfileCredentialsProvider::fetchCredentialFromInstanceRole( - const std::string& instance_role, const std::string& token_string) { + const std::string&& instance_role, const std::string&& token_string, + bool async /*default = false*/) { + if (instance_role.empty()) { + ENVOY_LOG(error, "No roles found to fetch AWS credentials from the EC2MetadataService"); + if (async) { + handleFetchDone(); + } return; } const auto instance_role_list = StringUtil::splitToken(StringUtil::trim(instance_role), "\n"); if (instance_role_list.empty()) { - ENVOY_LOG(error, "No AWS credentials were found in the EC2MetadataService"); + ENVOY_LOG(error, "No roles found to fetch AWS credentials from the EC2MetadataService"); + if (async) { + handleFetchDone(); + } return; } ENVOY_LOG(debug, "AWS credentials list:\n{}", instance_role); @@ -223,7 +368,6 @@ void InstanceProfileCredentialsProvider::fetchCredentialFromInstanceRole( std::string(instance_role_list[0].data(), instance_role_list[0].size()); ENVOY_LOG(debug, "AWS credentials path: {}", credential_path); - // Then fetch and parse the credentials Http::RequestMessageImpl message; message.headers().setMethod(Http::Headers::get().MethodValues.Get); message.headers().setHost(EC2_METADATA_HOST); @@ -232,23 +376,40 @@ void InstanceProfileCredentialsProvider::fetchCredentialFromInstanceRole( message.headers().setCopy(Http::LowerCaseString(EC2_IMDS_TOKEN_HEADER), StringUtil::trim(token_string)); } - const auto credential_document = metadata_fetcher_(message); - if (!credential_document) { - ENVOY_LOG(error, "Could not load AWS credentials document from the EC2MetadataService"); - return; + + if (!async) { + // Fetch and parse the credentials. + const auto credential_document = fetch_metadata_using_curl_(message); + if (!credential_document) { + ENVOY_LOG(error, "Could not load AWS credentials document from the EC2MetadataService"); + return; + } + extractCredentials(std::move(credential_document.value())); + } else { + // Using Http async client to fetch and parse the AWS credentials. + metadata_fetcher_->cancel(); // Cancel if there is any inflight request. + on_async_fetch_cb_ = [this](const std::string&& arg) { + return this->extractCredentialsAsync(std::move(arg)); + }; + metadata_fetcher_->fetch(message, Tracing::NullSpan::instance(), *this); } - extractCredentials(credential_document.value()); } void InstanceProfileCredentialsProvider::extractCredentials( - const std::string& credential_document_value) { + const std::string&& credential_document_value, bool async /*default = false*/) { if (credential_document_value.empty()) { + if (async) { + handleFetchDone(); + } return; } Json::ObjectSharedPtr document_json; TRY_NEEDS_AUDIT { document_json = Json::Factory::loadFromString(credential_document_value); } END_TRY catch (EnvoyException& e) { ENVOY_LOG(error, "Could not parse AWS credentials document: {}", e.what()); + if (async) { + handleFetchDone(); + } return; } @@ -262,10 +423,46 @@ void InstanceProfileCredentialsProvider::extractCredentials( secret_access_key.empty() ? "" : "*****", AWS_SESSION_TOKEN, session_token.empty() ? "" : "*****"); - cached_credentials_ = Credentials(access_key_id, secret_access_key, session_token); last_updated_ = api_.timeSource().systemTime(); + if (useHttpAsyncClient() && context_) { + setCredentialsToAllThreads( + std::make_unique(access_key_id, secret_access_key, session_token)); + } else { + cached_credentials_ = Credentials(access_key_id, secret_access_key, session_token); + } + handleFetchDone(); +} + +void InstanceProfileCredentialsProvider::onMetadataSuccess(const std::string&& body) { + // TODO(suniltheta): increment fetch success stats + ENVOY_LOG(info, "AWS Instance metadata fetch success, calling callback func"); + on_async_fetch_cb_(std::move(body)); } +void InstanceProfileCredentialsProvider::onMetadataError(Failure reason) { + // TODO(suniltheta): increment fetch failed stats + if (continue_on_async_fetch_failure_) { + ENVOY_LOG(critical, "{}. Reason: {}", continue_on_async_fetch_failure_reason_, + metadata_fetcher_->failureToString(reason)); + continue_on_async_fetch_failure_ = false; + continue_on_async_fetch_failure_reason_ = ""; + on_async_fetch_cb_(std::move("")); + } else { + ENVOY_LOG(error, "AWS Instance metadata fetch failure: {}", + metadata_fetcher_->failureToString(reason)); + handleFetchDone(); + } +} + +TaskRoleCredentialsProvider::TaskRoleCredentialsProvider( + Api::Api& api, ServerFactoryContextOptRef context, + const CurlMetadataFetcher& fetch_metadata_using_curl, + CreateMetadataFetcherCb create_metadata_fetcher_cb, absl::string_view credential_uri, + absl::string_view authorization_token = {}, absl::string_view cluster_name = {}) + : MetadataCredentialsProviderBase(api, context, fetch_metadata_using_curl, + create_metadata_fetcher_cb, cluster_name, credential_uri), + credential_uri_(credential_uri), authorization_token_(authorization_token) {} + bool TaskRoleCredentialsProvider::needsRefresh() { const auto now = api_.timeSource().systemTime(); return (now - last_updated_ > REFRESH_INTERVAL) || @@ -284,22 +481,43 @@ void TaskRoleCredentialsProvider::refresh() { message.headers().setHost(host); message.headers().setPath(path); message.headers().setCopy(Http::CustomHeaders::get().Authorization, authorization_token_); - const auto credential_document = metadata_fetcher_(message); - if (!credential_document) { - ENVOY_LOG(error, "Could not load AWS credentials document from the task role"); - return; + if (!useHttpAsyncClient() || !context_) { + // Using curl to fetch the AWS credentials. + const auto credential_document = fetch_metadata_using_curl_(message); + if (!credential_document) { + ENVOY_LOG(error, "Could not load AWS credentials document from the task role"); + return; + } + extractCredentials(std::move(credential_document.value())); + } else { + // Stop any existing timer. + if (cache_duration_timer_ && cache_duration_timer_->enabled()) { + cache_duration_timer_->disableTimer(); + } + // Using Http async client to fetch the AWS credentials. + if (!metadata_fetcher_) { + metadata_fetcher_ = create_metadata_fetcher_cb_(context_->clusterManager(), clusterName()); + } else { + metadata_fetcher_->cancel(); // Cancel if there is any inflight request. + } + on_async_fetch_cb_ = [this](const std::string&& arg) { + return this->extractCredentials(std::move(arg)); + }; + metadata_fetcher_->fetch(message, Tracing::NullSpan::instance(), *this); } - extractCredentials(credential_document.value()); } -void TaskRoleCredentialsProvider::extractCredentials(const std::string& credential_document_value) { +void TaskRoleCredentialsProvider::extractCredentials( + const std::string&& credential_document_value) { if (credential_document_value.empty()) { + handleFetchDone(); return; } Json::ObjectSharedPtr document_json; TRY_NEEDS_AUDIT { document_json = Json::Factory::loadFromString(credential_document_value); } END_TRY catch (EnvoyException& e) { ENVOY_LOG(error, "Could not parse AWS credentials document from the task role: {}", e.what()); + handleFetchDone(); return; } @@ -322,7 +540,25 @@ void TaskRoleCredentialsProvider::extractCredentials(const std::string& credenti } last_updated_ = api_.timeSource().systemTime(); - cached_credentials_ = Credentials(access_key_id, secret_access_key, session_token); + if (useHttpAsyncClient() && context_) { + setCredentialsToAllThreads( + std::make_unique(access_key_id, secret_access_key, session_token)); + } else { + cached_credentials_ = Credentials(access_key_id, secret_access_key, session_token); + } + handleFetchDone(); +} + +void TaskRoleCredentialsProvider::onMetadataSuccess(const std::string&& body) { + // TODO(suniltheta): increment fetch success stats + ENVOY_LOG(debug, "AWS metadata fetch success, calling callback func"); + on_async_fetch_cb_(std::move(body)); +} + +void TaskRoleCredentialsProvider::onMetadataError(Failure reason) { + // TODO(suniltheta): increment fetch failed stats + ENVOY_LOG(error, "AWS metadata fetch failure: {}", metadata_fetcher_->failureToString(reason)); + handleFetchDone(); } Credentials CredentialsProviderChain::getCredentials() { @@ -338,7 +574,8 @@ Credentials CredentialsProviderChain::getCredentials() { } DefaultCredentialsProviderChain::DefaultCredentialsProviderChain( - Api::Api& api, const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher, + Api::Api& api, ServerFactoryContextOptRef context, + const MetadataCredentialsProviderBase::CurlMetadataFetcher& fetch_metadata_using_curl, const CredentialsProviderChainFactories& factories) { ENVOY_LOG(debug, "Using environment credentials provider"); add(factories.createEnvironmentCredentialsProvider()); @@ -358,7 +595,9 @@ DefaultCredentialsProviderChain::DefaultCredentialsProviderChain( if (!relative_uri.empty()) { const auto uri = absl::StrCat(CONTAINER_METADATA_HOST, relative_uri); ENVOY_LOG(debug, "Using task role credentials provider with URI: {}", uri); - add(factories.createTaskRoleCredentialsProvider(api, metadata_fetcher, uri)); + add(factories.createTaskRoleCredentialsProvider(api, context, fetch_metadata_using_curl, + MetadataFetcher::create, + CONTAINER_METADATA_CLUSTER, uri)); } else if (!full_uri.empty()) { const auto authorization_token = absl::NullSafeStringView(std::getenv(AWS_CONTAINER_AUTHORIZATION_TOKEN)); @@ -367,15 +606,19 @@ DefaultCredentialsProviderChain::DefaultCredentialsProviderChain( "Using task role credentials provider with URI: " "{} and authorization token", full_uri); - add(factories.createTaskRoleCredentialsProvider(api, metadata_fetcher, full_uri, - authorization_token)); + add(factories.createTaskRoleCredentialsProvider( + api, context, fetch_metadata_using_curl, MetadataFetcher::create, + CONTAINER_METADATA_CLUSTER, full_uri, authorization_token)); } else { ENVOY_LOG(debug, "Using task role credentials provider with URI: {}", full_uri); - add(factories.createTaskRoleCredentialsProvider(api, metadata_fetcher, full_uri)); + add(factories.createTaskRoleCredentialsProvider(api, context, fetch_metadata_using_curl, + MetadataFetcher::create, + CONTAINER_METADATA_CLUSTER, full_uri)); } } else if (metadata_disabled != TRUE) { ENVOY_LOG(debug, "Using instance profile credentials provider"); - add(factories.createInstanceProfileCredentialsProvider(api, metadata_fetcher)); + add(factories.createInstanceProfileCredentialsProvider( + api, context, fetch_metadata_using_curl, MetadataFetcher::create, EC2_METADATA_CLUSTER)); } } diff --git a/source/extensions/common/aws/credentials_provider_impl.h b/source/extensions/common/aws/credentials_provider_impl.h index 0b207620ad88..9f875e0c2325 100644 --- a/source/extensions/common/aws/credentials_provider_impl.h +++ b/source/extensions/common/aws/credentials_provider_impl.h @@ -1,14 +1,23 @@ #pragma once #include +#include +#include #include "envoy/api/api.h" +#include "envoy/common/optref.h" #include "envoy/event/timer.h" #include "envoy/http/message.h" +#include "envoy/server/factory_context.h" +#include "source/common/common/lock_guard.h" #include "source/common/common/logger.h" #include "source/common/common/thread.h" +#include "source/common/init/target_impl.h" +#include "source/common/protobuf/message_validator_impl.h" +#include "source/common/protobuf/utility.h" #include "source/extensions/common/aws/credentials_provider.h" +#include "source/extensions/common/aws/metadata_fetcher.h" #include "absl/strings/string_view.h" @@ -17,6 +26,13 @@ namespace Extensions { namespace Common { namespace Aws { +/** + * CreateMetadataFetcherCb is a callback interface for creating a MetadataFetcher instance. + */ +using CreateMetadataFetcherCb = + std::function; +using ServerFactoryContextOptRef = OptRef; + /** * Retrieve AWS credentials from the environment variables. * @@ -68,14 +84,73 @@ class CredentialsFileCredentialsProvider : public CachedCredentialsProviderBase class MetadataCredentialsProviderBase : public CachedCredentialsProviderBase { public: - using MetadataFetcher = std::function(Http::RequestMessage&)>; + using CurlMetadataFetcher = std::function(Http::RequestMessage&)>; + using OnAsyncFetchCb = std::function; + + MetadataCredentialsProviderBase(Api::Api& api, ServerFactoryContextOptRef context, + const CurlMetadataFetcher& fetch_metadata_using_curl, + CreateMetadataFetcherCb create_metadata_fetcher_cb, + absl::string_view cluster_name, absl::string_view uri); + + Credentials getCredentials() override; - MetadataCredentialsProviderBase(Api::Api& api, const MetadataFetcher& metadata_fetcher) - : api_(api), metadata_fetcher_(metadata_fetcher) {} + // Get the Metadata credentials cache duration. + static std::chrono::seconds getCacheDuration(); protected: + struct ThreadLocalCredentialsCache : public ThreadLocal::ThreadLocalObject { + ThreadLocalCredentialsCache() { + credentials_ = std::make_shared(); // Creating empty credentials as default. + } + // The credentials object. + CredentialsConstSharedPtr credentials_; + }; + + const std::string& clusterName() const { return cluster_name_; } + + // Handle fetch done. + void handleFetchDone(); + + // Set Credentials shared_ptr on all threads. + void setCredentialsToAllThreads(CredentialsConstUniquePtr&& creds); + + // Returns true if http async client can be used instead of libcurl to fetch the aws credentials, + // else false. + bool useHttpAsyncClient(); + Api::Api& api_; - MetadataFetcher metadata_fetcher_; + // The optional server factory context. + ServerFactoryContextOptRef context_; + // Store the method to fetch metadata from libcurl (deprecated) + CurlMetadataFetcher fetch_metadata_using_curl_; + // The callback used to create a MetadataFetcher instance. + CreateMetadataFetcherCb create_metadata_fetcher_cb_; + // The cluster name to use for internal static cluster pointing towards the credentials provider. + const std::string cluster_name_; + // The cache duration of the fetched credentials. + const std::chrono::seconds cache_duration_; + // The thread local slot for cache. + ThreadLocal::TypedSlotPtr tls_; + // The timer to trigger fetch due to cache duration. + Envoy::Event::TimerPtr cache_duration_timer_; + // The Metadata fetcher object. + MetadataFetcherPtr metadata_fetcher_; + // Callback function to call on successful metadata fetch. + OnAsyncFetchCb on_async_fetch_cb_; + // To determine if credentials fetching can continue even after metadata fetch failure. + bool continue_on_async_fetch_failure_ = false; + // Reason to log on fetch failure while continue. + std::string continue_on_async_fetch_failure_reason_ = ""; + // Last update time to determine expiration. + SystemTime last_updated_; + // Cache credentials when using libcurl. + Credentials cached_credentials_; + // Lock guard. + Thread::MutexBasicLockable lock_; + // The init target. + std::unique_ptr init_target_; + // Used in logs. + const std::string debug_name_; }; /** @@ -83,17 +158,35 @@ class MetadataCredentialsProviderBase : public CachedCredentialsProviderBase { * * https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials */ -class InstanceProfileCredentialsProvider : public MetadataCredentialsProviderBase { +class InstanceProfileCredentialsProvider : public MetadataCredentialsProviderBase, + public MetadataFetcher::MetadataReceiver { public: - InstanceProfileCredentialsProvider(Api::Api& api, const MetadataFetcher& metadata_fetcher) - : MetadataCredentialsProviderBase(api, metadata_fetcher) {} + InstanceProfileCredentialsProvider(Api::Api& api, ServerFactoryContextOptRef context, + const CurlMetadataFetcher& fetch_metadata_using_curl, + CreateMetadataFetcherCb create_metadata_fetcher_cb, + absl::string_view cluster_name); + + // Following functions are for MetadataFetcher::MetadataReceiver interface + void onMetadataSuccess(const std::string&& body) override; + void onMetadataError(Failure reason) override; private: bool needsRefresh() override; void refresh() override; - void fetchInstanceRole(const std::string& token); - void fetchCredentialFromInstanceRole(const std::string& instance_role, const std::string& token); - void extractCredentials(const std::string& credential_document_value); + void fetchInstanceRole(const std::string&& token, bool async = false); + void fetchInstanceRoleAsync(const std::string&& token) { + fetchInstanceRole(std::move(token), true); + } + void fetchCredentialFromInstanceRole(const std::string&& instance_role, const std::string&& token, + bool async = false); + void fetchCredentialFromInstanceRoleAsync(const std::string&& instance_role, + const std::string&& token) { + fetchCredentialFromInstanceRole(std::move(instance_role), std::move(token), true); + } + void extractCredentials(const std::string&& credential_document_value, bool async = false); + void extractCredentialsAsync(const std::string&& credential_document_value) { + extractCredentials(std::move(credential_document_value), true); + } }; /** @@ -101,22 +194,28 @@ class InstanceProfileCredentialsProvider : public MetadataCredentialsProviderBas * * https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html#enable_task_iam_roles */ -class TaskRoleCredentialsProvider : public MetadataCredentialsProviderBase { +class TaskRoleCredentialsProvider : public MetadataCredentialsProviderBase, + public MetadataFetcher::MetadataReceiver { public: - TaskRoleCredentialsProvider(Api::Api& api, const MetadataFetcher& metadata_fetcher, + TaskRoleCredentialsProvider(Api::Api& api, ServerFactoryContextOptRef context, + const CurlMetadataFetcher& fetch_metadata_using_curl, + CreateMetadataFetcherCb create_metadata_fetcher_cb, absl::string_view credential_uri, - absl::string_view authorization_token = {}) - : MetadataCredentialsProviderBase(api, metadata_fetcher), credential_uri_(credential_uri), - authorization_token_(authorization_token) {} + absl::string_view authorization_token, + absl::string_view cluster_name); + + // Following functions are for MetadataFetcher::MetadataReceiver interface + void onMetadataSuccess(const std::string&& body) override; + void onMetadataError(Failure reason) override; private: SystemTime expiration_time_; - std::string credential_uri_; - std::string authorization_token_; + const std::string credential_uri_; + const std::string authorization_token_; bool needsRefresh() override; void refresh() override; - void extractCredentials(const std::string& credential_document_value); + void extractCredentials(const std::string&& credential_document_value); }; /** @@ -147,12 +246,16 @@ class CredentialsProviderChainFactories { createCredentialsFileCredentialsProvider(Api::Api& api) const PURE; virtual CredentialsProviderSharedPtr createTaskRoleCredentialsProvider( - Api::Api& api, const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher, + Api::Api& api, ServerFactoryContextOptRef context, + const MetadataCredentialsProviderBase::CurlMetadataFetcher& fetch_metadata_using_curl, + CreateMetadataFetcherCb create_metadata_fetcher_cb, absl::string_view cluster_name, absl::string_view credential_uri, absl::string_view authorization_token = {}) const PURE; virtual CredentialsProviderSharedPtr createInstanceProfileCredentialsProvider( - Api::Api& api, - const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher) const PURE; + Api::Api& api, ServerFactoryContextOptRef context, + const MetadataCredentialsProviderBase::CurlMetadataFetcher& fetch_metadata_using_curl, + CreateMetadataFetcherCb create_metadata_fetcher_cb, + absl::string_view cluster_name) const PURE; }; /** @@ -165,11 +268,13 @@ class DefaultCredentialsProviderChain : public CredentialsProviderChain, public CredentialsProviderChainFactories { public: DefaultCredentialsProviderChain( - Api::Api& api, const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher) - : DefaultCredentialsProviderChain(api, metadata_fetcher, *this) {} + Api::Api& api, ServerFactoryContextOptRef context, + const MetadataCredentialsProviderBase::CurlMetadataFetcher& fetch_metadata_using_curl) + : DefaultCredentialsProviderChain(api, context, fetch_metadata_using_curl, *this) {} DefaultCredentialsProviderChain( - Api::Api& api, const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher, + Api::Api& api, ServerFactoryContextOptRef context, + const MetadataCredentialsProviderBase::CurlMetadataFetcher& fetch_metadata_using_curl, const CredentialsProviderChainFactories& factories); private: @@ -183,19 +288,28 @@ class DefaultCredentialsProviderChain : public CredentialsProviderChain, } CredentialsProviderSharedPtr createTaskRoleCredentialsProvider( - Api::Api& api, const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher, + Api::Api& api, ServerFactoryContextOptRef context, + const MetadataCredentialsProviderBase::CurlMetadataFetcher& fetch_metadata_using_curl, + CreateMetadataFetcherCb create_metadata_fetcher_cb, absl::string_view cluster_name, absl::string_view credential_uri, absl::string_view authorization_token = {}) const override { - return std::make_shared(api, metadata_fetcher, credential_uri, - authorization_token); + return std::make_shared(api, context, fetch_metadata_using_curl, + create_metadata_fetcher_cb, credential_uri, + authorization_token, cluster_name); } CredentialsProviderSharedPtr createInstanceProfileCredentialsProvider( - Api::Api& api, - const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher) const override { - return std::make_shared(api, metadata_fetcher); + Api::Api& api, ServerFactoryContextOptRef context, + const MetadataCredentialsProviderBase::CurlMetadataFetcher& fetch_metadata_using_curl, + CreateMetadataFetcherCb create_metadata_fetcher_cb, + absl::string_view cluster_name) const override { + return std::make_shared( + api, context, fetch_metadata_using_curl, create_metadata_fetcher_cb, cluster_name); } }; +using InstanceProfileCredentialsProviderPtr = std::shared_ptr; +using TaskRoleCredentialsProviderPtr = std::shared_ptr; + } // namespace Aws } // namespace Common } // namespace Extensions diff --git a/source/extensions/filters/http/aws_lambda/config.cc b/source/extensions/filters/http/aws_lambda/config.cc index de5a1cfc0a39..adfbf2c277fe 100644 --- a/source/extensions/filters/http/aws_lambda/config.cc +++ b/source/extensions/filters/http/aws_lambda/config.cc @@ -1,5 +1,6 @@ #include "source/extensions/filters/http/aws_lambda/config.h" +#include "envoy/common/optref.h" #include "envoy/extensions/filters/http/aws_lambda/v3/aws_lambda.pb.validate.h" #include "envoy/registry/registry.h" #include "envoy/stats/scope.h" @@ -45,7 +46,8 @@ Http::FilterFactoryCb AwsLambdaFilterFactory::createFilterFactoryFromProtoTyped( auto credentials_provider = std::make_shared( - context.api(), Extensions::Common::Aws::Utility::fetchMetadata); + context.api(), makeOptRef(context.getServerFactoryContext()), + Extensions::Common::Aws::Utility::fetchMetadata); auto signer = std::make_shared( service_name, region, std::move(credentials_provider), diff --git a/source/extensions/filters/http/aws_request_signing/config.cc b/source/extensions/filters/http/aws_request_signing/config.cc index c277f8e600c6..6ad378b41f42 100644 --- a/source/extensions/filters/http/aws_request_signing/config.cc +++ b/source/extensions/filters/http/aws_request_signing/config.cc @@ -1,5 +1,6 @@ #include "source/extensions/filters/http/aws_request_signing/config.h" +#include "envoy/common/optref.h" #include "envoy/extensions/filters/http/aws_request_signing/v3/aws_request_signing.pb.h" #include "envoy/extensions/filters/http/aws_request_signing/v3/aws_request_signing.pb.validate.h" #include "envoy/registry/registry.h" @@ -20,7 +21,8 @@ Http::FilterFactoryCb AwsRequestSigningFilterFactory::createFilterFactoryFromPro auto credentials_provider = std::make_shared( - context.api(), Extensions::Common::Aws::Utility::fetchMetadata); + context.api(), makeOptRef(context.getServerFactoryContext()), + Extensions::Common::Aws::Utility::fetchMetadata); const auto matcher_config = Extensions::Common::Aws::AwsSigV4HeaderExclusionVector( config.match_excluded_headers().begin(), config.match_excluded_headers().end()); auto signer = std::make_unique( @@ -41,7 +43,7 @@ AwsRequestSigningFilterFactory::createRouteSpecificFilterConfigTyped( Server::Configuration::ServerFactoryContext& context, ProtobufMessage::ValidationVisitor&) { auto credentials_provider = std::make_shared( - context.api(), Extensions::Common::Aws::Utility::fetchMetadata); + context.api(), makeOptRef(context), Extensions::Common::Aws::Utility::fetchMetadata); const auto matcher_config = Extensions::Common::Aws::AwsSigV4HeaderExclusionVector( per_route_config.aws_request_signing().match_excluded_headers().begin(), per_route_config.aws_request_signing().match_excluded_headers().end()); diff --git a/source/extensions/grpc_credentials/aws_iam/config.cc b/source/extensions/grpc_credentials/aws_iam/config.cc index 0a8f0bab9782..87fe1c408e8d 100644 --- a/source/extensions/grpc_credentials/aws_iam/config.cc +++ b/source/extensions/grpc_credentials/aws_iam/config.cc @@ -44,8 +44,14 @@ std::shared_ptr AwsIamGrpcCredentialsFactory::getChann const auto& config = Envoy::MessageUtil::downcastAndValidate< const envoy::config::grpc_credential::v3::AwsIamConfig&>( *config_message, ProtobufMessage::getNullValidationVisitor()); + // TODO(suniltheta): Due to the reasons explained in + // https://github.com/envoyproxy/envoy/issues/27586 this aws iam plugin is not able to + // utilize http async client to fetch AWS credentials. For time being this is still using + // libcurl to fetch the credentials. To fully get rid of curl, need to address the below + // usage of AWS credentials common utils. Until then we are setting nullopt for server + // factory context. auto credentials_provider = std::make_shared( - api, Common::Aws::Utility::fetchMetadata); + api, absl::nullopt /*Empty factory context*/, Common::Aws::Utility::fetchMetadata); auto signer = std::make_unique( config.service_name(), getRegion(config), credentials_provider, api.timeSource(), // TODO: extend API to allow specifying header exclusion. ref: @@ -101,7 +107,9 @@ AwsIamHeaderAuthenticator::GetMetadata(grpc::string_ref service_url, grpc::strin absl::string_view(method_name.data(), method_name.length())); TRY_NEEDS_AUDIT { signer_->sign(message, false); } - END_TRY catch (const EnvoyException& e) { return {grpc::StatusCode::INTERNAL, e.what()}; } + END_TRY catch (const EnvoyException& e) { + return grpc::Status(grpc::StatusCode::INTERNAL, e.what()); + } signedHeadersToMetadata(message.headers(), *metadata); diff --git a/test/extensions/common/aws/BUILD b/test/extensions/common/aws/BUILD index 43ce091b0f55..2306da720307 100644 --- a/test/extensions/common/aws/BUILD +++ b/test/extensions/common/aws/BUILD @@ -75,10 +75,12 @@ envoy_cc_test( srcs = ["credentials_provider_impl_test.cc"], deps = [ "//source/extensions/common/aws:credentials_provider_impl_lib", + "//source/extensions/common/aws:metadata_fetcher_lib", "//test/extensions/common/aws:aws_mocks", "//test/mocks/api:api_mocks", "//test/mocks/event:event_mocks", "//test/mocks/runtime:runtime_mocks", + "//test/mocks/server:factory_context_mocks", "//test/test_common:environment_lib", "//test/test_common:simulated_time_system_lib", "//test/test_common:test_runtime_lib", diff --git a/test/extensions/common/aws/credentials_provider_impl_test.cc b/test/extensions/common/aws/credentials_provider_impl_test.cc index a93c9ccf770b..37ff05206f93 100644 --- a/test/extensions/common/aws/credentials_provider_impl_test.cc +++ b/test/extensions/common/aws/credentials_provider_impl_test.cc @@ -1,18 +1,28 @@ +#include +#include + #include "source/extensions/common/aws/credentials_provider_impl.h" +#include "source/extensions/common/aws/metadata_fetcher.h" #include "test/extensions/common/aws/mocks.h" #include "test/mocks/api/mocks.h" #include "test/mocks/event/mocks.h" #include "test/mocks/runtime/mocks.h" +#include "test/mocks/server/factory_context.h" #include "test/test_common/environment.h" #include "test/test_common/simulated_time_system.h" #include "test/test_common/test_runtime.h" +using Envoy::Extensions::Common::Aws::MetadataFetcher; +using Envoy::Extensions::Common::Aws::MetadataFetcherPtr; +using Envoy::Extensions::Common::Aws::MockMetadataFetcher; using testing::_; +using testing::Eq; using testing::InSequence; using testing::NiceMock; using testing::Ref; using testing::Return; +using testing::Throw; namespace Envoy { namespace Extensions { @@ -48,6 +58,37 @@ aws_secret_access_key = profile4_secret aws_session_token = profile4_token )"; +MATCHER_P(WithName, expectedName, "") { + *result_listener << "\nexpected { name: \"" << expectedName << "\"} but got {name: \"" + << arg.name() << "\"}\n"; + return ExplainMatchResult(expectedName, arg.name(), result_listener); +} + +MATCHER_P(WithAttribute, expectedCluster, "") { + const auto argSocketAddress = + arg.load_assignment().endpoints()[0].lb_endpoints()[0].endpoint().address().socket_address(); + const auto expectedSocketAddress = expectedCluster.load_assignment() + .endpoints()[0] + .lb_endpoints()[0] + .endpoint() + .address() + .socket_address(); + + *result_listener << "\nexpected {cluster name: \"" << expectedCluster.name() << "\", type: \"" + << expectedCluster.type() << "\", socket address: \"" + << expectedSocketAddress.address() << "\", port: \"" + << expectedSocketAddress.port_value() << "\"},\n but got {cluster name: \"" + << arg.name() << "\", type: \"" << arg.type() << "\", socket address: \"" + << argSocketAddress.address() << "\", port: \"" << argSocketAddress.port_value() + << "\"}\n"; + return ExplainMatchResult(expectedCluster.name(), arg.name(), result_listener) && + ExplainMatchResult(expectedCluster.type(), arg.type(), result_listener) && + ExplainMatchResult(expectedSocketAddress.address(), argSocketAddress.address(), + result_listener) && + ExplainMatchResult(expectedSocketAddress.port_value(), argSocketAddress.port_value(), + result_listener); +} + class EvironmentCredentialsProviderTest : public testing::Test { public: ~EvironmentCredentialsProviderTest() override { @@ -250,13 +291,847 @@ messageMatches(const Http::TestRequestHeaderMapImpl& expected_headers) { return testing::MakeMatcher(new MessageMatcher(expected_headers)); } +// Begin unit test for new option via Http Async client. class InstanceProfileCredentialsProviderTest : public testing::Test { public: InstanceProfileCredentialsProviderTest() - : api_(Api::createApiForTest(time_system_)), - provider_(*api_, [this](Http::RequestMessage& message) -> absl::optional { + : api_(Api::createApiForTest(time_system_)), raw_metadata_fetcher_(new MockMetadataFetcher) {} + + void setupProvider() { + ON_CALL(context_, clusterManager()).WillByDefault(ReturnRef(cluster_manager_)); + provider_ = std::make_shared( + *api_, context_, + [this](Http::RequestMessage& message) -> absl::optional { + return this->fetch_metadata_.fetch(message); + }, + [this](Upstream::ClusterManager&, absl::string_view) { + metadata_fetcher_.reset(raw_metadata_fetcher_); + return std::move(metadata_fetcher_); + }, + "credentials_provider_cluster"); + } + + void setupProviderWithContext() { + EXPECT_CALL(context_.init_manager_, add(_)).WillOnce(Invoke([this](const Init::Target& target) { + init_target_handle_ = target.createHandle("test"); + })); + + setupProvider(); + expected_duration_ = provider_->getCacheDuration(); + init_target_handle_->initialize(init_watcher_); + } + + void expectSessionToken(const uint64_t status_code, const std::string&& token) { + Http::TestRequestHeaderMapImpl headers{{":path", "/latest/api/token"}, + {":authority", "169.254.169.254:80"}, + {":method", "PUT"}, + {"X-aws-ec2-metadata-token-ttl-seconds", "21600"}}; + EXPECT_CALL(*raw_metadata_fetcher_, fetch(messageMatches(headers), _, _)) + .WillRepeatedly(Invoke([this, status_code, token = std::move(token)]( + Http::RequestMessage&, Tracing::Span&, + MetadataFetcher::MetadataReceiver& receiver) { + if (status_code == enumToInt(Http::Code::OK)) { + if (!token.empty()) { + receiver.onMetadataSuccess(std::move(token)); + } else { + EXPECT_CALL( + *raw_metadata_fetcher_, + failureToString(Eq(MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata))) + .WillRepeatedly(testing::Return("InvalidMetadata")); + receiver.onMetadataError(MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata); + } + } else { + EXPECT_CALL(*raw_metadata_fetcher_, + failureToString(Eq(MetadataFetcher::MetadataReceiver::Failure::Network))) + .WillRepeatedly(testing::Return("Network")); + receiver.onMetadataError(MetadataFetcher::MetadataReceiver::Failure::Network); + } + })); + } + + void expectCredentialListing(const uint64_t status_code, const std::string&& instance_role) { + Http::TestRequestHeaderMapImpl headers{{":path", "/latest/meta-data/iam/security-credentials"}, + {":authority", "169.254.169.254:80"}, + {":method", "GET"}}; + EXPECT_CALL(*raw_metadata_fetcher_, fetch(messageMatches(headers), _, _)) + .WillRepeatedly(Invoke([this, status_code, instance_role = std::move(instance_role)]( + Http::RequestMessage&, Tracing::Span&, + MetadataFetcher::MetadataReceiver& receiver) { + if (status_code == enumToInt(Http::Code::OK)) { + if (!instance_role.empty()) { + receiver.onMetadataSuccess(std::move(instance_role)); + } else { + EXPECT_CALL( + *raw_metadata_fetcher_, + failureToString(Eq(MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata))) + .WillRepeatedly(testing::Return("InvalidMetadata")); + receiver.onMetadataError(MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata); + } + } else { + EXPECT_CALL(*raw_metadata_fetcher_, + failureToString(Eq(MetadataFetcher::MetadataReceiver::Failure::Network))) + .WillRepeatedly(testing::Return("Network")); + receiver.onMetadataError(MetadataFetcher::MetadataReceiver::Failure::Network); + } + })); + } + + void expectCredentialListingSecure(const uint64_t status_code, + const std::string&& instance_role) { + Http::TestRequestHeaderMapImpl headers{{":path", "/latest/meta-data/iam/security-credentials"}, + {":authority", "169.254.169.254:80"}, + {":method", "GET"}, + {"X-aws-ec2-metadata-token", "TOKEN"}}; + EXPECT_CALL(*raw_metadata_fetcher_, fetch(messageMatches(headers), _, _)) + .WillRepeatedly(Invoke([this, status_code, instance_role = std::move(instance_role)]( + Http::RequestMessage&, Tracing::Span&, + MetadataFetcher::MetadataReceiver& receiver) { + if (status_code == enumToInt(Http::Code::OK)) { + if (!instance_role.empty()) { + receiver.onMetadataSuccess(std::move(instance_role)); + } else { + EXPECT_CALL( + *raw_metadata_fetcher_, + failureToString(Eq(MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata))) + .WillRepeatedly(testing::Return("InvalidMetadata")); + receiver.onMetadataError(MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata); + } + } else { + EXPECT_CALL(*raw_metadata_fetcher_, + failureToString(Eq(MetadataFetcher::MetadataReceiver::Failure::Network))) + .WillRepeatedly(testing::Return("Network")); + receiver.onMetadataError(MetadataFetcher::MetadataReceiver::Failure::Network); + } + })); + } + + void expectDocument(const uint64_t status_code, const std::string&& credential_document_value) { + Http::TestRequestHeaderMapImpl headers{ + {":path", "/latest/meta-data/iam/security-credentials/doc1"}, + {":authority", "169.254.169.254:80"}, + {":method", "GET"}}; + EXPECT_CALL(*raw_metadata_fetcher_, fetch(messageMatches(headers), _, _)) + .WillRepeatedly(Invoke([this, status_code, + credential_document_value = std::move(credential_document_value)]( + Http::RequestMessage&, Tracing::Span&, + MetadataFetcher::MetadataReceiver& receiver) { + if (status_code == enumToInt(Http::Code::OK)) { + if (!credential_document_value.empty()) { + receiver.onMetadataSuccess(std::move(credential_document_value)); + } else { + EXPECT_CALL( + *raw_metadata_fetcher_, + failureToString(Eq(MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata))) + .WillRepeatedly(testing::Return("InvalidMetadata")); + receiver.onMetadataError(MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata); + } + } else { + EXPECT_CALL(*raw_metadata_fetcher_, + failureToString(Eq(MetadataFetcher::MetadataReceiver::Failure::Network))) + .WillRepeatedly(testing::Return("Network")); + receiver.onMetadataError(MetadataFetcher::MetadataReceiver::Failure::Network); + } + })); + } + + void expectDocumentSecure(const uint64_t status_code, + const std::string&& credential_document_value) { + Http::TestRequestHeaderMapImpl headers{ + {":path", "/latest/meta-data/iam/security-credentials/doc1"}, + {":authority", "169.254.169.254:80"}, + + {":method", "GET"}, + {"X-aws-ec2-metadata-token", "TOKEN"}}; + EXPECT_CALL(*raw_metadata_fetcher_, fetch(messageMatches(headers), _, _)) + .WillRepeatedly(Invoke([this, status_code, + credential_document_value = std::move(credential_document_value)]( + Http::RequestMessage&, Tracing::Span&, + MetadataFetcher::MetadataReceiver& receiver) { + if (status_code == enumToInt(Http::Code::OK)) { + if (!credential_document_value.empty()) { + receiver.onMetadataSuccess(std::move(credential_document_value)); + } else { + EXPECT_CALL( + *raw_metadata_fetcher_, + failureToString(Eq(MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata))) + .WillRepeatedly(testing::Return("InvalidMetadata")); + receiver.onMetadataError(MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata); + } + } else { + EXPECT_CALL(*raw_metadata_fetcher_, + failureToString(Eq(MetadataFetcher::MetadataReceiver::Failure::Network))) + .WillRepeatedly(testing::Return("Network")); + receiver.onMetadataError(MetadataFetcher::MetadataReceiver::Failure::Network); + } + })); + } + + TestScopedRuntime scoped_runtime_; + Event::SimulatedTimeSystem time_system_; + Api::ApiPtr api_; + NiceMock fetch_metadata_; + MockMetadataFetcher* raw_metadata_fetcher_; + MetadataFetcherPtr metadata_fetcher_; + NiceMock cluster_manager_; + NiceMock context_; + InstanceProfileCredentialsProviderPtr provider_; + Init::TargetHandlePtr init_target_handle_; + NiceMock init_watcher_; + Event::MockTimer* timer_{}; + std::chrono::milliseconds expected_duration_; +}; + +TEST_F(InstanceProfileCredentialsProviderTest, TestAddMissingCluster) { + // Setup without thread local cluster yet + envoy::config::cluster::v3::Cluster expected_cluster; + constexpr static const char* kStaticCluster = R"EOF( +name: credentials_provider_cluster +type: static +connectTimeout: 2s +lb_policy: ROUND_ROBIN +loadAssignment: + clusterName: credentials_provider_cluster + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: "169.254.169.254" + portValue: 80 +typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http_protocol_options: + accept_http_10: true + )EOF"; + MessageUtil::loadFromYaml(kStaticCluster, expected_cluster, + ProtobufMessage::getNullValidationVisitor()); + + EXPECT_CALL(cluster_manager_, getThreadLocalCluster(_)).WillOnce(Return(nullptr)); + EXPECT_CALL(cluster_manager_, addOrUpdateCluster(WithAttribute(expected_cluster), _)) + .WillOnce(Return(true)); + + expectSessionToken(200, std::move("TOKEN")); + expectCredentialListingSecure(200, std::move(std::string("doc1"))); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + expectDocumentSecure(200, std::move(R"EOF( + { + "AccessKeyId": "akid", + "SecretAccessKey": "secret", + "Token": "token" + } + )EOF")); + + setupProviderWithContext(); +} + +TEST_F(InstanceProfileCredentialsProviderTest, TestClusterMissing) { + // Setup without thread local cluster + Http::RequestMessageImpl message; + + EXPECT_CALL(cluster_manager_, getThreadLocalCluster(_)).WillOnce(Return(nullptr)); + EXPECT_CALL(cluster_manager_, addOrUpdateCluster(WithName("credentials_provider_cluster"), _)) + .WillOnce(Throw(EnvoyException("exeption message"))); + + // init_watcher ready is not called. + init_watcher_.expectReady().Times(0); + setupProvider(); + // Below line is not testing anything, will just avoid asan failure with memory leak. + metadata_fetcher_.reset(raw_metadata_fetcher_); +} + +TEST_F(InstanceProfileCredentialsProviderTest, FailedCredentialListingUnsecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(403 /*Forbidden*/, std::move(std::string())); + expectCredentialListing(403 /*Forbidden*/, std::move(std::string())); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called once. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()); + // Expect refresh timer to be started. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, FailedCredentialListingSecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move("TOKEN")); + expectCredentialListingSecure(401 /*Unauthorized*/, std::move(std::string())); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called once. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()); + // Expect refresh timer to be started after fetch done from init. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, EmptyCredentialListingUnsecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move(std::string())); + expectCredentialListing(200, std::move(std::string(""))); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called once. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()); + // Expect refresh timer to be started after fetch done from init. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + // Cancel is called once for fetching once again as previous attempt wasn't a success. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, EmptyCredentialListingSecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move("TOKEN")); + expectCredentialListingSecure(200, std::move(std::string(""))); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called once. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()); + // Expect refresh timer to be started after fetch done from init. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, EmptyListCredentialListingUnsecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move(std::string())); + expectCredentialListing(200, std::move(std::string("\n"))); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called once. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()); + // Expect refresh timer to be started after fetch done from init. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + // Cancel is called once for fetching once again as previous attempt wasn't a success. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, EmptyListCredentialListingSecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move("TOKEN")); + expectCredentialListingSecure(200, std::move(std::string("\n"))); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called once. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()); + // Expect refresh timer to be started after fetch done from init. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, FailedDocumentUnsecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move(std::string())); + expectCredentialListing(200, std::move(std::string("doc1\ndoc2\ndoc3"))); + expectDocument(401 /*Unauthorized*/, std::move(std::string())); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be started after fetch done from init. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + // Cancel is called thrice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(3); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, FailedDocumentSecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move("TOKEN")); + expectCredentialListingSecure(200, std::move(std::string("doc1\ndoc2\ndoc3"))); + expectDocumentSecure(401 /*Unauthorized*/, std::move(std::string())); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be started after fetch done from init. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + // Cancel is called thrice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(3); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, MissingDocumentUnsecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move(std::string())); + expectCredentialListing(200, std::move(std::string("doc1\ndoc2\ndoc3"))); + expectDocument(200, std::move(std::string())); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be started after fetch done from init. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + // Cancel is called thrice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(3); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, MissingDocumentSecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move("TOKEN")); + expectCredentialListingSecure(200, std::move(std::string("doc1\ndoc2\ndoc3"))); + expectDocumentSecure(200, std::move(std::string())); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be started after fetch done from init. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + // Cancel is called thrice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(3); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, MalformedDocumentUnsecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move(std::string())); + expectCredentialListing(200, std::move(std::string("doc1"))); + expectDocument(200, std::move(R"EOF( + not json + )EOF")); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be started after fetch done from init. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + // Cancel is called thrice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(3); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, MalformedDocumentSecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move("TOKEN")); + expectCredentialListingSecure(200, std::move(std::string("doc1"))); + expectDocumentSecure(200, std::move(R"EOF( + not json + )EOF")); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be started after fetch done from init. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + // Cancel is called thrice + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(3); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, EmptyValuesUnsecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move(std::string())); + expectCredentialListing(200, std::move(std::string("doc1"))); + expectDocument(200, std::move(R"EOF( + { + "AccessKeyId": "", + "SecretAccessKey": "", + "Token": "" + } + )EOF")); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be started after fetch done from init. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + // Cancel is not called again as we don't expect any more call to fetch until timeout. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(0); + EXPECT_CALL(*raw_metadata_fetcher_, fetch(_, _, _)).Times(0); + + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, EmptyValuesSecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move("TOKEN")); + expectCredentialListingSecure(200, std::move(std::string("doc1"))); + expectDocumentSecure(200, std::move(R"EOF( + { + "AccessKeyId": "", + "SecretAccessKey": "", + "Token": "" + } + )EOF")); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be started after fetch done from init. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + // Cancel is not called again as we don't expect any more call to fetch until timeout. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(0); + EXPECT_CALL(*raw_metadata_fetcher_, fetch(_, _, _)).Times(0); + + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, FullCachedCredentialsUnsecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move(std::string())); + expectCredentialListing(200, std::move(std::string("doc1"))); + expectDocument(200, std::move(R"EOF( + { + "AccessKeyId": "akid", + "SecretAccessKey": "secret", + "Token": "token" + } + )EOF")); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be started after fetch done from init. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + + // init_watcher ready is not called again. + init_watcher_.expectReady().Times(0); + // No need to restart timer since credentials are fetched from cache. + EXPECT_CALL(*timer_, disableTimer()).Times(0); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)).Times(0); + // Cancel is not called again as we don't expect any more call to fetch until timeout. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(0); + EXPECT_CALL(*raw_metadata_fetcher_, fetch(_, _, _)).Times(0); + + const auto credentials = provider_->getCredentials(); + EXPECT_EQ("akid", credentials.accessKeyId().value()); + EXPECT_EQ("secret", credentials.secretAccessKey().value()); + EXPECT_EQ("token", credentials.sessionToken().value()); + + // init_watcher ready is not called again. + init_watcher_.expectReady().Times(0); + // No need to restart timer since credentials are fetched from cache. + EXPECT_CALL(*timer_, disableTimer()).Times(0); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)).Times(0); + // We don't expect any more call to fetch again. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(0); + EXPECT_CALL(*raw_metadata_fetcher_, fetch(_, _, _)).Times(0); + + const auto cached_credentials = provider_->getCredentials(); + EXPECT_EQ("akid", cached_credentials.accessKeyId().value()); + EXPECT_EQ("secret", cached_credentials.secretAccessKey().value()); + EXPECT_EQ("token", cached_credentials.sessionToken().value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, FullCachedCredentialsSecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move("TOKEN")); + expectCredentialListingSecure(200, std::move(std::string("doc1"))); + expectDocumentSecure(200, std::move(R"EOF( + { + "AccessKeyId": "akid", + "SecretAccessKey": "secret", + "Token": "token" + } + )EOF")); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be started after fetch done from init. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + + // init_watcher ready is not called again. + init_watcher_.expectReady().Times(0); + // No need to restart timer since credentials are fetched from cache. + EXPECT_CALL(*timer_, disableTimer()).Times(0); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)).Times(0); + // Cancel is not called again as we don't expect any more call to fetch until timeout. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(0); + EXPECT_CALL(*raw_metadata_fetcher_, fetch(_, _, _)).Times(0); + + const auto credentials = provider_->getCredentials(); + EXPECT_EQ("akid", credentials.accessKeyId().value()); + EXPECT_EQ("secret", credentials.secretAccessKey().value()); + EXPECT_EQ("token", credentials.sessionToken().value()); + + // init_watcher ready is not called again. + init_watcher_.expectReady().Times(0); + // No need to restart timer since credentials are fetched from cache. + EXPECT_CALL(*timer_, disableTimer()).Times(0); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)).Times(0); + // We don't expect any more call to fetch again. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(0); + EXPECT_CALL(*raw_metadata_fetcher_, fetch(_, _, _)).Times(0); + + const auto cached_credentials = provider_->getCredentials(); + EXPECT_EQ("akid", cached_credentials.accessKeyId().value()); + EXPECT_EQ("secret", cached_credentials.secretAccessKey().value()); + EXPECT_EQ("token", cached_credentials.sessionToken().value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, RefreshOnCredentialExpirationUnsecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move(std::string())); + expectCredentialListing(200, std::move(std::string("doc1"))); + expectDocument(200, std::move(R"EOF( + { + "AccessKeyId": "akid", + "SecretAccessKey": "secret", + "Token": "token" + } + )EOF")); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be started. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + + // init_watcher ready is not called again. + init_watcher_.expectReady().Times(0); + // No need to restart timer since credentials are fetched from cache. + EXPECT_CALL(*timer_, disableTimer()).Times(0); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)).Times(0); + // Cancel is not called again as we don't expect any more call to fetch until timeout. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(0); + EXPECT_CALL(*raw_metadata_fetcher_, fetch(_, _, _)).Times(0); + + const auto credentials = provider_->getCredentials(); + EXPECT_EQ("akid", credentials.accessKeyId().value()); + EXPECT_EQ("secret", credentials.secretAccessKey().value()); + EXPECT_EQ("token", credentials.sessionToken().value()); + + expectSessionToken(200, std::move(std::string())); + expectCredentialListing(200, std::move(std::string("doc1"))); + expectDocument(200, std::move(R"EOF( + { + "AccessKeyId": "new_akid", + "SecretAccessKey": "new_secret", + "Token": "new_token1" + } + )EOF")); + + // Expect timer to have expired but we would re-start the timer eventually after refresh. + EXPECT_CALL(*timer_, disableTimer()).Times(0); + // Cancel will be called thrice back to back to back. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(3); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + time_system_.advanceTimeWait(std::chrono::minutes(61)); + timer_->invokeCallback(); + + // We don't expect timer to be reset again for new fetch. + EXPECT_CALL(*timer_, disableTimer()).Times(0); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)).Times(0); + // Similarly we won't call fetch or cancel on metadata fetcher. + EXPECT_CALL(*raw_metadata_fetcher_, fetch(_, _, _)).Times(0); + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(0); + + const auto new_credentials = provider_->getCredentials(); + EXPECT_EQ("new_akid", new_credentials.accessKeyId().value()); + EXPECT_EQ("new_secret", new_credentials.secretAccessKey().value()); + EXPECT_EQ("new_token1", new_credentials.sessionToken().value()); +} + +TEST_F(InstanceProfileCredentialsProviderTest, RefreshOnCredentialExpirationSecure) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectSessionToken(200, std::move("TOKEN")); + expectCredentialListingSecure(200, std::move(std::string("doc1"))); + expectDocumentSecure(200, std::move(R"EOF( + { + "AccessKeyId": "akid", + "SecretAccessKey": "secret", + "Token": "token" + } + )EOF")); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Cancel is called twice. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(2); + // Expect refresh timer to be started. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + + // init_watcher ready is not called again. + init_watcher_.expectReady().Times(0); + // No need to restart timer since credentials are fetched from cache. + EXPECT_CALL(*timer_, disableTimer()).Times(0); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)).Times(0); + // Cancel is not called again as we don't expect any more call to fetch until timeout. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(0); + EXPECT_CALL(*raw_metadata_fetcher_, fetch(_, _, _)).Times(0); + + const auto credentials = provider_->getCredentials(); + EXPECT_EQ("akid", credentials.accessKeyId().value()); + EXPECT_EQ("secret", credentials.secretAccessKey().value()); + EXPECT_EQ("token", credentials.sessionToken().value()); + + expectSessionToken(200, std::move("TOKEN")); + expectCredentialListingSecure(200, std::move(std::string("doc1"))); + expectDocumentSecure(200, std::move(R"EOF( + { + "AccessKeyId": "new_akid", + "SecretAccessKey": "new_secret", + "Token": "new_token1" + } + )EOF")); + + // Expect timer to have expired but we would re-start the timer eventually after refresh. + EXPECT_CALL(*timer_, disableTimer()).Times(0); + // Cancel will be called thrice back to back to back. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(3); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + time_system_.advanceTimeWait(std::chrono::minutes(61)); + timer_->invokeCallback(); + + // We don't expect timer to be reset again for new fetch. + EXPECT_CALL(*timer_, disableTimer()).Times(0); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)).Times(0); + // Similarly we won't call fetch or cancel on metadata fetcher. + EXPECT_CALL(*raw_metadata_fetcher_, fetch(_, _, _)).Times(0); + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(0); + + const auto new_credentials = provider_->getCredentials(); + EXPECT_EQ("new_akid", new_credentials.accessKeyId().value()); + EXPECT_EQ("new_secret", new_credentials.secretAccessKey().value()); + EXPECT_EQ("new_token1", new_credentials.sessionToken().value()); +} +// End unit test for new option via Http Async client. + +// Begin unit test for deprecated option using Libcurl client. +// TODO(suniltheta): Remove this test class once libcurl is removed from Envoy. +class InstanceProfileCredentialsProviderUsingLibcurlTest : public testing::Test { +public: + InstanceProfileCredentialsProviderUsingLibcurlTest() + : api_(Api::createApiForTest(time_system_)) {} + + void setupProvider() { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.use_libcurl_to_fetch_aws_credentials", "true"}}); + provider_ = std::make_shared( + *api_, absl::nullopt, + [this](Http::RequestMessage& message) -> absl::optional { return this->fetch_metadata_.fetch(message); - }) {} + }, + nullptr, "credentials_provider_cluster"); + } void expectSessionToken(const absl::optional& token) { Http::TestRequestHeaderMapImpl headers{{":path", "/latest/api/token"}, @@ -298,93 +1173,123 @@ class InstanceProfileCredentialsProviderTest : public testing::Test { EXPECT_CALL(fetch_metadata_, fetch(messageMatches(headers))).WillOnce(Return(document)); } + TestScopedRuntime scoped_runtime_; Event::SimulatedTimeSystem time_system_; Api::ApiPtr api_; NiceMock fetch_metadata_; - InstanceProfileCredentialsProvider provider_; + InstanceProfileCredentialsProviderPtr provider_; }; -TEST_F(InstanceProfileCredentialsProviderTest, FailedCredentialListing) { +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, FailedCredentialListingUnsecure) { + setupProvider(); expectSessionToken(absl::optional()); expectCredentialListing(absl::optional()); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_FALSE(credentials.accessKeyId().has_value()); EXPECT_FALSE(credentials.secretAccessKey().has_value()); EXPECT_FALSE(credentials.sessionToken().has_value()); } -TEST_F(InstanceProfileCredentialsProviderTest, FailedCredentialListingSecure) { +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, FailedCredentialListingSecure) { + setupProvider(); expectSessionToken("TOKEN"); expectCredentialListingSecure(absl::optional()); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_FALSE(credentials.accessKeyId().has_value()); EXPECT_FALSE(credentials.secretAccessKey().has_value()); EXPECT_FALSE(credentials.sessionToken().has_value()); } -TEST_F(InstanceProfileCredentialsProviderTest, EmptyCredentialListing) { +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, EmptyCredentialListingUnsecure) { + setupProvider(); expectSessionToken(absl::optional()); expectCredentialListing(""); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_FALSE(credentials.accessKeyId().has_value()); EXPECT_FALSE(credentials.secretAccessKey().has_value()); EXPECT_FALSE(credentials.sessionToken().has_value()); } -TEST_F(InstanceProfileCredentialsProviderTest, EmptyCredentialListingSecure) { +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, EmptyCredentialListingSecure) { + setupProvider(); + expectSessionToken("TOKEN"); + expectCredentialListingSecure("\n"); + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, EmptyListCredentialListingUnsecure) { + setupProvider(); + expectSessionToken(absl::optional()); + expectCredentialListing("\n"); + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, EmptyListCredentialListingSecure) { + setupProvider(); expectSessionToken("TOKEN"); expectCredentialListingSecure(""); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_FALSE(credentials.accessKeyId().has_value()); EXPECT_FALSE(credentials.secretAccessKey().has_value()); EXPECT_FALSE(credentials.sessionToken().has_value()); } -TEST_F(InstanceProfileCredentialsProviderTest, MissingDocument) { +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, MissingDocumentUnsecure) { + setupProvider(); expectSessionToken(absl::optional()); expectCredentialListing("doc1\ndoc2\ndoc3"); expectDocument(absl::optional()); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_FALSE(credentials.accessKeyId().has_value()); EXPECT_FALSE(credentials.secretAccessKey().has_value()); EXPECT_FALSE(credentials.sessionToken().has_value()); } -TEST_F(InstanceProfileCredentialsProviderTest, MissingDocumentSecure) { +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, MissingDocumentSecure) { + setupProvider(); expectSessionToken("TOKEN"); expectCredentialListingSecure("doc1\ndoc2\ndoc3"); expectDocumentSecure(absl::optional()); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_FALSE(credentials.accessKeyId().has_value()); EXPECT_FALSE(credentials.secretAccessKey().has_value()); EXPECT_FALSE(credentials.sessionToken().has_value()); } -TEST_F(InstanceProfileCredentialsProviderTest, MalformedDocumenet) { +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, MalformedDocumentUnsecure) { + setupProvider(); expectSessionToken(absl::optional()); expectCredentialListing("doc1"); expectDocument(R"EOF( not json -)EOF"); - const auto credentials = provider_.getCredentials(); + )EOF"); + const auto credentials = provider_->getCredentials(); EXPECT_FALSE(credentials.accessKeyId().has_value()); EXPECT_FALSE(credentials.secretAccessKey().has_value()); EXPECT_FALSE(credentials.sessionToken().has_value()); } -TEST_F(InstanceProfileCredentialsProviderTest, MalformedDocumentSecure) { +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, MalformedDocumentSecure) { + setupProvider(); expectSessionToken("TOKEN"); expectCredentialListingSecure("doc1"); expectDocumentSecure(R"EOF( not json )EOF"); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_FALSE(credentials.accessKeyId().has_value()); EXPECT_FALSE(credentials.secretAccessKey().has_value()); EXPECT_FALSE(credentials.sessionToken().has_value()); } -TEST_F(InstanceProfileCredentialsProviderTest, EmptyValues) { +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, EmptyValuesUnsecure) { + setupProvider(); expectSessionToken(absl::optional()); expectCredentialListing("doc1"); expectDocument(R"EOF( @@ -393,14 +1298,15 @@ TEST_F(InstanceProfileCredentialsProviderTest, EmptyValues) { "SecretAccessKey": "", "Token": "" } -)EOF"); - const auto credentials = provider_.getCredentials(); + )EOF"); + const auto credentials = provider_->getCredentials(); EXPECT_FALSE(credentials.accessKeyId().has_value()); EXPECT_FALSE(credentials.secretAccessKey().has_value()); EXPECT_FALSE(credentials.sessionToken().has_value()); } -TEST_F(InstanceProfileCredentialsProviderTest, EmptyValuesSecure) { +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, EmptyValuesSecure) { + setupProvider(); expectSessionToken("TOKEN"); expectCredentialListingSecure("doc1"); expectDocumentSecure(R"EOF( @@ -410,13 +1316,14 @@ TEST_F(InstanceProfileCredentialsProviderTest, EmptyValuesSecure) { "Token": "" } )EOF"); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_FALSE(credentials.accessKeyId().has_value()); EXPECT_FALSE(credentials.secretAccessKey().has_value()); EXPECT_FALSE(credentials.sessionToken().has_value()); } -TEST_F(InstanceProfileCredentialsProviderTest, FullCachedCredentials) { +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, FullCachedCredentialsUnsecure) { + setupProvider(); expectSessionToken(absl::optional()); expectCredentialListing("doc1"); expectDocument(R"EOF( @@ -425,18 +1332,19 @@ TEST_F(InstanceProfileCredentialsProviderTest, FullCachedCredentials) { "SecretAccessKey": "secret", "Token": "token" } -)EOF"); - const auto credentials = provider_.getCredentials(); + )EOF"); + const auto credentials = provider_->getCredentials(); EXPECT_EQ("akid", credentials.accessKeyId().value()); EXPECT_EQ("secret", credentials.secretAccessKey().value()); EXPECT_EQ("token", credentials.sessionToken().value()); - const auto cached_credentials = provider_.getCredentials(); + const auto cached_credentials = provider_->getCredentials(); EXPECT_EQ("akid", cached_credentials.accessKeyId().value()); EXPECT_EQ("secret", cached_credentials.secretAccessKey().value()); EXPECT_EQ("token", cached_credentials.sessionToken().value()); } -TEST_F(InstanceProfileCredentialsProviderTest, FullCachedCredentialsSecure) { +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, FullCachedCredentialsSecure) { + setupProvider(); expectSessionToken("TOKEN"); expectCredentialListingSecure("doc1"); expectDocumentSecure(R"EOF( @@ -446,17 +1354,18 @@ TEST_F(InstanceProfileCredentialsProviderTest, FullCachedCredentialsSecure) { "Token": "token" } )EOF"); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_EQ("akid", credentials.accessKeyId().value()); EXPECT_EQ("secret", credentials.secretAccessKey().value()); EXPECT_EQ("token", credentials.sessionToken().value()); - const auto cached_credentials = provider_.getCredentials(); + const auto cached_credentials = provider_->getCredentials(); EXPECT_EQ("akid", cached_credentials.accessKeyId().value()); EXPECT_EQ("secret", cached_credentials.secretAccessKey().value()); EXPECT_EQ("token", cached_credentials.sessionToken().value()); } -TEST_F(InstanceProfileCredentialsProviderTest, CredentialExpiration) { +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, CredentialExpirationUnsecure) { + setupProvider(); InSequence sequence; expectSessionToken(absl::optional()); expectCredentialListing("doc1"); @@ -466,8 +1375,8 @@ TEST_F(InstanceProfileCredentialsProviderTest, CredentialExpiration) { "SecretAccessKey": "secret", "Token": "token" } -)EOF"); - const auto credentials = provider_.getCredentials(); + )EOF"); + const auto credentials = provider_->getCredentials(); EXPECT_EQ("akid", credentials.accessKeyId().value()); EXPECT_EQ("secret", credentials.secretAccessKey().value()); EXPECT_EQ("token", credentials.sessionToken().value()); @@ -480,14 +1389,15 @@ TEST_F(InstanceProfileCredentialsProviderTest, CredentialExpiration) { "SecretAccessKey": "new_secret", "Token": "new_token" } -)EOF"); - const auto new_credentials = provider_.getCredentials(); + )EOF"); + const auto new_credentials = provider_->getCredentials(); EXPECT_EQ("new_akid", new_credentials.accessKeyId().value()); EXPECT_EQ("new_secret", new_credentials.secretAccessKey().value()); EXPECT_EQ("new_token", new_credentials.sessionToken().value()); } -TEST_F(InstanceProfileCredentialsProviderTest, CredentialExpirationSecure) { +TEST_F(InstanceProfileCredentialsProviderUsingLibcurlTest, CredentialExpirationSecure) { + setupProvider(); InSequence sequence; expectSessionToken("TOKEN"); expectCredentialListingSecure("doc1"); @@ -498,7 +1408,7 @@ TEST_F(InstanceProfileCredentialsProviderTest, CredentialExpirationSecure) { "Token": "token" } )EOF"); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_EQ("akid", credentials.accessKeyId().value()); EXPECT_EQ("secret", credentials.secretAccessKey().value()); EXPECT_EQ("token", credentials.sessionToken().value()); @@ -512,26 +1422,422 @@ TEST_F(InstanceProfileCredentialsProviderTest, CredentialExpirationSecure) { "Token": "new_token" } )EOF"); - const auto new_credentials = provider_.getCredentials(); + const auto new_credentials = provider_->getCredentials(); EXPECT_EQ("new_akid", new_credentials.accessKeyId().value()); EXPECT_EQ("new_secret", new_credentials.secretAccessKey().value()); EXPECT_EQ("new_token", new_credentials.sessionToken().value()); } +// End unit test for deprecated option using Libcurl client. +// Begin unit test for new option via Http Async client. class TaskRoleCredentialsProviderTest : public testing::Test { public: TaskRoleCredentialsProviderTest() - : api_(Api::createApiForTest(time_system_)), - provider_( - *api_, - [this](Http::RequestMessage& message) -> absl::optional { - return this->fetch_metadata_.fetch(message); - }, - "169.254.170.2:80/path/to/doc", "auth_token") { + : api_(Api::createApiForTest(time_system_)), raw_metadata_fetcher_(new MockMetadataFetcher) { + // Tue Jan 2 03:04:05 UTC 2018 + time_system_.setSystemTime(std::chrono::milliseconds(1514862245000)); + } + + void setupProvider() { + ON_CALL(context_, clusterManager()).WillByDefault(ReturnRef(cluster_manager_)); + provider_ = std::make_shared( + *api_, context_, + [this](Http::RequestMessage& message) -> absl::optional { + return this->fetch_metadata_.fetch(message); + }, + [this](Upstream::ClusterManager&, absl::string_view) { + metadata_fetcher_.reset(raw_metadata_fetcher_); + return std::move(metadata_fetcher_); + }, + "169.254.170.2:80/path/to/doc", "auth_token", "credentials_provider_cluster"); + } + + void setupProviderWithContext() { + EXPECT_CALL(context_.init_manager_, add(_)).WillOnce(Invoke([this](const Init::Target& target) { + init_target_handle_ = target.createHandle("test"); + })); + setupProvider(); + expected_duration_ = provider_->getCacheDuration(); + init_target_handle_->initialize(init_watcher_); + } + + void expectDocument(const uint64_t status_code, const std::string&& document) { + Http::TestRequestHeaderMapImpl headers{{":path", "/path/to/doc"}, + {":authority", "169.254.170.2:80"}, + {":method", "GET"}, + {"authorization", "auth_token"}}; + EXPECT_CALL(*raw_metadata_fetcher_, fetch(messageMatches(headers), _, _)) + .WillRepeatedly(Invoke([this, status_code, document = std::move(document)]( + Http::RequestMessage&, Tracing::Span&, + MetadataFetcher::MetadataReceiver& receiver) { + if (status_code == enumToInt(Http::Code::OK)) { + if (!document.empty()) { + receiver.onMetadataSuccess(std::move(document)); + } else { + EXPECT_CALL( + *raw_metadata_fetcher_, + failureToString(Eq(MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata))) + .WillRepeatedly(testing::Return("InvalidMetadata")); + receiver.onMetadataError(MetadataFetcher::MetadataReceiver::Failure::InvalidMetadata); + } + } else { + EXPECT_CALL(*raw_metadata_fetcher_, + failureToString(Eq(MetadataFetcher::MetadataReceiver::Failure::Network))) + .WillRepeatedly(testing::Return("Network")); + receiver.onMetadataError(MetadataFetcher::MetadataReceiver::Failure::Network); + } + })); + } + + TestScopedRuntime scoped_runtime_; + Event::SimulatedTimeSystem time_system_; + Api::ApiPtr api_; + NiceMock fetch_metadata_; + MockMetadataFetcher* raw_metadata_fetcher_; + MetadataFetcherPtr metadata_fetcher_; + NiceMock cluster_manager_; + NiceMock context_; + TaskRoleCredentialsProviderPtr provider_; + Init::TargetHandlePtr init_target_handle_; + NiceMock init_watcher_; + Event::MockTimer* timer_{}; + std::chrono::milliseconds expected_duration_; +}; + +TEST_F(TaskRoleCredentialsProviderTest, TestAddMissingCluster) { + // Setup without thread local cluster yet + envoy::config::cluster::v3::Cluster expected_cluster; + constexpr static const char* kStaticCluster = R"EOF( +name: credentials_provider_cluster +type: static +connectTimeout: 2s +lb_policy: ROUND_ROBIN +loadAssignment: + clusterName: credentials_provider_cluster + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: "169.254.170.2" + portValue: 80 +typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http_protocol_options: + accept_http_10: true + )EOF"; + MessageUtil::loadFromYaml(kStaticCluster, expected_cluster, + ProtobufMessage::getNullValidationVisitor()); + + EXPECT_CALL(cluster_manager_, getThreadLocalCluster(_)).WillOnce(Return(nullptr)); + EXPECT_CALL(cluster_manager_, addOrUpdateCluster(WithAttribute(expected_cluster), _)) + .WillOnce(Return(true)); + + expectDocument(200, std::move(R"EOF( +{ + "AccessKeyId": "akid", + "SecretAccessKey": "secret", + "Token": "token", + "Expiration": "2018-01-02T03:05:00Z" +} +)EOF")); + + setupProviderWithContext(); +} + +TEST_F(TaskRoleCredentialsProviderTest, TestClusterMissing) { + // Setup without thread local cluster + Http::RequestMessageImpl message; + + EXPECT_CALL(cluster_manager_, getThreadLocalCluster(_)).WillOnce(Return(nullptr)); + EXPECT_CALL(cluster_manager_, addOrUpdateCluster(WithName("credentials_provider_cluster"), _)) + .WillOnce(Throw(EnvoyException("exeption message"))); + // init_watcher ready is not called. + init_watcher_.expectReady().Times(0); + setupProvider(); + // Below line is not testing anything, will just avoid asan failure with memory leak. + metadata_fetcher_.reset(raw_metadata_fetcher_); +} + +TEST_F(TaskRoleCredentialsProviderTest, FailedFetchingDocument) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectDocument(403 /*Forbidden*/, std::move(std::string())); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Expect refresh timer to be started. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + + // Cancel is called for fetching once again as previous attempt wasn't a success. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(TaskRoleCredentialsProviderTest, EmptyDocument) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectDocument(200, std::move(std::string())); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Expect refresh timer to be started. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + + // Cancel is called for fetching once again as previous attempt wasn't a success. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(TaskRoleCredentialsProviderTest, MalformedDocument) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + + expectDocument(200, std::move(R"EOF( +not json +)EOF")); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Expect refresh timer to be started. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + + // Cancel is called for fetching once again as previous attempt wasn't a success. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(TaskRoleCredentialsProviderTest, EmptyValues) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + + expectDocument(200, std::move(R"EOF( +{ + "AccessKeyId": "", + "SecretAccessKey": "", + "Token": "", + "Expiration": "" +} +)EOF")); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Expect refresh timer to be started. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + + // Cancel is called for fetching once again as previous attempt wasn't a success with updating + // expiration time. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(TaskRoleCredentialsProviderTest, FullCachedCredentials) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectDocument(200, std::move(R"EOF( +{ + "AccessKeyId": "akid", + "SecretAccessKey": "secret", + "Token": "token", + "Expiration": "2018-01-02T03:05:00Z" +} +)EOF")); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Expect refresh timer to be started. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + + // init_watcher ready is not called again. + init_watcher_.expectReady().Times(0); + // No need to restart timer since credentials are fetched from cache. + EXPECT_CALL(*timer_, disableTimer()).Times(0); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)).Times(0); + // We don't expect any more call to cancel or fetch again. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(0); + EXPECT_CALL(*raw_metadata_fetcher_, fetch(_, _, _)).Times(0); + + const auto credentials = provider_->getCredentials(); + EXPECT_EQ("akid", credentials.accessKeyId().value()); + EXPECT_EQ("secret", credentials.secretAccessKey().value()); + EXPECT_EQ("token", credentials.sessionToken().value()); + + // init_watcher ready is not called again. + init_watcher_.expectReady().Times(0); + // No need to restart timer since credentials are fetched from cache. + EXPECT_CALL(*timer_, disableTimer()).Times(0); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)).Times(0); + // We don't expect any more call to cancel or fetch again. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(0); + EXPECT_CALL(*raw_metadata_fetcher_, fetch(_, _, _)).Times(0); + + const auto cached_credentials = provider_->getCredentials(); + EXPECT_EQ("akid", cached_credentials.accessKeyId().value()); + EXPECT_EQ("secret", cached_credentials.secretAccessKey().value()); + EXPECT_EQ("token", cached_credentials.sessionToken().value()); +} + +TEST_F(TaskRoleCredentialsProviderTest, RefreshOnNormalCredentialExpiration) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + + expectDocument(200, std::move(R"EOF( +{ + "AccessKeyId": "akid", + "SecretAccessKey": "secret", + "Token": "token", + "Expiration": "2019-01-02T03:04:05Z" +} +)EOF")); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Expect refresh timer to be started. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + + // init_watcher ready is not called again. + init_watcher_.expectReady().Times(0); + // No need to restart timer since credentials are fetched from cache. + EXPECT_CALL(*timer_, disableTimer()).Times(0); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)).Times(0); + // We don't expect any more call to cancel or fetch again. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(0); + EXPECT_CALL(*raw_metadata_fetcher_, fetch(_, _, _)).Times(0); + + const auto credentials = provider_->getCredentials(); + EXPECT_EQ("akid", credentials.accessKeyId().value()); + EXPECT_EQ("secret", credentials.secretAccessKey().value()); + EXPECT_EQ("token", credentials.sessionToken().value()); + + expectDocument(200, std::move(R"EOF( +{ + "AccessKeyId": "new_akid", + "SecretAccessKey": "new_secret", + "Token": "new_token", + "Expiration": "2019-01-02T03:04:05Z" +} +)EOF")); + // Expect timer to have expired but we would re-start the timer eventually after refresh. + EXPECT_CALL(*timer_, disableTimer()).Times(0); + // Cancel will be called once more. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + time_system_.advanceTimeWait(std::chrono::minutes(61)); + timer_->invokeCallback(); + + // We don't expect timer to be reset again for new fetch. + EXPECT_CALL(*timer_, disableTimer()).Times(0); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)).Times(0); + // Similarly we won't call fetch or cancel on metadata fetcher. + EXPECT_CALL(*raw_metadata_fetcher_, fetch(_, _, _)).Times(0); + EXPECT_CALL(*raw_metadata_fetcher_, cancel()).Times(0); + + const auto cached_credentials = provider_->getCredentials(); + EXPECT_EQ("new_akid", cached_credentials.accessKeyId().value()); + EXPECT_EQ("new_secret", cached_credentials.secretAccessKey().value()); + EXPECT_EQ("new_token", cached_credentials.sessionToken().value()); +} + +TEST_F(TaskRoleCredentialsProviderTest, TimestampCredentialExpiration) { + // Setup timer. + timer_ = new NiceMock(&context_.dispatcher_); + expectDocument(200, std::move(R"EOF( +{ + "AccessKeyId": "akid", + "SecretAccessKey": "secret", + "Token": "token", + "Expiration": "2018-01-02T03:04:05Z" +} +)EOF")); + // init_watcher ready is called. + init_watcher_.expectReady(); + // Expect refresh timer to be started. + EXPECT_CALL(*timer_, enableTimer(_, nullptr)); + setupProviderWithContext(); + + // init_watcher ready is not called again. + init_watcher_.expectReady().Times(0); + // Need to disable and restart timer since credentials are expired and fetched again + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + // We call cancel once. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()); + + const auto credentials = provider_->getCredentials(); + EXPECT_EQ("akid", credentials.accessKeyId().value()); + EXPECT_EQ("secret", credentials.secretAccessKey().value()); + EXPECT_EQ("token", credentials.sessionToken().value()); + + // Cancel is called once. + EXPECT_CALL(*raw_metadata_fetcher_, cancel()); + expectDocument(200, std::move(R"EOF( +{ + "AccessKeyId": "new_akid", + "SecretAccessKey": "new_secret", + "Token": "new_token", + "Expiration": "2019-01-02T03:04:05Z" +} +)EOF")); + // Expect refresh timer to be stopped and started. + EXPECT_CALL(*timer_, disableTimer()); + EXPECT_CALL(*timer_, enableTimer(expected_duration_, nullptr)); + const auto cached_credentials = provider_->getCredentials(); + EXPECT_EQ("new_akid", cached_credentials.accessKeyId().value()); + EXPECT_EQ("new_secret", cached_credentials.secretAccessKey().value()); + EXPECT_EQ("new_token", cached_credentials.sessionToken().value()); +} +// End unit test for new option via Http Async client. + +// Begin unit test for deprecated option using Libcurl client. +// TODO(suniltheta): Remove this test class once libcurl is removed from Envoy. +class TaskRoleCredentialsProviderUsingLibcurlTest : public testing::Test { +public: + TaskRoleCredentialsProviderUsingLibcurlTest() : api_(Api::createApiForTest(time_system_)) { // Tue Jan 2 03:04:05 UTC 2018 time_system_.setSystemTime(std::chrono::milliseconds(1514862245000)); } + void setupProvider() { + scoped_runtime_.mergeValues( + {{"envoy.reloadable_features.use_libcurl_to_fetch_aws_credentials", "true"}}); + provider_ = std::make_shared( + *api_, absl::nullopt, + [this](Http::RequestMessage& message) -> absl::optional { + return this->fetch_metadata_.fetch(message); + }, + nullptr, "169.254.170.2:80/path/to/doc", "auth_token", "credentials_provider_cluster"); + } + void expectDocument(const absl::optional& document) { Http::TestRequestHeaderMapImpl headers{{":path", "/path/to/doc"}, {":authority", "169.254.170.2:80"}, @@ -540,31 +1846,44 @@ class TaskRoleCredentialsProviderTest : public testing::Test { EXPECT_CALL(fetch_metadata_, fetch(messageMatches(headers))).WillOnce(Return(document)); } + TestScopedRuntime scoped_runtime_; Event::SimulatedTimeSystem time_system_; Api::ApiPtr api_; NiceMock fetch_metadata_; - TaskRoleCredentialsProvider provider_; + TaskRoleCredentialsProviderPtr provider_; }; -TEST_F(TaskRoleCredentialsProviderTest, FailedFetchingDocument) { +TEST_F(TaskRoleCredentialsProviderUsingLibcurlTest, FailedFetchingDocument) { + setupProvider(); expectDocument(absl::optional()); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_FALSE(credentials.accessKeyId().has_value()); EXPECT_FALSE(credentials.secretAccessKey().has_value()); EXPECT_FALSE(credentials.sessionToken().has_value()); } -TEST_F(TaskRoleCredentialsProviderTest, MalformedDocumenet) { +TEST_F(TaskRoleCredentialsProviderUsingLibcurlTest, EmptyDocument) { + setupProvider(); + expectDocument(""); + const auto credentials = provider_->getCredentials(); + EXPECT_FALSE(credentials.accessKeyId().has_value()); + EXPECT_FALSE(credentials.secretAccessKey().has_value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +TEST_F(TaskRoleCredentialsProviderUsingLibcurlTest, MalformedDocument) { + setupProvider(); expectDocument(R"EOF( not json )EOF"); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_FALSE(credentials.accessKeyId().has_value()); EXPECT_FALSE(credentials.secretAccessKey().has_value()); EXPECT_FALSE(credentials.sessionToken().has_value()); } -TEST_F(TaskRoleCredentialsProviderTest, EmptyValues) { +TEST_F(TaskRoleCredentialsProviderUsingLibcurlTest, EmptyValues) { + setupProvider(); expectDocument(R"EOF( { "AccessKeyId": "", @@ -573,13 +1892,14 @@ TEST_F(TaskRoleCredentialsProviderTest, EmptyValues) { "Expiration": "" } )EOF"); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_FALSE(credentials.accessKeyId().has_value()); EXPECT_FALSE(credentials.secretAccessKey().has_value()); EXPECT_FALSE(credentials.sessionToken().has_value()); } -TEST_F(TaskRoleCredentialsProviderTest, FullCachedCredentials) { +TEST_F(TaskRoleCredentialsProviderUsingLibcurlTest, FullCachedCredentials) { + setupProvider(); expectDocument(R"EOF( { "AccessKeyId": "akid", @@ -588,17 +1908,18 @@ TEST_F(TaskRoleCredentialsProviderTest, FullCachedCredentials) { "Expiration": "2018-01-02T03:05:00Z" } )EOF"); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_EQ("akid", credentials.accessKeyId().value()); EXPECT_EQ("secret", credentials.secretAccessKey().value()); EXPECT_EQ("token", credentials.sessionToken().value()); - const auto cached_credentials = provider_.getCredentials(); + const auto cached_credentials = provider_->getCredentials(); EXPECT_EQ("akid", cached_credentials.accessKeyId().value()); EXPECT_EQ("secret", cached_credentials.secretAccessKey().value()); EXPECT_EQ("token", cached_credentials.sessionToken().value()); } -TEST_F(TaskRoleCredentialsProviderTest, NormalCredentialExpiration) { +TEST_F(TaskRoleCredentialsProviderUsingLibcurlTest, NormalCredentialExpiration) { + setupProvider(); InSequence sequence; expectDocument(R"EOF( { @@ -608,7 +1929,7 @@ TEST_F(TaskRoleCredentialsProviderTest, NormalCredentialExpiration) { "Expiration": "2019-01-02T03:04:05Z" } )EOF"); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_EQ("akid", credentials.accessKeyId().value()); EXPECT_EQ("secret", credentials.secretAccessKey().value()); EXPECT_EQ("token", credentials.sessionToken().value()); @@ -621,13 +1942,14 @@ TEST_F(TaskRoleCredentialsProviderTest, NormalCredentialExpiration) { "Expiration": "2019-01-02T03:04:05Z" } )EOF"); - const auto cached_credentials = provider_.getCredentials(); + const auto cached_credentials = provider_->getCredentials(); EXPECT_EQ("new_akid", cached_credentials.accessKeyId().value()); EXPECT_EQ("new_secret", cached_credentials.secretAccessKey().value()); EXPECT_EQ("new_token", cached_credentials.sessionToken().value()); } -TEST_F(TaskRoleCredentialsProviderTest, TimestampCredentialExpiration) { +TEST_F(TaskRoleCredentialsProviderUsingLibcurlTest, TimestampCredentialExpiration) { + setupProvider(); InSequence sequence; expectDocument(R"EOF( { @@ -637,7 +1959,7 @@ TEST_F(TaskRoleCredentialsProviderTest, TimestampCredentialExpiration) { "Expiration": "2018-01-02T03:04:05Z" } )EOF"); - const auto credentials = provider_.getCredentials(); + const auto credentials = provider_->getCredentials(); EXPECT_EQ("akid", credentials.accessKeyId().value()); EXPECT_EQ("secret", credentials.secretAccessKey().value()); EXPECT_EQ("token", credentials.sessionToken().value()); @@ -649,15 +1971,18 @@ TEST_F(TaskRoleCredentialsProviderTest, TimestampCredentialExpiration) { "Expiration": "2019-01-02T03:04:05Z" } )EOF"); - const auto cached_credentials = provider_.getCredentials(); + const auto cached_credentials = provider_->getCredentials(); EXPECT_EQ("new_akid", cached_credentials.accessKeyId().value()); EXPECT_EQ("new_secret", cached_credentials.secretAccessKey().value()); EXPECT_EQ("new_token", cached_credentials.sessionToken().value()); } +// End unit test for deprecated option using Libcurl client. class DefaultCredentialsProviderChainTest : public testing::Test { public: DefaultCredentialsProviderChainTest() : api_(Api::createApiForTest(time_system_)) { + ON_CALL(context_, clusterManager()).WillByDefault(ReturnRef(cluster_manager_)); + cluster_manager_.initializeThreadLocalClusters({"credentials_provider_cluster"}); EXPECT_CALL(factories_, createEnvironmentCredentialsProvider()); } @@ -674,62 +1999,67 @@ class DefaultCredentialsProviderChainTest : public testing::Test { MOCK_METHOD(CredentialsProviderSharedPtr, createCredentialsFileCredentialsProvider, (Api::Api&), (const)); MOCK_METHOD(CredentialsProviderSharedPtr, createTaskRoleCredentialsProvider, - (Api::Api&, const MetadataCredentialsProviderBase::MetadataFetcher&, - absl::string_view, absl::string_view), + (Api::Api&, ServerFactoryContextOptRef, + const MetadataCredentialsProviderBase::CurlMetadataFetcher&, + CreateMetadataFetcherCb, absl::string_view, absl::string_view, absl::string_view), (const)); MOCK_METHOD(CredentialsProviderSharedPtr, createInstanceProfileCredentialsProvider, - (Api::Api&, const MetadataCredentialsProviderBase::MetadataFetcher& fetcher), + (Api::Api&, ServerFactoryContextOptRef, + const MetadataCredentialsProviderBase::CurlMetadataFetcher&, + CreateMetadataFetcherCb, absl::string_view), (const)); }; + TestScopedRuntime scoped_runtime_; Event::SimulatedTimeSystem time_system_; Api::ApiPtr api_; + NiceMock cluster_manager_; + NiceMock context_; NiceMock factories_; }; TEST_F(DefaultCredentialsProviderChainTest, NoEnvironmentVars) { EXPECT_CALL(factories_, createCredentialsFileCredentialsProvider(Ref(*api_))); - EXPECT_CALL(factories_, createInstanceProfileCredentialsProvider(Ref(*api_), _)); - DefaultCredentialsProviderChain chain(*api_, DummyMetadataFetcher(), factories_); + EXPECT_CALL(factories_, createInstanceProfileCredentialsProvider(Ref(*api_), _, _, _, _)); + DefaultCredentialsProviderChain chain(*api_, context_, DummyMetadataFetcher(), factories_); } TEST_F(DefaultCredentialsProviderChainTest, CredentialsFileDisabled) { - TestScopedRuntime scoped_runtime; - scoped_runtime.mergeValues({{"envoy.reloadable_features.enable_aws_credentials_file", "false"}}); - + scoped_runtime_.mergeValues({{"envoy.reloadable_features.enable_aws_credentials_file", "false"}}); EXPECT_CALL(factories_, createCredentialsFileCredentialsProvider(Ref(*api_))).Times(0); - EXPECT_CALL(factories_, createInstanceProfileCredentialsProvider(Ref(*api_), _)); - DefaultCredentialsProviderChain chain(*api_, DummyMetadataFetcher(), factories_); + EXPECT_CALL(factories_, createInstanceProfileCredentialsProvider(Ref(*api_), _, _, _, _)); + DefaultCredentialsProviderChain chain(*api_, context_, DummyMetadataFetcher(), factories_); } TEST_F(DefaultCredentialsProviderChainTest, MetadataDisabled) { TestEnvironment::setEnvVar("AWS_EC2_METADATA_DISABLED", "true", 1); EXPECT_CALL(factories_, createCredentialsFileCredentialsProvider(Ref(*api_))); - EXPECT_CALL(factories_, createInstanceProfileCredentialsProvider(Ref(*api_), _)).Times(0); - DefaultCredentialsProviderChain chain(*api_, DummyMetadataFetcher(), factories_); + EXPECT_CALL(factories_, createInstanceProfileCredentialsProvider(Ref(*api_), _, _, _, _)) + .Times(0); + DefaultCredentialsProviderChain chain(*api_, context_, DummyMetadataFetcher(), factories_); } TEST_F(DefaultCredentialsProviderChainTest, MetadataNotDisabled) { TestEnvironment::setEnvVar("AWS_EC2_METADATA_DISABLED", "false", 1); EXPECT_CALL(factories_, createCredentialsFileCredentialsProvider(Ref(*api_))); - EXPECT_CALL(factories_, createInstanceProfileCredentialsProvider(Ref(*api_), _)); - DefaultCredentialsProviderChain chain(*api_, DummyMetadataFetcher(), factories_); + EXPECT_CALL(factories_, createInstanceProfileCredentialsProvider(Ref(*api_), _, _, _, _)); + DefaultCredentialsProviderChain chain(*api_, context_, DummyMetadataFetcher(), factories_); } TEST_F(DefaultCredentialsProviderChainTest, RelativeUri) { TestEnvironment::setEnvVar("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/path/to/creds", 1); EXPECT_CALL(factories_, createCredentialsFileCredentialsProvider(Ref(*api_))); - EXPECT_CALL(factories_, createTaskRoleCredentialsProvider(Ref(*api_), _, + EXPECT_CALL(factories_, createTaskRoleCredentialsProvider(Ref(*api_), _, _, _, _, "169.254.170.2:80/path/to/creds", "")); - DefaultCredentialsProviderChain chain(*api_, DummyMetadataFetcher(), factories_); + DefaultCredentialsProviderChain chain(*api_, context_, DummyMetadataFetcher(), factories_); } TEST_F(DefaultCredentialsProviderChainTest, FullUriNoAuthorizationToken) { TestEnvironment::setEnvVar("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://host/path/to/creds", 1); EXPECT_CALL(factories_, createCredentialsFileCredentialsProvider(Ref(*api_))); - EXPECT_CALL(factories_, - createTaskRoleCredentialsProvider(Ref(*api_), _, "http://host/path/to/creds", "")); - DefaultCredentialsProviderChain chain(*api_, DummyMetadataFetcher(), factories_); + EXPECT_CALL(factories_, createTaskRoleCredentialsProvider(Ref(*api_), _, _, _, _, + "http://host/path/to/creds", "")); + DefaultCredentialsProviderChain chain(*api_, context_, DummyMetadataFetcher(), factories_); } TEST_F(DefaultCredentialsProviderChainTest, FullUriWithAuthorizationToken) { @@ -737,8 +2067,8 @@ TEST_F(DefaultCredentialsProviderChainTest, FullUriWithAuthorizationToken) { TestEnvironment::setEnvVar("AWS_CONTAINER_AUTHORIZATION_TOKEN", "auth_token", 1); EXPECT_CALL(factories_, createCredentialsFileCredentialsProvider(Ref(*api_))); EXPECT_CALL(factories_, createTaskRoleCredentialsProvider( - Ref(*api_), _, "http://host/path/to/creds", "auth_token")); - DefaultCredentialsProviderChain chain(*api_, DummyMetadataFetcher(), factories_); + Ref(*api_), _, _, _, _, "http://host/path/to/creds", "auth_token")); + DefaultCredentialsProviderChain chain(*api_, context_, DummyMetadataFetcher(), factories_); } TEST(CredentialsProviderChainTest, getCredentials_noCredentials) { diff --git a/test/extensions/filters/http/aws_lambda/aws_lambda_filter_integration_test.cc b/test/extensions/filters/http/aws_lambda/aws_lambda_filter_integration_test.cc index b42b5977b839..bee8f3ad2a2e 100644 --- a/test/extensions/filters/http/aws_lambda/aws_lambda_filter_integration_test.cc +++ b/test/extensions/filters/http/aws_lambda/aws_lambda_filter_integration_test.cc @@ -23,6 +23,7 @@ class AwsLambdaFilterIntegrationTest : public testing::TestWithParam