From 91efc17238c31a8c575445b976ef4d6ba2f76d27 Mon Sep 17 00:00:00 2001 From: Yan Wang <68562925+yanw-bq@users.noreply.github.com> Date: Wed, 29 Mar 2023 23:08:19 -0700 Subject: [PATCH] Secrets Manager Proxy (#121) * new secrets_manager files * working secrets manager client * add cache for secrets manager proxy * first unit test for secrets manager proxy * added more unit tests * fix unique lock missing template arguments * fix multiple calls to aws api in secrets manager unit tests * fix multiple aws init/shutdown api calls in integration tests * fix calling init/shutdownApi in windows * verbose log for windows community tests * wrap initapi and shutdownapi inside secrets manager proxy contructor/destructor * move init/shutdown aws api to alloc/free env handle * move init/shutdown aws api to alloc/free env handle * test * test * fix static variables * fix atomic int init * fix unit tests * renaming * cleanup * introduce aws sdk helpoer * address comments * nit * fix rebase * fix rebase * set error * fix build * better error message --- driver/CMakeLists.txt | 2 + driver/handle.cc | 7 +- driver/secrets_manager_proxy.cc | 168 +++++++++++++++ driver/secrets_manager_proxy.h | 71 +++++++ unit_testing/CMakeLists.txt | 1 + unit_testing/mock_objects.h | 10 + unit_testing/secrets_manager_proxy_test.cc | 234 +++++++++++++++++++++ unit_testing/test_utils.cc | 6 +- unit_testing/test_utils.h | 4 +- 9 files changed, 498 insertions(+), 5 deletions(-) create mode 100644 driver/secrets_manager_proxy.cc create mode 100644 driver/secrets_manager_proxy.h create mode 100644 unit_testing/secrets_manager_proxy_test.cc diff --git a/driver/CMakeLists.txt b/driver/CMakeLists.txt index b52fa9c21..6366f05e9 100644 --- a/driver/CMakeLists.txt +++ b/driver/CMakeLists.txt @@ -96,6 +96,7 @@ WHILE(${DRIVER_INDEX} LESS ${DRIVERS_COUNT}) prepare.cc query_parsing.cc results.cc + secrets_manager_proxy.cc topology_service.cc transact.cc utility.cc) @@ -140,6 +141,7 @@ WHILE(${DRIVER_INDEX} LESS ${DRIVERS_COUNT}) myutil.h parse.h query_parsing.h + secrets_manager_proxy.h topology_service.h ../MYODBC_MYSQL.h ../MYODBC_CONF.h ../MYODBC_ODBC.h) ENDIF(WIN32) diff --git a/driver/handle.cc b/driver/handle.cc index cfc2f98ff..dd55b528d 100644 --- a/driver/handle.cc +++ b/driver/handle.cc @@ -51,6 +51,7 @@ #include "efm_proxy.h" #include "iam_proxy.h" #include "mysql_proxy.h" +#include "secrets_manager_proxy.h" #include @@ -134,9 +135,9 @@ void DBC::init_proxy_chain(DataSource* dsrc) head = iam_proxy; } else if (!myodbc_strcasecmp(AUTH_MODE_SECRETS_MANAGER, auth_mode)) { - // CONNECTION_PROXY* secrets_manager_proxy = new SECRETS_MANAGER_PROXY(his, dsrc); - // secrets_manager_proxy->set_next_proxy(head); - // head = secrets_manager_proxy; + CONNECTION_PROXY* secrets_manager_proxy = new SECRETS_MANAGER_PROXY(this, dsrc); + secrets_manager_proxy->set_next_proxy(head); + head = secrets_manager_proxy; } } diff --git a/driver/secrets_manager_proxy.cc b/driver/secrets_manager_proxy.cc new file mode 100644 index 000000000..e12c3ab3d --- /dev/null +++ b/driver/secrets_manager_proxy.cc @@ -0,0 +1,168 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include +#include + +#include "aws_sdk_helper.h" +#include "secrets_manager_proxy.h" + +#include "mylog.h" + +#undef GetMessage // workaround for AWSError method GetMessage() + +using namespace Aws::SecretsManager; + +namespace { + AWS_SDK_HELPER SDK_HELPER; + const Aws::String USERNAME_KEY{ "username" }; + const Aws::String PASSWORD_KEY{ "password" }; +} + +std::map, Aws::Utils::Json::JsonValue> SECRETS_MANAGER_PROXY::secrets_cache; +std::mutex SECRETS_MANAGER_PROXY::secrets_cache_mutex; + +SECRETS_MANAGER_PROXY::SECRETS_MANAGER_PROXY(DBC* dbc, DataSource* ds) : CONNECTION_PROXY(dbc, ds) { + ++SDK_HELPER; + + SecretsManagerClientConfiguration config; + const auto region = ds_get_utf8attr(ds->auth_region, &ds->auth_region8); + config.region = region ? region : Aws::Region::US_EAST_1; + this->sm_client = std::make_shared(config); + + const auto secret_ID = ds_get_utf8attr(ds->auth_secret_id, &ds->auth_secret_id8); + this->secret_key = std::make_pair(secret_ID ? secret_ID : "", config.region); + this->next_proxy = nullptr; +} + +#ifdef UNIT_TEST_BUILD +SECRETS_MANAGER_PROXY::SECRETS_MANAGER_PROXY(DBC* dbc, DataSource* ds, CONNECTION_PROXY* next_proxy, + std::shared_ptr sm_client) : + CONNECTION_PROXY(dbc, ds), sm_client{std::move(sm_client)} { + + const Aws::String region = ds_get_utf8attr(ds->auth_region, &ds->auth_region8); + const Aws::String secret_ID = ds_get_utf8attr(ds->auth_secret_id, &ds->auth_secret_id8); + this->secret_key = std::make_pair(secret_ID, region); + this->next_proxy = next_proxy; +} +#endif + +SECRETS_MANAGER_PROXY::~SECRETS_MANAGER_PROXY() { + this->sm_client.reset(); + --SDK_HELPER; +} + +bool SECRETS_MANAGER_PROXY::connect(const char* host, const char* user, const char* passwd, const char* database, + unsigned int port, const char* unix_socket, unsigned long flags) { + + if (this->secret_key.first.empty()) { + const auto error = "Missing required config parameter for Secrets Manager: Secret ID"; + MYLOG_DBC_TRACE(dbc, error); + this->set_custom_error_message(error); + return false; + } + + bool fetched = update_secret(false); + std::string username = get_from_secret_json_value(USERNAME_KEY); + std::string password = get_from_secret_json_value(PASSWORD_KEY); + fetched = false; + bool ret = next_proxy->connect(host, username.c_str(), password.c_str(), database, port, unix_socket, flags); + + if (!ret && next_proxy->error_code() == ER_ACCESS_DENIED_ERROR && !fetched) { + // Login unsuccessful with cached credentials + // Try to re-fetch credentials and try again + fetched = update_secret(true); + if (fetched) { + username = get_from_secret_json_value(USERNAME_KEY); + password = get_from_secret_json_value(PASSWORD_KEY); + ret = next_proxy->connect(host, username.c_str(), password.c_str(), database, port, unix_socket, flags); + } + } + + return ret; +} + +bool SECRETS_MANAGER_PROXY::update_secret(bool force_re_fetch) { + bool fetched = false; + { + std::unique_lock lock(secrets_cache_mutex); + + const auto search = secrets_cache.find(this->secret_key); + if (search != secrets_cache.end() && !force_re_fetch) { + this->secret_json_value = search->second; + } + else { + this->secret_json_value = fetch_latest_credentials(); + fetched = true; + secrets_cache[this->secret_key] = this->secret_json_value; + } + } + + return fetched; +} + +Aws::Utils::Json::JsonValue SECRETS_MANAGER_PROXY::fetch_latest_credentials() const { + Aws::String secret_string; + + Model::GetSecretValueRequest request; + request.SetSecretId(this->secret_key.first); + auto get_secret_value_outcome = this->sm_client->GetSecretValue(request); + + if (get_secret_value_outcome.IsSuccess()) { + secret_string = get_secret_value_outcome.GetResult().GetSecretString(); + } + else { + MYLOG_DBC_TRACE(dbc, get_secret_value_outcome.GetError().GetMessage().c_str()); + } + return parse_json_value(secret_string); +} + +Aws::Utils::Json::JsonValue SECRETS_MANAGER_PROXY::parse_json_value(Aws::String json_string) const { + auto res_json = Aws::Utils::Json::JsonValue(json_string); + if (!res_json.WasParseSuccessful()) { + MYLOG_DBC_TRACE(dbc, res_json.GetErrorMessage().c_str()); + throw std::runtime_error("Error parsing secrets manager response body. " + res_json.GetErrorMessage()); + } + return res_json; +} + +std::string SECRETS_MANAGER_PROXY::get_from_secret_json_value(std::string key) const { + std::string value; + const auto view = this->secret_json_value.View(); + + if (view.ValueExists(key)) { + value = view.GetString(key); + } + else { + const auto error = "Unable to extract the " + key + " from secrets manager response."; + MYLOG_DBC_TRACE(dbc, error.c_str()); + throw std::runtime_error(error); + } + return value; +} diff --git a/driver/secrets_manager_proxy.h b/driver/secrets_manager_proxy.h new file mode 100644 index 000000000..3cbaabc9c --- /dev/null +++ b/driver/secrets_manager_proxy.h @@ -0,0 +1,71 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __SECRETS_MANAGER_PROXY__ +#define __SECRETS_MANAGER_PROXY__ + +#include +#include +#include +#include + +#include "connection_proxy.h" +#include "driver.h" + +class SECRETS_MANAGER_PROXY : public CONNECTION_PROXY { +public: + SECRETS_MANAGER_PROXY(DBC* dbc, DataSource* ds); +#ifdef UNIT_TEST_BUILD + SECRETS_MANAGER_PROXY(DBC* dbc, DataSource* ds, CONNECTION_PROXY* next_proxy, std::shared_ptr sm_client); +#endif + ~SECRETS_MANAGER_PROXY() override; + + bool connect(const char* host, const char* user, const char* passwd, const char* database, + unsigned int port, const char* unix_socket, unsigned long flags) override; + +private: + std::shared_ptr sm_client; + std::pair secret_key; + Aws::Utils::Json::JsonValue secret_json_value; + + bool update_secret(bool force_re_fetch); + Aws::Utils::Json::JsonValue fetch_latest_credentials() const; + Aws::Utils::Json::JsonValue parse_json_value(Aws::String json_string) const; + std::string get_from_secret_json_value(std::string key) const; + + static std::map, Aws::Utils::Json::JsonValue> secrets_cache; + static std::mutex secrets_cache_mutex; + +#ifdef UNIT_TEST_BUILD + // Allows for testing private/protected methods + friend class TEST_UTILS; +#endif +}; + +#endif /* __SECRETS_MANAGER_PROXY__ */ diff --git a/unit_testing/CMakeLists.txt b/unit_testing/CMakeLists.txt index 42845af77..9b58b671e 100644 --- a/unit_testing/CMakeLists.txt +++ b/unit_testing/CMakeLists.txt @@ -66,6 +66,7 @@ add_executable( multi_threaded_monitor_service_test.cc query_parsing_test.cc main.cc + secrets_manager_proxy_test.cc topology_service_test.cc ) diff --git a/unit_testing/mock_objects.h b/unit_testing/mock_objects.h index 345c10e79..2914eefae 100644 --- a/unit_testing/mock_objects.h +++ b/unit_testing/mock_objects.h @@ -30,6 +30,7 @@ #ifndef __MOCKOBJECTS_H__ #define __MOCKOBJECTS_H__ +#include #include #include "driver/connection_proxy.h" @@ -83,6 +84,8 @@ class MOCK_CONNECTION_PROXY : public CONNECTION_PROXY { MOCK_METHOD(void, init, ()); MOCK_METHOD(int, ping, ()); MOCK_METHOD(void, delete_ds, ()); + MOCK_METHOD(bool, connect, (const char*, const char*, const char*, const char*, unsigned int, const char*, unsigned long)); + MOCK_METHOD(unsigned int, error_code, ()); }; class MOCK_IAM_PROXY : public IAM_PROXY { @@ -203,4 +206,11 @@ class MOCK_MONITOR_SERVICE : public MONITOR_SERVICE { MOCK_METHOD(void, stop_monitoring_for_all_connections, (std::set)); }; +class MOCK_SECRETS_MANAGER_CLIENT : public Aws::SecretsManager::SecretsManagerClient { +public: + MOCK_SECRETS_MANAGER_CLIENT() : SecretsManagerClient() {}; + + MOCK_METHOD(Aws::SecretsManager::Model::GetSecretValueOutcome, GetSecretValue, (const Aws::SecretsManager::Model::GetSecretValueRequest&), (const)); +}; + #endif /* __MOCKOBJECTS_H__ */ diff --git a/unit_testing/secrets_manager_proxy_test.cc b/unit_testing/secrets_manager_proxy_test.cc new file mode 100644 index 000000000..cf3e5cde7 --- /dev/null +++ b/unit_testing/secrets_manager_proxy_test.cc @@ -0,0 +1,234 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "driver/secrets_manager_proxy.h" + +#include +#include +#include + +#include +#include + +#include "test_utils.h" +#include "mock_objects.h" + +using testing::_; +using testing::InSequence; +using testing::Property; +using testing::Return; +using testing::StrEq; + +using namespace Aws::SecretsManager; + +namespace { + const Aws::String TEST_SECRET_ID{"secret_ID"}; + const Aws::String TEST_REGION{"us-east-2"}; + const Aws::String TEST_USERNAME{"test-user"}; + const Aws::String TEST_PASSWORD{"test-password"}; + const std::pair SECRET_CACHE_KEY = std::make_pair(TEST_SECRET_ID, TEST_REGION); + const auto TEST_SECRET_STRING = R"({"username": ")" + TEST_USERNAME + R"(", "password": ")" + TEST_PASSWORD + + R"("})"; + const auto TEST_SECRET = Aws::Utils::Json::JsonValue(TEST_SECRET_STRING); + const char* TEST_HOST = "test-domain"; + const auto INVALID_SECRET_STRING = "{username: invalid, password: invalid}"; +} + +static SQLHENV env; +static Aws::SDKOptions sdk_options; + +class SecretsManagerProxyTest : public testing::Test { +protected: + DBC* dbc; + DataSource* ds; + MOCK_CONNECTION_PROXY* mock_connection_proxy; + std::shared_ptr mock_sm_client; + + static void SetUpTestSuite() { + Aws::InitAPI(sdk_options); + SQLAllocHandle(SQL_HANDLE_ENV, nullptr, &env); + } + + static void TearDownTestSuite() { + SQLFreeHandle(SQL_HANDLE_ENV, env); + Aws::ShutdownAPI(sdk_options); + } + + void SetUp() override { + SQLHDBC hdbc = nullptr; + SQLAllocHandle(SQL_HANDLE_DBC, env, &hdbc); + dbc = static_cast(hdbc); + ds = ds_new(); + + ds_setattr_from_utf8(&ds->auth_region, (SQLCHAR*)TEST_REGION.c_str()); + ds_setattr_from_utf8(&ds->auth_secret_id, (SQLCHAR*)TEST_SECRET_ID.c_str()); + + mock_sm_client = std::make_shared(); + + mock_connection_proxy = new MOCK_CONNECTION_PROXY(dbc, ds); + } + + void TearDown() override { + TEST_UTILS::get_secrets_cache().clear(); + cleanup_odbc_handles(nullptr, dbc, ds); + } +}; + +TEST_F(SecretsManagerProxyTest, NullDBC) { + EXPECT_THROW(SECRETS_MANAGER_PROXY sm_proxy(nullptr, ds, mock_connection_proxy, mock_sm_client), + std::runtime_error); + delete mock_connection_proxy; +} + +TEST_F(SecretsManagerProxyTest, NullDS) { + EXPECT_THROW(SECRETS_MANAGER_PROXY sm_proxy(dbc, nullptr, mock_connection_proxy, mock_sm_client), + std::runtime_error); + delete mock_connection_proxy; +} + +// The proxy will successfully open a connection with a cached secret. +TEST_F(SecretsManagerProxyTest, TestConnectWithCachedSecrets) { + SECRETS_MANAGER_PROXY sm_proxy(dbc, ds, mock_connection_proxy, mock_sm_client); + + TEST_UTILS::get_secrets_cache().insert({SECRET_CACHE_KEY, TEST_SECRET}); + + EXPECT_CALL(*mock_sm_client, GetSecretValue(_)).Times(0); + EXPECT_CALL(*mock_connection_proxy, + connect(StrEq(TEST_HOST), StrEq(TEST_USERNAME), StrEq(TEST_PASSWORD), nullptr, 0, nullptr, 0)). + WillOnce(Return(true)); + + const auto ret = sm_proxy.connect(TEST_HOST, nullptr, nullptr, nullptr, 0, nullptr, 0); + + EXPECT_EQ(1, TEST_UTILS::get_secrets_cache().size()); + EXPECT_TRUE(ret); +} + +// The proxy will attempt to open a connection with an empty secret cache. +// The proxy will fetch the secret from the AWS Secrets Manager. +TEST_F(SecretsManagerProxyTest, TestConnectWithNewSecrets) { + SECRETS_MANAGER_PROXY sm_proxy(dbc, ds, mock_connection_proxy, mock_sm_client); + + const auto expected_result = Model::GetSecretValueResult().WithSecretString(TEST_SECRET_STRING); + const auto expected_outcome = Model::GetSecretValueOutcome(expected_result); + + EXPECT_CALL(*mock_sm_client, + GetSecretValue(Property("GetSecretId", &Aws::SecretsManager::Model::GetSecretValueRequest::GetSecretId, + StrEq(TEST_SECRET_ID)))).WillOnce(Return(expected_outcome)); + EXPECT_CALL(*mock_connection_proxy, + connect(StrEq(TEST_HOST), StrEq(TEST_USERNAME), StrEq(TEST_PASSWORD), nullptr, 0, nullptr, 0)). + WillOnce(Return(true)); + + const auto ret = sm_proxy.connect(TEST_HOST, nullptr, nullptr, nullptr, 0, nullptr, 0); + + EXPECT_EQ(1, TEST_UTILS::get_secrets_cache().size()); + EXPECT_TRUE(ret); +} + +// The proxy will attempt to open a connection with a cached secret, but it will fail with a generic error. +// In this case, the proxy will return the error back to the user. +TEST_F(SecretsManagerProxyTest, TestFailedInitialConnectionWithUnhandledError) { + SECRETS_MANAGER_PROXY sm_proxy(dbc, ds, mock_connection_proxy, mock_sm_client); + + TEST_UTILS::get_secrets_cache().insert({SECRET_CACHE_KEY, TEST_SECRET}); + + EXPECT_CALL(*mock_sm_client, GetSecretValue(_)).Times(0); + EXPECT_CALL(*mock_connection_proxy, + connect(StrEq(TEST_HOST), StrEq(TEST_USERNAME), StrEq(TEST_PASSWORD), nullptr, 0, nullptr, 0)). + WillOnce(Return(false)); + EXPECT_CALL(*mock_connection_proxy, error_code()).WillOnce(Return(ER_BAD_HOST_ERROR)); + + const auto ret = sm_proxy.connect(TEST_HOST, nullptr, nullptr, nullptr, 0, nullptr, 0); + EXPECT_FALSE(ret); +} + +// The proxy will attempt to open a connection with a cached secret, but it will fail with an access error. +// In this case, the proxy will fetch the secret and will retry the connection. +TEST_F(SecretsManagerProxyTest, TestConnectWithNewSecretsAfterTryingWithCachedSecrets) { + SECRETS_MANAGER_PROXY sm_proxy(dbc, ds, mock_connection_proxy, mock_sm_client); + + TEST_UTILS::get_secrets_cache().insert({SECRET_CACHE_KEY, TEST_SECRET}); + + const auto expected_result = Model::GetSecretValueResult().WithSecretString(TEST_SECRET_STRING); + const auto expected_outcome = Model::GetSecretValueOutcome(expected_result); + + EXPECT_CALL(*mock_sm_client, + GetSecretValue(Property("GetSecretId", &Aws::SecretsManager::Model::GetSecretValueRequest::GetSecretId, + StrEq(TEST_SECRET_ID)))).WillOnce(Return(expected_outcome)); + { + InSequence s; + + EXPECT_CALL(*mock_connection_proxy, + connect(StrEq(TEST_HOST), StrEq(TEST_USERNAME), StrEq(TEST_PASSWORD), nullptr, 0, nullptr, 0)). + WillOnce(Return(false)); + EXPECT_CALL(*mock_connection_proxy, error_code()).WillOnce(Return(ER_ACCESS_DENIED_ERROR)); + EXPECT_CALL(*mock_connection_proxy, + connect(StrEq(TEST_HOST), StrEq(TEST_USERNAME), StrEq(TEST_PASSWORD), nullptr, 0, nullptr, 0)). + WillOnce(Return(true)); + } + + const auto ret = sm_proxy.connect(TEST_HOST, nullptr, nullptr, nullptr, 0, nullptr, 0); + + EXPECT_EQ(1, TEST_UTILS::get_secrets_cache().size()); + EXPECT_TRUE(ret); +} + +// The proxy will attempt to open a connection after fetching a secret, +// but it will fail because the returned secret could not be parsed. +TEST_F(SecretsManagerProxyTest, TestFailedToReadSecrets) { + SECRETS_MANAGER_PROXY sm_proxy(dbc, ds, mock_connection_proxy, mock_sm_client); + + const auto expected_result = Model::GetSecretValueResult().WithSecretString(INVALID_SECRET_STRING); + const auto expected_outcome = Model::GetSecretValueOutcome(expected_result); + + EXPECT_CALL(*mock_sm_client, + GetSecretValue(Property("GetSecretId", &Aws::SecretsManager::Model::GetSecretValueRequest::GetSecretId, + StrEq(TEST_SECRET_ID)))).WillOnce(Return(expected_outcome)); + + EXPECT_CALL(*mock_connection_proxy, connect(_, _, _, _, _, _, _)).Times(0); + + EXPECT_THROW(sm_proxy.connect(TEST_HOST, nullptr, nullptr, nullptr, 0, nullptr, 0), std::runtime_error); + EXPECT_EQ(0, TEST_UTILS::get_secrets_cache().size()); +} + +// The proxy will attempt to open a connection after fetching a secret, +// but it will fail because the outcome from the AWS Secrets Manager is not successful. +TEST_F(SecretsManagerProxyTest, TestFailedToGetSecrets) { + SECRETS_MANAGER_PROXY sm_proxy(dbc, ds, mock_connection_proxy, mock_sm_client); + + const auto unsuccessful_outcome = Model::GetSecretValueOutcome(); + + EXPECT_CALL(*mock_sm_client, + GetSecretValue(Property("GetSecretId", &Aws::SecretsManager::Model::GetSecretValueRequest::GetSecretId, + StrEq(TEST_SECRET_ID)))).WillOnce(Return(unsuccessful_outcome)); + + EXPECT_CALL(*mock_connection_proxy, connect(_, _, _, _, _, _, _)).Times(0); + + EXPECT_THROW(sm_proxy.connect(TEST_HOST, nullptr, nullptr, nullptr, 0, nullptr, 0), std::runtime_error); + EXPECT_EQ(0, TEST_UTILS::get_secrets_cache().size()); +} diff --git a/unit_testing/test_utils.cc b/unit_testing/test_utils.cc index 56533056b..a47466c22 100644 --- a/unit_testing/test_utils.cc +++ b/unit_testing/test_utils.cc @@ -38,7 +38,7 @@ void allocate_odbc_handles(SQLHENV& env, DBC*& dbc, DataSource*& ds) { ds = ds_new(); } -void cleanup_odbc_handles(SQLHENV& env, DBC*& dbc, DataSource*& ds, bool call_myodbc_end) { +void cleanup_odbc_handles(SQLHENV env, DBC*& dbc, DataSource*& ds, bool call_myodbc_end) { SQLHDBC hdbc = static_cast(dbc); if (nullptr != hdbc) { SQLFreeHandle(SQL_HANDLE_DBC, hdbc); @@ -122,3 +122,7 @@ bool TEST_UTILS::token_cache_contains_key(std::string cache_key) { void TEST_UTILS::clear_token_cache(IAM_PROXY* iam_proxy) { iam_proxy->clear_token_cache(); } + +std::map, Aws::Utils::Json::JsonValue>& TEST_UTILS::get_secrets_cache() { + return std::ref(SECRETS_MANAGER_PROXY::secrets_cache); +} diff --git a/unit_testing/test_utils.h b/unit_testing/test_utils.h index 1412992f6..f0fb8209e 100644 --- a/unit_testing/test_utils.h +++ b/unit_testing/test_utils.h @@ -34,9 +34,10 @@ #include "driver/iam_proxy.h" #include "driver/monitor.h" #include "driver/monitor_thread_container.h" +#include "driver/secrets_manager_proxy.h" void allocate_odbc_handles(SQLHENV& env, DBC*& dbc, DataSource*& ds); -void cleanup_odbc_handles(SQLHENV& env, DBC*& dbc, DataSource*& ds, bool call_myodbc_end = false); +void cleanup_odbc_handles(SQLHENV env, DBC*& dbc, DataSource*& ds, bool call_myodbc_end = false); class TEST_UTILS { public: @@ -56,6 +57,7 @@ class TEST_UTILS { static std::string build_cache_key(const char* host, const char* region, unsigned int port, const char* user); static bool token_cache_contains_key(std::string cache_key); static void clear_token_cache(IAM_PROXY* iam_proxy); + static std::map, Aws::Utils::Json::JsonValue>& get_secrets_cache(); }; #endif /* __TESTUTILS_H__ */