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: Token cache for libsnowflakeclient #773

Merged
merged 25 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
30facc3
Move SecureStorageImplementation from ODBC
sfc-gh-jszczerbinski Nov 7, 2024
e83e5d2
Implement MFA Token Caching
sfc-gh-jszczerbinski Oct 9, 2024
1fb0bce
After review fixes
sfc-gh-jszczerbinski Nov 19, 2024
9f8fdf2
Adjust implementation to design doc
sfc-gh-jszczerbinski Jan 9, 2025
f79acba
Fix compilation errors on windows
sfc-gh-jszczerbinski Jan 13, 2025
a46a3c8
Add more tests and apply fixes
sfc-gh-jszczerbinski Jan 28, 2025
0405da5
After review fixes
sfc-gh-jszczerbinski Jan 14, 2025
c96937f
Add more tests and apply fixes
sfc-gh-jszczerbinski Jan 15, 2025
6f86af8
Fix test and disable cred_cache
sfc-gh-jszczerbinski Jan 28, 2025
bda119d
Fix
sfc-gh-jszczerbinski Jan 28, 2025
a628e70
Fix
sfc-gh-jszczerbinski Jan 28, 2025
87c1e77
After review fixes
sfc-gh-jszczerbinski Jan 29, 2025
68b0b6b
Refactor
sfc-gh-jszczerbinski Jan 30, 2025
4ea5532
Remove jwt
sfc-gh-jszczerbinski Jan 30, 2025
1988980
Fix tests
sfc-gh-jszczerbinski Jan 30, 2025
639e9c1
Add negative tests
sfc-gh-jszczerbinski Jan 31, 2025
a37fe6b
UNUSED -> SF_UNUSED
sfc-gh-jszczerbinski Jan 31, 2025
87ada32
Increase coverage
sfc-gh-jszczerbinski Feb 3, 2025
758443c
Move functions to anonymous namespace and fix include order
sfc-gh-jszczerbinski Feb 3, 2025
c6620b6
Merge branch 'master' into SNOW-715510-mfa-token-cache-for-c-api-3
sfc-gh-jszczerbinski Feb 3, 2025
df4f7f6
Fix bug in SecureStorage::removeToken
sfc-gh-jszczerbinski Feb 3, 2025
f92e2e1
Add jira number to TODO and add debug logs to successful execution path
sfc-gh-jszczerbinski Feb 4, 2025
1d464b3
Merge branch 'master' into SNOW-715510-mfa-token-cache-for-c-api-3
sfc-gh-jszczerbinski Feb 4, 2025
26b548f
Review fixes
sfc-gh-jszczerbinski Feb 4, 2025
300c04f
Merge branch 'master' into SNOW-715510-mfa-token-cache-for-c-api-3
sfc-gh-jszczerbinski Feb 5, 2025
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
25 changes: 23 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ set(SOURCE_FILES
include/snowflake/logger.h
include/snowflake/version.h
include/snowflake/platform.h
include/snowflake/secure_storage.h
lib/client.c
lib/constants.h
lib/cJSON.h
Expand Down Expand Up @@ -63,7 +64,9 @@ set(SOURCE_FILES
lib/chunk_downloader.h
lib/chunk_downloader.c
lib/mock_http_perform.h
lib/http_perform.c)
lib/http_perform.c
cpp/lib/CacheFile.cpp
)

set (SOURCE_FILES_PUT_GET
cpp/EncryptionProvider.cpp
Expand Down Expand Up @@ -150,6 +153,7 @@ set(SOURCE_FILES_CPP_WRAPPER
include/snowflake/CurlDesc.hpp
include/snowflake/CurlDescPool.hpp
include/snowflake/BindUploader.hpp
include/snowflake/SecureStorage.hpp
cpp/lib/Exceptions.cpp
cpp/lib/Connection.cpp
cpp/lib/Statement.cpp
Expand All @@ -173,14 +177,27 @@ set(SOURCE_FILES_CPP_WRAPPER
cpp/lib/BindUploader.cpp
cpp/lib/ClientBindUploader.hpp
cpp/lib/ClientBindUploader.cpp
cpp/lib/CacheFile.cpp
cpp/lib/CacheFile.hpp
cpp/platform/secure_storage.cpp
cpp/platform/SecureStorage.cpp
cpp/platform/SecureStorageApple.cpp
cpp/platform/SecureStorageLinux.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
cpp/util/CurlDescPool.cpp
cpp/util/Sha256.cpp
cpp/util/Sha256.hpp
cpp/platform/SecureStorage.cpp
lib/result_set.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 @@ -284,6 +301,7 @@ if (WIN32)
find_library(BOOST_REGEX_LIB boost_regex-vc140-mt-gd.lib PATHS deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/boost/lib/ REQUIRED NO_DEFAULT_PATH)
find_library(BOOST_SYSTEM_LIB boost_system-vc140-mt-gd.lib PATHS deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/boost/lib/ REQUIRED NO_DEFAULT_PATH)
else()
message(deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/boost/lib/)
find_library(BOOST_FILESYSTEM_LIB boost_filesystem-vc140-mt.lib PATHS deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/boost/lib/ REQUIRED NO_DEFAULT_PATH)
find_library(BOOST_REGEX_LIB boost_regex-vc140-mt.lib PATHS deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/boost/lib/ REQUIRED NO_DEFAULT_PATH)
find_library(BOOST_SYSTEM_LIB boost_system-vc140-mt.lib PATHS deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/boost/lib/ REQUIRED NO_DEFAULT_PATH)
Expand Down Expand Up @@ -339,6 +357,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 @@ -354,6 +373,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 @@ -368,6 +388,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
262 changes: 262 additions & 0 deletions cpp/lib/CacheFile.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
/*
* File: CacheFile.cpp *
* Copyright (c) 2025 Snowflake Computing
*/

#include "CacheFile.hpp"

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

#include <fstream>
#include <string>
#include <sstream>

#include <picojson.h>
#include <boost/filesystem.hpp>

#include "snowflake/platform.h"
#include "../logger/SFLogger.hpp"
#include "../util/Sha256.hpp"

namespace {
using namespace Snowflake::Client;
const char* CREDENTIAL_FILE_NAME = "credential_cache_v1.json";

bool mkdirIfNotExists(const std::string& dir)
{
int result = sf_mkdir(dir.c_str());
if (result == 0)
{
CXX_LOG_DEBUG("Created %s directory.", dir.c_str());
return true;
}

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

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

}

boost::optional<std::string> getEnv(const std::string& envVar)
{
char *root = getenv(envVar.c_str());

if (root == nullptr)
{
return {};
}

return std::string(root);
}

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

picojson::object& getTokens(picojson::value& cache)
{
ensureObject(cache);
auto &obj = cache.get<picojson::object>();
auto pair = obj.emplace("tokens", picojson::value(picojson::object()));
auto& tokens = pair.first->second;
ensureObject(tokens);
return tokens.get<picojson::object>();
}

#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;
}
#endif
}

namespace Snowflake {

namespace Client {
boost::optional<std::string> getCacheDir(const std::string& envVar, const std::vector<std::string>& subPathSegments)
{
#ifdef __linux__
auto envVarValueOpt = getEnv(envVar);
if (!envVarValueOpt)
{
return {};
}

const std::string& envVarValue = envVarValueOpt.get();

struct stat s = {};
int err = stat(envVarValue.c_str(), &s);

if (err != 0)
{
CXX_LOG_INFO("Failed to stat %s=%s, errno=%d. Skipping it in cache file location lookup.", envVar.c_str(), envVarValue.c_str(), errno);
return {};
}

if (!S_ISDIR(s.st_mode))
{
CXX_LOG_INFO("%s=%s is not a directory. Skipping it in cache file location lookup.", envVar.c_str(), envVarValue.c_str());
return {};
}

auto cacheDir = envVarValue;
for (const auto& segment: subPathSegments)
{
cacheDir.append(PATH_SEP + segment);
if (!mkdirIfNotExists(cacheDir))
{
CXX_LOG_INFO("Could not create cache dir=%s. Skipping it in cache file location lookup.", cacheDir.c_str());
return {};
}
}

if (!subPathSegments.empty())
{
err = stat(cacheDir.c_str(), &s);
if (err != 0)
{
CXX_LOG_INFO("Failed to stat %s, errno=%d. Skipping it in cache file location lookup.", cacheDir.c_str(), errno);
return {};
}
}

if (s.st_uid != geteuid())
{
CXX_LOG_INFO("%s=%s is not owned by current user. Skipping it in cache file location lookup.", envVar.c_str(), envVarValue.c_str());
return {};
}

unsigned permissions = s.st_mode & 0777;
if (permissions != 0700)
{
CXX_LOG_INFO("Incorrect permissions=%o for cache dir %s. Changing permissions to 700.", permissions, cacheDir.c_str())
if (chmod(cacheDir.c_str(), 0700) != 0)
{
CXX_LOG_WARN("Failed to change permissions for a cache dir %s, errno=%d. Skipping it in cache file location lookup.", cacheDir.c_str(), errno);
return {};
}
}
return cacheDir;
#else
CXX_LOG_FATAL("Using NOOP implementation. This function is implemented only for linux.");
return {};
#endif
}

boost::optional<std::string> getCredentialFilePath()
{
std::vector<std::function<boost::optional<std::string>()>> lookupFunctions =
{
[]() { return getCacheDir("SF_TEMPORARY_CREDENTIAL_CACHE_DIR", {}); },
#ifdef __linux__
[](){ return getCacheDir("XDG_CACHE_HOME", {"snowflake"}); },
#endif
[](){ return getCacheDir("HOME", {".cache", "snowflake"}); },
};

for (const auto& lf: lookupFunctions) {
boost::optional<std::string> directory = lf();
if (directory)
{
auto path = directory.get() + PATH_SEP + CREDENTIAL_FILE_NAME;
CXX_LOG_TRACE("Successfully found credential file path=%s", path.c_str());
return path;
}
}

return {};
};

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 {};
}

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, 0600))
{
return "Cannot ensure correct permissions on a file";
}

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

void cacheFileUpdate(picojson::value &cache, const std::string &key, const std::string &credential)
{
picojson::object& tokens = getTokens(cache);
tokens.emplace(key, credential);
}

void cacheFileRemove(picojson::value &cache, const std::string &key)
{
picojson::object& tokens = getTokens(cache);
tokens.erase(key);
}

boost::optional<std::string> cacheFileGet(picojson::value &cache, const std::string &key) {
picojson::object& tokens = getTokens(cache);
auto it = tokens.find(key);

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

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

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

}

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

#include <string>
#include <fstream>

#include <boost/optional.hpp>
#include <picojson.h>

namespace Snowflake {

namespace Client {

boost::optional<std::string> getCacheDir(const std::string& envVar, const std::vector<std::string>& subPathSegments);

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 std::string &key, const std::string &credential);

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

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

}

}

#endif // SNOWFLAKECLIENT_CACHEFILE_HPP
Loading
Loading