diff --git a/source/extensions/filters/http/common/aws/BUILD b/source/extensions/filters/http/common/aws/BUILD index 0a0c543fb426c..db1057ca56d04 100644 --- a/source/extensions/filters/http/common/aws/BUILD +++ b/source/extensions/filters/http/common/aws/BUILD @@ -40,6 +40,21 @@ envoy_cc_library( external_deps = ["abseil_optional"], ) +envoy_cc_library( + name = "credentials_provider_impl_lib", + srcs = ["credentials_provider_impl.cc"], + hdrs = ["credentials_provider_impl.h"], + external_deps = ["abseil_time"], + deps = [ + ":credentials_provider_interface", + "//include/envoy/api:api_interface", + "//source/common/common:logger_lib", + "//source/common/common:thread_lib", + "//source/common/http:utility_lib", + "//source/common/json:json_loader_lib", + ], +) + envoy_cc_library( name = "utility_lib", srcs = ["utility.cc"], diff --git a/source/extensions/filters/http/common/aws/credentials_provider.h b/source/extensions/filters/http/common/aws/credentials_provider.h index e761b915ba4ed..04ad9d8b101c7 100644 --- a/source/extensions/filters/http/common/aws/credentials_provider.h +++ b/source/extensions/filters/http/common/aws/credentials_provider.h @@ -13,17 +13,27 @@ namespace HttpFilters { namespace Common { namespace Aws { +/** + * AWS credentials container + * + * If a credential component was not found in the execution environment, it's getter method will + * return absl::nullopt. Credential components with the empty string value are treated as not found. + */ class Credentials { public: - Credentials() = default; - - Credentials(absl::string_view access_key_id, absl::string_view secret_access_key) - : access_key_id_(access_key_id), secret_access_key_(secret_access_key) {} - - Credentials(absl::string_view access_key_id, absl::string_view secret_access_key, - absl::string_view session_token) - : access_key_id_(access_key_id), secret_access_key_(secret_access_key), - session_token_(session_token) {} + Credentials(absl::string_view access_key_id = absl::string_view(), + absl::string_view secret_access_key = absl::string_view(), + absl::string_view session_token = absl::string_view()) { + if (!access_key_id.empty()) { + access_key_id_ = std::string(access_key_id); + if (!secret_access_key.empty()) { + secret_access_key_ = std::string(secret_access_key); + if (!session_token.empty()) { + session_token_ = std::string(session_token); + } + } + } + } const absl::optional& accessKeyId() const { return access_key_id_; } @@ -31,16 +41,29 @@ class Credentials { const absl::optional& sessionToken() const { return session_token_; } + bool operator==(const Credentials& other) const { + return access_key_id_ == other.access_key_id_ && + secret_access_key_ == other.secret_access_key_ && session_token_ == other.session_token_; + } + private: absl::optional access_key_id_; absl::optional secret_access_key_; absl::optional session_token_; }; +/** + * Interface for classes able to fetch AWS credentials from the execution environment. + */ class CredentialsProvider { public: virtual ~CredentialsProvider() = default; + /** + * Get credentials from the environment. + * + * @return AWS credentials + */ virtual Credentials getCredentials() PURE; }; @@ -50,4 +73,4 @@ using CredentialsProviderSharedPtr = std::shared_ptr; } // namespace Common } // namespace HttpFilters } // namespace Extensions -} // namespace Envoy \ No newline at end of file +} // namespace Envoy diff --git a/source/extensions/filters/http/common/aws/credentials_provider_impl.cc b/source/extensions/filters/http/common/aws/credentials_provider_impl.cc new file mode 100644 index 0000000000000..43cde8c1ddd88 --- /dev/null +++ b/source/extensions/filters/http/common/aws/credentials_provider_impl.cc @@ -0,0 +1,220 @@ +#include "extensions/filters/http/common/aws/credentials_provider_impl.h" + +#include "envoy/common/exception.h" + +#include "common/common/lock_guard.h" +#include "common/http/utility.h" +#include "common/json/json_loader.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace Aws { + +constexpr static char AWS_ACCESS_KEY_ID[] = "AWS_ACCESS_KEY_ID"; +constexpr static char AWS_SECRET_ACCESS_KEY[] = "AWS_SECRET_ACCESS_KEY"; +constexpr static char AWS_SESSION_TOKEN[] = "AWS_SESSION_TOKEN"; + +constexpr static char ACCESS_KEY_ID[] = "AccessKeyId"; +constexpr static char SECRET_ACCESS_KEY[] = "SecretAccessKey"; +constexpr static char TOKEN[] = "Token"; +constexpr static char EXPIRATION[] = "Expiration"; +constexpr static char EXPIRATION_FORMAT[] = "%E4Y%m%dT%H%M%S%z"; +constexpr static char TRUE[] = "true"; + +constexpr static char AWS_CONTAINER_CREDENTIALS_RELATIVE_URI[] = + "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"; +constexpr static char AWS_CONTAINER_CREDENTIALS_FULL_URI[] = "AWS_CONTAINER_CREDENTIALS_FULL_URI"; +constexpr static char AWS_CONTAINER_AUTHORIZATION_TOKEN[] = "AWS_CONTAINER_AUTHORIZATION_TOKEN"; +constexpr static char AWS_EC2_METADATA_DISABLED[] = "AWS_EC2_METADATA_DISABLED"; + +constexpr static std::chrono::hours REFRESH_INTERVAL{1}; +constexpr static std::chrono::seconds REFRESH_GRACE_PERIOD{5}; +constexpr static char EC2_METADATA_HOST[] = "169.254.169.254:80"; +constexpr static char CONTAINER_METADATA_HOST[] = "169.254.170.2:80"; +constexpr static char SECURITY_CREDENTIALS_PATH[] = "/latest/meta-data/iam/security-credentials"; + +Credentials EnvironmentCredentialsProvider::getCredentials() { + ENVOY_LOG(debug, "Getting AWS credentials from the environment"); + + const auto access_key_id = std::getenv(AWS_ACCESS_KEY_ID); + if (access_key_id == nullptr) { + return Credentials(); + } + + const auto secret_access_key = std::getenv(AWS_SECRET_ACCESS_KEY); + const auto session_token = std::getenv(AWS_SESSION_TOKEN); + + ENVOY_LOG(debug, "Found following AWS credentials in the environment: {}={}, {}={}, {}={}", + AWS_ACCESS_KEY_ID, access_key_id ? access_key_id : "", AWS_SECRET_ACCESS_KEY, + secret_access_key ? "*****" : "", AWS_SESSION_TOKEN, session_token ? "*****" : ""); + + return Credentials(access_key_id, secret_access_key, session_token); +} + +void MetadataCredentialsProviderBase::refreshIfNeeded() { + const Thread::LockGuard lock(lock_); + if (needsRefresh()) { + refresh(); + } +} + +bool InstanceProfileCredentialsProvider::needsRefresh() { + return api_.timeSource().systemTime() - last_updated_ > REFRESH_INTERVAL; +} + +void InstanceProfileCredentialsProvider::refresh() { + ENVOY_LOG(debug, "Getting AWS credentials from the instance metadata"); + + // First discover the Role of this instance + const auto instance_role_string = + metadata_fetcher_(EC2_METADATA_HOST, SECURITY_CREDENTIALS_PATH, ""); + if (!instance_role_string) { + ENVOY_LOG(error, "Could not retrieve credentials listing from the instance metadata"); + return; + } + + const auto instance_role_list = + StringUtil::splitToken(StringUtil::trim(instance_role_string.value()), "\n"); + if (instance_role_list.empty()) { + ENVOY_LOG(error, "No AWS credentials were found in the instance metadata"); + return; + } + ENVOY_LOG(debug, "AWS credentials list:\n{}", instance_role_string.value()); + + // Only one Role can be associated with an instance: + // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html + const auto credential_path = + std::string(SECURITY_CREDENTIALS_PATH) + "/" + + 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 + const auto credential_document = metadata_fetcher_(EC2_METADATA_HOST, credential_path, ""); + if (!credential_document) { + ENVOY_LOG(error, "Could not load AWS credentials document from the instance metadata"); + return; + } + + Json::ObjectSharedPtr document_json; + try { + document_json = Json::Factory::loadFromString(credential_document.value()); + } catch (EnvoyException& e) { + ENVOY_LOG(error, "Could not parse AWS credentials document: {}", e.what()); + return; + } + + const auto access_key_id = document_json->getString(ACCESS_KEY_ID, ""); + const auto secret_access_key = document_json->getString(SECRET_ACCESS_KEY, ""); + const auto session_token = document_json->getString(TOKEN, ""); + + ENVOY_LOG(debug, "Found following AWS credentials in the instance metadata: {}={}, {}={}, {}={}", + AWS_ACCESS_KEY_ID, access_key_id, AWS_SECRET_ACCESS_KEY, + 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(); +} + +bool TaskRoleCredentialsProvider::needsRefresh() { + const auto now = api_.timeSource().systemTime(); + return (now - last_updated_ > REFRESH_INTERVAL) || + (expiration_time_ - now < REFRESH_GRACE_PERIOD); +} + +void TaskRoleCredentialsProvider::refresh() { + ENVOY_LOG(debug, "Getting AWS credentials from the task role at URI: {}", credential_uri_); + + absl::string_view host; + absl::string_view path; + Http::Utility::extractHostPathFromUri(credential_uri_, host, path); + const auto credential_document = + metadata_fetcher_(std::string(host.data(), host.size()), + std::string(path.data(), path.size()), authorization_token_); + if (!credential_document) { + ENVOY_LOG(error, "Could not load AWS credentials document from the task role"); + return; + } + + Json::ObjectSharedPtr document_json; + try { + document_json = Json::Factory::loadFromString(credential_document.value()); + } catch (EnvoyException& e) { + ENVOY_LOG(error, "Could not parse AWS credentials document from the task role: {}", e.what()); + return; + } + + const auto access_key_id = document_json->getString(ACCESS_KEY_ID, ""); + const auto secret_access_key = document_json->getString(SECRET_ACCESS_KEY, ""); + const auto session_token = document_json->getString(TOKEN, ""); + + ENVOY_LOG(debug, "Found following AWS credentials in the task role: {}={}, {}={}, {}={}", + AWS_ACCESS_KEY_ID, access_key_id, AWS_SECRET_ACCESS_KEY, + secret_access_key.empty() ? "" : "*****", AWS_SESSION_TOKEN, + session_token.empty() ? "" : "*****"); + + const auto expiration_str = document_json->getString(EXPIRATION, ""); + if (!expiration_str.empty()) { + absl::Time expiration_time; + if (absl::ParseTime(EXPIRATION_FORMAT, expiration_str, &expiration_time, nullptr)) { + ENVOY_LOG(debug, "Task role AWS credentials expiration time: {}", expiration_str); + expiration_time_ = absl::ToChronoTime(expiration_time); + } + } + + last_updated_ = api_.timeSource().systemTime(); + cached_credentials_ = Credentials(access_key_id, secret_access_key, session_token); +} + +Credentials CredentialsProviderChain::getCredentials() { + for (auto& provider : providers_) { + const auto credentials = provider->getCredentials(); + if (credentials.accessKeyId() && credentials.secretAccessKey()) { + return credentials; + } + } + + ENVOY_LOG(debug, "No AWS credentials found, using anonymous credentials"); + return Credentials(); +} + +DefaultCredentialsProviderChain::DefaultCredentialsProviderChain( + Api::Api& api, const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher, + const CredentialsProviderChainFactories& factories) { + ENVOY_LOG(debug, "Using environment credentials provider"); + add(factories.createEnvironmentCredentialsProvider()); + + const auto relative_uri = std::getenv(AWS_CONTAINER_CREDENTIALS_RELATIVE_URI); + const auto full_uri = std::getenv(AWS_CONTAINER_CREDENTIALS_FULL_URI); + const auto metadata_disabled = std::getenv(AWS_EC2_METADATA_DISABLED); + + if (relative_uri != nullptr) { + const auto uri = std::string(CONTAINER_METADATA_HOST) + relative_uri; + ENVOY_LOG(debug, "Using task role credentials provider with URI: {}", uri); + add(factories.createTaskRoleCredentialsProvider(api, metadata_fetcher, uri)); + } else if (full_uri != nullptr) { + const auto authorization_token = std::getenv(AWS_CONTAINER_AUTHORIZATION_TOKEN); + if (authorization_token != nullptr) { + ENVOY_LOG(debug, + "Using task role credentials provider with URI: " + "{} and authorization token", + full_uri); + add(factories.createTaskRoleCredentialsProvider(api, metadata_fetcher, 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)); + } + } else if (metadata_disabled == nullptr || strncmp(metadata_disabled, TRUE, strlen(TRUE)) != 0) { + ENVOY_LOG(debug, "Using instance profile credentials provider"); + add(factories.createInstanceProfileCredentialsProvider(api, metadata_fetcher)); + } +} + +} // namespace Aws +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/common/aws/credentials_provider_impl.h b/source/extensions/filters/http/common/aws/credentials_provider_impl.h new file mode 100644 index 0000000000000..31e4d25d5d87b --- /dev/null +++ b/source/extensions/filters/http/common/aws/credentials_provider_impl.h @@ -0,0 +1,170 @@ +#pragma once + +#include + +#include "envoy/api/api.h" +#include "envoy/event/timer.h" + +#include "common/common/logger.h" +#include "common/common/thread.h" + +#include "extensions/filters/http/common/aws/credentials_provider.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace Aws { + +/** + * Retrieve AWS credentials from the environment variables. + * + * Adheres to conventions specified in: + * https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html + */ +class EnvironmentCredentialsProvider : public CredentialsProvider, + public Logger::Loggable { +public: + Credentials getCredentials() override; +}; + +class MetadataCredentialsProviderBase : public CredentialsProvider, + public Logger::Loggable { +public: + using MetadataFetcher = std::function( + const std::string& host, const std::string& path, const std::string& auth_token)>; + + MetadataCredentialsProviderBase(Api::Api& api, const MetadataFetcher& metadata_fetcher) + : api_(api), metadata_fetcher_(metadata_fetcher) {} + + Credentials getCredentials() override { + refreshIfNeeded(); + return cached_credentials_; + } + +protected: + Api::Api& api_; + MetadataFetcher metadata_fetcher_; + SystemTime last_updated_; + Credentials cached_credentials_; + Thread::MutexBasicLockable lock_; + + void refreshIfNeeded(); + + virtual bool needsRefresh() PURE; + virtual void refresh() PURE; +}; + +/** + * Retrieve AWS credentials from the instance metadata. + * + * https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials + */ +class InstanceProfileCredentialsProvider : public MetadataCredentialsProviderBase { +public: + InstanceProfileCredentialsProvider(Api::Api& api, const MetadataFetcher& metadata_fetcher) + : MetadataCredentialsProviderBase(api, metadata_fetcher) {} + +private: + bool needsRefresh() override; + void refresh() override; +}; + +/** + * Retrieve AWS credentials from the task metadata. + * + * https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html#enable_task_iam_roles + */ +class TaskRoleCredentialsProvider : public MetadataCredentialsProviderBase { +public: + TaskRoleCredentialsProvider(Api::Api& api, const MetadataFetcher& metadata_fetcher, + const std::string& credential_uri, + const std::string& authorization_token = std::string()) + : MetadataCredentialsProviderBase(api, metadata_fetcher), credential_uri_(credential_uri), + authorization_token_(authorization_token) {} + +private: + SystemTime expiration_time_; + std::string credential_uri_; + std::string authorization_token_; + + bool needsRefresh() override; + void refresh() override; +}; + +/** + * AWS credentials provider chain, able to fallback between multiple credential providers. + */ +class CredentialsProviderChain : public CredentialsProvider, + public Logger::Loggable { +public: + virtual ~CredentialsProviderChain() = default; + + void add(const CredentialsProviderSharedPtr& credentials_provider) { + providers_.emplace_back(credentials_provider); + } + + Credentials getCredentials() override; + +protected: + std::list providers_; +}; + +class CredentialsProviderChainFactories { +public: + virtual ~CredentialsProviderChainFactories() = default; + + virtual CredentialsProviderSharedPtr createEnvironmentCredentialsProvider() const PURE; + + virtual CredentialsProviderSharedPtr createTaskRoleCredentialsProvider( + Api::Api& api, const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher, + const std::string& credential_uri, + const std::string& authorization_token = std::string()) const PURE; + + virtual CredentialsProviderSharedPtr createInstanceProfileCredentialsProvider( + Api::Api& api, + const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher) const PURE; +}; + +/** + * Default AWS credentials provider chain. + * + * Reference implementation: + * https://github.com/aws/aws-sdk-cpp/blob/master/aws-cpp-sdk-core/source/auth/AWSCredentialsProviderChain.cpp#L44 + */ +class DefaultCredentialsProviderChain : public CredentialsProviderChain, + public CredentialsProviderChainFactories { +public: + DefaultCredentialsProviderChain( + Api::Api& api, const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher) + : DefaultCredentialsProviderChain(api, metadata_fetcher, *this) {} + + DefaultCredentialsProviderChain( + Api::Api& api, const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher, + const CredentialsProviderChainFactories& factories); + +private: + CredentialsProviderSharedPtr createEnvironmentCredentialsProvider() const override { + return std::make_shared(); + } + + CredentialsProviderSharedPtr createTaskRoleCredentialsProvider( + Api::Api& api, const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher, + const std::string& credential_uri, + const std::string& authorization_token = std::string()) const override { + return std::make_shared(api, metadata_fetcher, credential_uri, + authorization_token); + } + + CredentialsProviderSharedPtr createInstanceProfileCredentialsProvider( + Api::Api& api, + const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher) const override { + return std::make_shared(api, metadata_fetcher); + } +}; + +} // namespace Aws +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/common/aws/BUILD b/test/extensions/filters/http/common/aws/BUILD index 67bc0a150c6e5..1f8f81417d0a7 100644 --- a/test/extensions/filters/http/common/aws/BUILD +++ b/test/extensions/filters/http/common/aws/BUILD @@ -49,3 +49,24 @@ envoy_cc_test( "//test/test_common:environment_lib", ], ) + +envoy_cc_test( + name = "credentials_provider_impl_test", + srcs = ["credentials_provider_impl_test.cc"], + deps = [ + "//source/extensions/filters/http/common/aws:credentials_provider_impl_lib", + "//test/extensions/filters/http/common/aws:aws_mocks", + "//test/mocks/api:api_mocks", + "//test/mocks/event:event_mocks", + "//test/test_common:environment_lib", + "//test/test_common:simulated_time_system_lib", + ], +) + +envoy_cc_test( + name = "credentials_provider_test", + srcs = ["credentials_provider_test.cc"], + deps = [ + "//source/extensions/filters/http/common/aws:credentials_provider_interface", + ], +) diff --git a/test/extensions/filters/http/common/aws/credentials_provider_impl_test.cc b/test/extensions/filters/http/common/aws/credentials_provider_impl_test.cc new file mode 100644 index 0000000000000..d4c22e6215aff --- /dev/null +++ b/test/extensions/filters/http/common/aws/credentials_provider_impl_test.cc @@ -0,0 +1,457 @@ +#include "extensions/filters/http/common/aws/credentials_provider_impl.h" + +#include "test/extensions/filters/http/common/aws/mocks.h" +#include "test/mocks/api/mocks.h" +#include "test/mocks/event/mocks.h" +#include "test/test_common/environment.h" +#include "test/test_common/simulated_time_system.h" + +using testing::_; +using testing::InSequence; +using testing::NiceMock; +using testing::Ref; +using testing::Return; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace Aws { + +class EvironmentCredentialsProviderTest : public testing::Test { +public: + ~EvironmentCredentialsProviderTest() { + TestEnvironment::unsetEnvVar("AWS_ACCESS_KEY_ID"); + TestEnvironment::unsetEnvVar("AWS_SECRET_ACCESS_KEY"); + TestEnvironment::unsetEnvVar("AWS_SESSION_TOKEN"); + } + + EnvironmentCredentialsProvider provider_; +}; + +TEST_F(EvironmentCredentialsProviderTest, AllEnvironmentVars) { + TestEnvironment::setEnvVar("AWS_ACCESS_KEY_ID", "akid", 1); + TestEnvironment::setEnvVar("AWS_SECRET_ACCESS_KEY", "secret", 1); + TestEnvironment::setEnvVar("AWS_SESSION_TOKEN", "token", 1); + const auto credentials = provider_.getCredentials(); + EXPECT_EQ("akid", credentials.accessKeyId().value()); + EXPECT_EQ("secret", credentials.secretAccessKey().value()); + EXPECT_EQ("token", credentials.sessionToken().value()); +} + +TEST_F(EvironmentCredentialsProviderTest, NoEnvironmentVars) { + 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(EvironmentCredentialsProviderTest, MissingAccessKeyId) { + TestEnvironment::setEnvVar("AWS_SECRET_ACCESS_KEY", "secret", 1); + 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(EvironmentCredentialsProviderTest, NoSessionToken) { + TestEnvironment::setEnvVar("AWS_ACCESS_KEY_ID", "akid", 1); + TestEnvironment::setEnvVar("AWS_SECRET_ACCESS_KEY", "secret", 1); + const auto credentials = provider_.getCredentials(); + EXPECT_EQ("akid", credentials.accessKeyId().value()); + EXPECT_EQ("secret", credentials.secretAccessKey().value()); + EXPECT_FALSE(credentials.sessionToken().has_value()); +} + +class InstanceProfileCredentialsProviderTest : public testing::Test { +public: + InstanceProfileCredentialsProviderTest() + : api_(Api::createApiForTest(time_system_)), + provider_(*api_, + [this](const std::string& host, const std::string& path, + const std::string& auth_token) -> absl::optional { + return this->fetcher_.fetch(host, path, auth_token); + }) {} + + void expectCredentialListing(const absl::optional& listing) { + EXPECT_CALL(fetcher_, + fetch("169.254.169.254:80", "/latest/meta-data/iam/security-credentials", _)) + .WillOnce(Return(listing)); + } + + void expectDocument(const absl::optional& document) { + EXPECT_CALL(fetcher_, + fetch("169.254.169.254:80", "/latest/meta-data/iam/security-credentials/doc1", _)) + .WillOnce(Return(document)); + } + + Event::SimulatedTimeSystem time_system_; + Api::ApiPtr api_; + NiceMock fetcher_; + InstanceProfileCredentialsProvider provider_; +}; + +TEST_F(InstanceProfileCredentialsProviderTest, FailedCredentailListing) { + expectCredentialListing(absl::optional()); + 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) { + expectCredentialListing(""); + 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) { + expectCredentialListing("doc1\ndoc2\ndoc3"); + expectDocument(absl::optional()); + 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) { + expectCredentialListing("doc1"); + expectDocument(R"EOF( +not json +)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, EmptyValues) { + expectCredentialListing("doc1"); + expectDocument(R"EOF( +{ + "AccessKeyId": "", + "SecretAccessKey": "", + "Token": "" +} +)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, FullCachedCredentials) { + expectCredentialListing("doc1"); + expectDocument(R"EOF( +{ + "AccessKeyId": "akid", + "SecretAccessKey": "secret", + "Token": "token" +} +)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(); + 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) { + InSequence sequence; + expectCredentialListing("doc1"); + expectDocument(R"EOF( +{ + "AccessKeyId": "akid", + "SecretAccessKey": "secret", + "Token": "token" +} +)EOF"); + const auto credentials = provider_.getCredentials(); + EXPECT_EQ("akid", credentials.accessKeyId().value()); + EXPECT_EQ("secret", credentials.secretAccessKey().value()); + EXPECT_EQ("token", credentials.sessionToken().value()); + time_system_.sleep(std::chrono::hours(2)); + expectCredentialListing("doc1"); + expectDocument(R"EOF( +{ + "AccessKeyId": "new_akid", + "SecretAccessKey": "new_secret", + "Token": "new_token" +} +)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()); +} + +class TaskRoleCredentialsProviderTest : public testing::Test { +public: + TaskRoleCredentialsProviderTest() + : api_(Api::createApiForTest(time_system_)), + provider_( + *api_, + [this](const std::string& host, const std::string& path, + const absl::optional& auth_token) -> absl::optional { + return this->fetcher_.fetch(host, path, auth_token); + }, + "169.254.170.2:80/path/to/doc", "auth_token") { + // Tue Jan 2 03:04:05 UTC 2018 + time_system_.setSystemTime(std::chrono::milliseconds(1514862245000)); + } + + void expectDocument(const absl::optional& document) { + EXPECT_CALL(fetcher_, fetch("169.254.170.2:80", "/path/to/doc", _)).WillOnce(Return(document)); + } + + Event::SimulatedTimeSystem time_system_; + Api::ApiPtr api_; + NiceMock fetcher_; + TaskRoleCredentialsProvider provider_; +}; + +TEST_F(TaskRoleCredentialsProviderTest, FailedFetchingDocument) { + expectDocument(absl::optional()); + 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) { + expectDocument(R"EOF( +not json +)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(TaskRoleCredentialsProviderTest, EmptyValues) { + expectDocument(R"EOF( +{ + "AccessKeyId": "", + "SecretAccessKey": "", + "Token": "", + "Expiration": "" +} +)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(TaskRoleCredentialsProviderTest, FullCachedCredentials) { + expectDocument(R"EOF( +{ + "AccessKeyId": "akid", + "SecretAccessKey": "secret", + "Token": "token", + "Expiration": "20180102T030500Z" +} +)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(); + 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) { + InSequence sequence; + expectDocument(R"EOF( +{ + "AccessKeyId": "akid", + "SecretAccessKey": "secret", + "Token": "token", + "Expiration": "20190102T030405Z" +} +)EOF"); + const auto credentials = provider_.getCredentials(); + EXPECT_EQ("akid", credentials.accessKeyId().value()); + EXPECT_EQ("secret", credentials.secretAccessKey().value()); + EXPECT_EQ("token", credentials.sessionToken().value()); + time_system_.sleep(std::chrono::hours(2)); + expectDocument(R"EOF( +{ + "AccessKeyId": "new_akid", + "SecretAccessKey": "new_secret", + "Token": "new_token", + "Expiration": "20190102T030405Z" +} +)EOF"); + 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) { + InSequence sequence; + expectDocument(R"EOF( +{ + "AccessKeyId": "akid", + "SecretAccessKey": "secret", + "Token": "token", + "Expiration": "20180102T030405Z" +} +)EOF"); + const auto credentials = provider_.getCredentials(); + EXPECT_EQ("akid", credentials.accessKeyId().value()); + EXPECT_EQ("secret", credentials.secretAccessKey().value()); + EXPECT_EQ("token", credentials.sessionToken().value()); + expectDocument(R"EOF( +{ + "AccessKeyId": "new_akid", + "SecretAccessKey": "new_secret", + "Token": "new_token", + "Expiration": "20190102T030405Z" +} +)EOF"); + 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()); +} + +class DefaultCredentialsProviderChainTest : public testing::Test { +public: + DefaultCredentialsProviderChainTest() : api_(Api::createApiForTest(time_system_)) { + EXPECT_CALL(factories_, createEnvironmentCredentialsProvider()); + } + + ~DefaultCredentialsProviderChainTest() { + TestEnvironment::unsetEnvVar("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"); + TestEnvironment::unsetEnvVar("AWS_CONTAINER_CREDENTIALS_FULL_URI"); + TestEnvironment::unsetEnvVar("AWS_CONTAINER_AUTHORIZATION_TOKEN"); + TestEnvironment::unsetEnvVar("AWS_EC2_METADATA_DISABLED"); + } + + class MockCredentialsProviderChainFactories : public CredentialsProviderChainFactories { + public: + MOCK_CONST_METHOD0(createEnvironmentCredentialsProvider, CredentialsProviderSharedPtr()); + MOCK_CONST_METHOD4(createTaskRoleCredentialsProviderMock, + CredentialsProviderSharedPtr( + Api::Api&, const MetadataCredentialsProviderBase::MetadataFetcher&, + const std::string&, const std::string&)); + MOCK_CONST_METHOD2(createInstanceProfileCredentialsProvider, + CredentialsProviderSharedPtr( + Api::Api&, + const MetadataCredentialsProviderBase::MetadataFetcher& fetcher)); + + virtual CredentialsProviderSharedPtr createTaskRoleCredentialsProvider( + Api::Api& api, const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher, + const std::string& credential_uri, const std::string& authorization_token) const { + return createTaskRoleCredentialsProviderMock(api, metadata_fetcher, credential_uri, + authorization_token); + } + }; + + Event::SimulatedTimeSystem time_system_; + Api::ApiPtr api_; + NiceMock factories_; +}; + +TEST_F(DefaultCredentialsProviderChainTest, NoEnvironmentVars) { + EXPECT_CALL(factories_, createInstanceProfileCredentialsProvider(Ref(*api_), _)); + DefaultCredentialsProviderChain chain(*api_, DummyMetadataFetcher(), factories_); +} + +TEST_F(DefaultCredentialsProviderChainTest, MetadataDisabled) { + TestEnvironment::setEnvVar("AWS_EC2_METADATA_DISABLED", "true", 1); + EXPECT_CALL(factories_, createInstanceProfileCredentialsProvider(Ref(*api_), _)).Times(0); + DefaultCredentialsProviderChain chain(*api_, DummyMetadataFetcher(), factories_); +} + +TEST_F(DefaultCredentialsProviderChainTest, MetadataNotDisabled) { + TestEnvironment::setEnvVar("AWS_EC2_METADATA_DISABLED", "false", 1); + EXPECT_CALL(factories_, createInstanceProfileCredentialsProvider(Ref(*api_), _)); + DefaultCredentialsProviderChain chain(*api_, DummyMetadataFetcher(), factories_); +} + +TEST_F(DefaultCredentialsProviderChainTest, RelativeUri) { + TestEnvironment::setEnvVar("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/path/to/creds", 1); + EXPECT_CALL(factories_, createTaskRoleCredentialsProviderMock( + Ref(*api_), _, "169.254.170.2:80/path/to/creds", "")); + DefaultCredentialsProviderChain chain(*api_, DummyMetadataFetcher(), factories_); +} + +TEST_F(DefaultCredentialsProviderChainTest, FullUriNoAuthorizationToken) { + TestEnvironment::setEnvVar("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://host/path/to/creds", 1); + EXPECT_CALL(factories_, createTaskRoleCredentialsProviderMock(Ref(*api_), _, + "http://host/path/to/creds", "")); + DefaultCredentialsProviderChain chain(*api_, DummyMetadataFetcher(), factories_); +} + +TEST_F(DefaultCredentialsProviderChainTest, FullUriWithAuthorizationToken) { + TestEnvironment::setEnvVar("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://host/path/to/creds", 1); + TestEnvironment::setEnvVar("AWS_CONTAINER_AUTHORIZATION_TOKEN", "auth_token", 1); + EXPECT_CALL(factories_, createTaskRoleCredentialsProviderMock( + Ref(*api_), _, "http://host/path/to/creds", "auth_token")); + DefaultCredentialsProviderChain chain(*api_, DummyMetadataFetcher(), factories_); +} + +TEST(CredentialsProviderChainTest, getCredentials_noCredentials) { + auto mock_provider1 = std::make_shared(); + auto mock_provider2 = std::make_shared(); + + EXPECT_CALL(*mock_provider1, getCredentials()).Times(1); + EXPECT_CALL(*mock_provider2, getCredentials()).Times(1); + + CredentialsProviderChain chain; + chain.add(mock_provider1); + chain.add(mock_provider2); + + const Credentials creds = chain.getCredentials(); + EXPECT_EQ(Credentials(), creds); +} + +TEST(CredentialsProviderChainTest, getCredentials_firstProviderReturns) { + auto mock_provider1 = std::make_shared(); + auto mock_provider2 = std::make_shared(); + + const Credentials creds("access_key", "secret_key"); + + EXPECT_CALL(*mock_provider1, getCredentials()).WillOnce(Return(creds)); + EXPECT_CALL(*mock_provider2, getCredentials()).Times(0); + + CredentialsProviderChain chain; + chain.add(mock_provider1); + chain.add(mock_provider2); + + const Credentials ret_creds = chain.getCredentials(); + EXPECT_EQ(creds, ret_creds); +} + +TEST(CredentialsProviderChainTest, getCredentials_secondProviderReturns) { + auto mock_provider1 = std::make_shared(); + auto mock_provider2 = std::make_shared(); + + const Credentials creds("access_key", "secret_key"); + + EXPECT_CALL(*mock_provider1, getCredentials()).Times(1); + EXPECT_CALL(*mock_provider2, getCredentials()).WillOnce(Return(creds)); + + CredentialsProviderChain chain; + chain.add(mock_provider1); + chain.add(mock_provider2); + + const Credentials ret_creds = chain.getCredentials(); + EXPECT_EQ(creds, ret_creds); +} + +} // namespace Aws +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/common/aws/credentials_provider_test.cc b/test/extensions/filters/http/common/aws/credentials_provider_test.cc new file mode 100644 index 0000000000000..a492ec9b2a87c --- /dev/null +++ b/test/extensions/filters/http/common/aws/credentials_provider_test.cc @@ -0,0 +1,57 @@ +#include "extensions/filters/http/common/aws/credentials_provider.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Common { +namespace Aws { + +TEST(Credentials, Default) { + const auto c = Credentials(); + EXPECT_FALSE(c.accessKeyId().has_value()); + EXPECT_FALSE(c.secretAccessKey().has_value()); + EXPECT_FALSE(c.sessionToken().has_value()); +} + +TEST(Credentials, AllNull) { + const auto c = Credentials(nullptr, nullptr, nullptr); + EXPECT_FALSE(c.accessKeyId().has_value()); + EXPECT_FALSE(c.secretAccessKey().has_value()); + EXPECT_FALSE(c.sessionToken().has_value()); +} + +TEST(Credentials, AllEmpty) { + const auto c = Credentials("", "", ""); + EXPECT_FALSE(c.accessKeyId().has_value()); + EXPECT_FALSE(c.secretAccessKey().has_value()); + EXPECT_FALSE(c.sessionToken().has_value()); +} + +TEST(Credentials, OnlyAccessKeyId) { + const auto c = Credentials("access_key", "", ""); + EXPECT_EQ("access_key", c.accessKeyId()); + EXPECT_FALSE(c.secretAccessKey().has_value()); + EXPECT_FALSE(c.sessionToken().has_value()); +} + +TEST(Credentials, AccessKeyIdAndSecretKey) { + const auto c = Credentials("access_key", "secret_key", ""); + EXPECT_EQ("access_key", c.accessKeyId()); + EXPECT_EQ("secret_key", c.secretAccessKey()); + EXPECT_FALSE(c.sessionToken().has_value()); +} + +TEST(Credentials, AllNonEmpty) { + const auto c = Credentials("access_key", "secret_key", "session_token"); + EXPECT_EQ("access_key", c.accessKeyId()); + EXPECT_EQ("secret_key", c.secretAccessKey()); + EXPECT_EQ("session_token", c.sessionToken()); +} + +} // namespace Aws +} // namespace Common +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/common/aws/mocks.cc b/test/extensions/filters/http/common/aws/mocks.cc index cdc43b1ed1c30..ee5048203bb18 100644 --- a/test/extensions/filters/http/common/aws/mocks.cc +++ b/test/extensions/filters/http/common/aws/mocks.cc @@ -18,4 +18,4 @@ MockSigner::~MockSigner() {} } // namespace Common } // namespace HttpFilters } // namespace Extensions -} // namespace Envoy \ No newline at end of file +} // namespace Envoy diff --git a/test/extensions/filters/http/common/aws/mocks.h b/test/extensions/filters/http/common/aws/mocks.h index 857ed433ac2ba..a19f906f238be 100644 --- a/test/extensions/filters/http/common/aws/mocks.h +++ b/test/extensions/filters/http/common/aws/mocks.h @@ -27,6 +27,22 @@ class MockSigner : public Signer { MOCK_METHOD2(sign, void(Http::Message&, bool)); }; +class MockMetadataFetcher { +public: + virtual ~MockMetadataFetcher(){}; + + MOCK_CONST_METHOD3(fetch, absl::optional(const std::string&, const std::string&, + const absl::optional&)); +}; + +class DummyMetadataFetcher { +public: + absl::optional operator()(const std::string&, const std::string&, + const absl::optional&) { + return absl::nullopt; + } +}; + } // namespace Aws } // namespace Common } // namespace HttpFilters