Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SNOW-715510: MFA Token cache for libsnowflakeclient #773

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ set(SOURCE_FILES
include/snowflake/logger.h
include/snowflake/version.h
include/snowflake/platform.h
include/snowflake/mfa_token_cache.h
lib/client.c
lib/constants.h
lib/cJSON.h
Expand Down Expand Up @@ -128,7 +129,8 @@ set(SOURCE_FILES
lib/chunk_downloader.h
lib/chunk_downloader.c
lib/mock_http_perform.h
lib/http_perform.c)
lib/http_perform.c
)

set (SOURCE_FILES_PUT_GET
cpp/EncryptionProvider.cpp
Expand Down Expand Up @@ -213,6 +215,7 @@ set(SOURCE_FILES_CPP_WRAPPER
include/snowflake/SFURL.hpp
include/snowflake/CurlDesc.hpp
include/snowflake/CurlDescPool.hpp
cpp/jwt/jwtWrapper.cpp
cpp/lib/Exceptions.cpp
cpp/lib/Connection.cpp
cpp/lib/Statement.cpp
Expand All @@ -235,7 +238,18 @@ set(SOURCE_FILES_CPP_WRAPPER
cpp/lib/ResultSetJson.hpp
cpp/lib/Authenticator.cpp
cpp/lib/Authenticator.hpp
cpp/jwt/jwtWrapper.cpp
cpp/lib/CacheFile.cpp
cpp/lib/CacheFile.hpp
cpp/lib/CredentialCache.cpp
cpp/lib/CredentialCache.hpp
cpp/platform/SecureStorage.cpp
cpp/platform/SecureStorage.hpp
cpp/platform/SecureStorageApple.cpp
cpp/platform/SecureStorageImpl.hpp
cpp/platform/SecureStorageUnSup.cpp
cpp/platform/SecureStorageWin.cpp
cpp/platform/FileLock.cpp
cpp/platform/FileLock.hpp
cpp/util/SnowflakeCommon.cpp
cpp/util/SFURL.cpp
cpp/util/CurlDesc.cpp
Expand All @@ -245,7 +259,8 @@ set(SOURCE_FILES_CPP_WRAPPER
lib/result_set_json.h
lib/query_context_cache.h
lib/curl_desc_pool.h
lib/authenticator.h)
lib/authenticator.h
)

if (UNIX)
if (LINUX)
Expand Down Expand Up @@ -406,6 +421,7 @@ if (LINUX)
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/azure/include
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/cmocka/include
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/uuid/include
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/picojson/include
include
lib)
endif()
Expand All @@ -421,6 +437,7 @@ if (APPLE)
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/aws/include
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/azure/include
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/cmocka/include
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/picojson/include
include
lib)
endif()
Expand All @@ -435,6 +452,7 @@ if (WIN32)
deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/aws/include
deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/azure/include
deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/cmocka/include
deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/picojson/include
include
lib)
if (CMAKE_SIZEOF_VOID_P EQUAL 8)
Expand Down
224 changes: 224 additions & 0 deletions cpp/lib/CacheFile.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
#include <fstream>
#include <string>
#include <boost/filesystem.hpp>

#include "picojson.h"

#include "CacheFile.hpp"
#include "CredentialCache.hpp"
#include "snowflake/platform.h"
#include "../logger/SFLogger.hpp"

#if defined(__linux__) || defined(__APPLE__)
#include <sys/stat.h>
#endif

namespace Snowflake {

namespace Client {

const char* CREDENTIAL_FILE_NAME = "temporary_credential.json";

const std::vector<std::string> CACHE_ROOT_ENV_VARS =
{
"SF_TEMPORARY_CREDENTIAL_CACHE_DIR",
"HOME",
"TMP"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think TMP should be there

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but removing it is a BCR in ODBC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will make changes after security modeling is complete

};

const std::vector<std::string> CACHE_DIR_PATH =
{
".cache",
"snowflake"
};


bool mkdirIfNotExists(const char *dir)
{
int result = sf_mkdir(dir);
if (result == 0)
{
CXX_LOG_DEBUG("Created %s directory.", dir);
return true;
}

if (errno == EEXIST)
{
CXX_LOG_TRACE("Directory %s already exists.", dir);
return true;
}

CXX_LOG_ERROR("Failed to create %s directory. Error: %d", dir, errno);
return false;

}

boost::optional<std::string> findCacheDirRoot() {
for (auto const &envVar: CACHE_ROOT_ENV_VARS)
{
char *root = getenv(envVar.c_str());
if (root != nullptr)
{
return std::string(root);
}
}
return {};
}

boost::optional<std::string> getCredentialFilePath()
{
const auto cacheDirRootOpt = findCacheDirRoot();
if (!cacheDirRootOpt)
{
return {};
}
const std::string &cacheDirRoot = cacheDirRootOpt.value();

if (!mkdirIfNotExists(cacheDirRoot.c_str()))
{
return {};
}

std::string cacheDir = cacheDirRoot;
for (const auto &segment: CACHE_DIR_PATH)
{
cacheDir += PATH_SEP + segment;
if (!mkdirIfNotExists(cacheDir.c_str()))
{
return {};
}
}
return cacheDir + PATH_SEP + CREDENTIAL_FILE_NAME;
};

std::string credItemStr(const CredentialKey &key)
{
// TODO Make :SNOWFLAKE-ODBC-DRIVER: part more generic (support generic driver, PHP etc.)
return key.host + ":" + key.user + ":SNOWFLAKE-ODBC-DRIVER:" + credTypeToString(key.type);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be hardcoded to ODBC-DRIVER? I wonder if there's some way to determine whether libsfclient is executed within ODBC or PHP driver or possibly even other drivers in the future.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should be configurable and supported not only for ODBC but also PHP

}

void ensureObject(picojson::value &val)
{
if (!val.is<picojson::object>())
{
val = picojson::value(picojson::object());
}
}

std::string readFile(const std::string &path, picojson::value &result) {
if (!boost::filesystem::exists(path))
{
result = picojson::value(picojson::object());
return {};
}

std::ifstream cacheFile(path);
if (!cacheFile.is_open())
{
return "Failed to open the file(path=" + path + ")";
}

std::string error = picojson::parse(result, cacheFile);
if (!error.empty())
{
return "Failed to parse the file: " + error;
}
return {};
}

#if defined(__linux__) || defined(__APPLE__)
bool ensurePermissions(const std::string& path, mode_t mode)
{
if (chmod(path.c_str(), mode) == -1)
{
CXX_LOG_ERROR("Cannot ensure permissions. chmod(%s, %o) failed with errno=%d", path.c_str(), mode, errno);
return false;
}

return true;
}
#else
bool ensurePermissions(const std::string& path, unsigned mode)
{
CXX_LOG_ERROR("Cannot ensure permissions on current platform");
return false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it ok that for WINDOWS it will be always false?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File caches are not used on windows at all. This definition ensures that the code compiles on windows, but in practice it's never used.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe then just make the code unnacessible from the beginning? For MacOS are we using the file cache or some secure storage?

}
#endif

std::string writeFile(const std::string &path, const picojson::value &result) {
std::ofstream cacheFile(path, std::ios_base::trunc);
if (!cacheFile.is_open())
{
return "Failed to open the file";
}

if (!ensurePermissions(path, 0700))
{
return "Cannot ensure correct permissions on a file";
}

cacheFile << result.serialize(true);
return {};
}

void cacheFileUpdate(picojson::value &cache, const CredentialKey &key, const std::string &credential)
{
ensureObject(cache);
picojson::object &obj = cache.get<picojson::object>();
std::pair<picojson::object::iterator, bool> pair = obj.emplace(key.account, picojson::value(picojson::object()));
auto accountCacheIt = pair.first;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Here I also wasn't sure why we're calling pair.first then accountCacheIt->second gets the credential value. I admit it's a skill issue but would appreciate getting verbose type instead of auto either here or for pair

Copy link
Contributor Author

@sfc-gh-jszczerbinski sfc-gh-jszczerbinski Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. picojson::object in pratice is an alias for std::map<std::string, picojson::value>
    typedef std::map<std::string, value> object;
  1. Doc of the emplace method https://en.cppreference.com/w/cpp/container/map/emplace
  2. This code would be clear with more modern c++
/* inserted is true if key.account wasn't in obj and we added new element */
 auto [accountCacheIt, inserted] = obj.emplace(key.account, picojson::value(picojson::object())); 


ensureObject(accountCacheIt->second);
accountCacheIt->second.get<picojson::object>().emplace(credItemStr(key), credential);
}

void cacheFileRemove(picojson::value &cache, const CredentialKey &key)
{
ensureObject(cache);
picojson::object &cacheObj = cache.get<picojson::object>();

auto accountCacheIt = cacheObj.find(key.account);
if (accountCacheIt == cacheObj.end())
{
return;
}

ensureObject(accountCacheIt->second);
picojson::object &accountCacheObj = accountCacheIt->second.get<picojson::object>();
accountCacheObj.erase(credItemStr(key));
if (accountCacheObj.empty())
{
cacheObj.erase(accountCacheIt);
}
}

boost::optional<std::string> cacheFileGet(picojson::value &cache, const CredentialKey &key) {
ensureObject(cache);
picojson::object &cacheObj = cache.get<picojson::object>();

auto accountCacheIt = cacheObj.find(key.account);
if (accountCacheIt == cacheObj.end())
{
return {};
}

ensureObject(accountCacheIt->second);
picojson::object &accountCacheObj = accountCacheIt->second.get<picojson::object>();
auto it = accountCacheObj.find(credItemStr(key));

if (it == accountCacheObj.end())
{
return {};
}

if (!it->second.is<std::string>())
{
return {};
}

return it->second.get<std::string>();
}

}

}
31 changes: 31 additions & 0 deletions cpp/lib/CacheFile.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#ifndef SNOWFLAKECLIENT_CACHEFILE_HPP
#define SNOWFLAKECLIENT_CACHEFILE_HPP

#include <string>
#include <fstream>

#include "picojson.h"

#include "CredentialCache.hpp"

namespace Snowflake {

namespace Client {

boost::optional<std::string> getCredentialFilePath();

std::string readFile(const std::string &path, picojson::value &result);

std::string writeFile(const std::string &path, const picojson::value &result);

void cacheFileUpdate(picojson::value &cache, const CredentialKey &key, const std::string &credential);

void cacheFileRemove(picojson::value &cache, const CredentialKey &key);

boost::optional<std::string> cacheFileGet(picojson::value &cache, const CredentialKey &key);

}

}

#endif // SNOWFLAKECLIENT_CACHEFILE_HPP
Loading
Loading