Skip to content

Commit 954c938

Browse files
SNOW-715510: Token cache for libsnowflakeclient (#773)
1 parent fedf1f9 commit 954c938

24 files changed

+1758
-11
lines changed

CMakeLists.txt

+23-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ set(SOURCE_FILES
3131
include/snowflake/logger.h
3232
include/snowflake/version.h
3333
include/snowflake/platform.h
34+
include/snowflake/secure_storage.h
3435
lib/client.c
3536
lib/constants.h
3637
lib/cJSON.h
@@ -63,7 +64,9 @@ set(SOURCE_FILES
6364
lib/chunk_downloader.h
6465
lib/chunk_downloader.c
6566
lib/mock_http_perform.h
66-
lib/http_perform.c)
67+
lib/http_perform.c
68+
cpp/lib/CacheFile.cpp
69+
)
6770

6871
set (SOURCE_FILES_PUT_GET
6972
cpp/EncryptionProvider.cpp
@@ -150,6 +153,7 @@ set(SOURCE_FILES_CPP_WRAPPER
150153
include/snowflake/CurlDesc.hpp
151154
include/snowflake/CurlDescPool.hpp
152155
include/snowflake/BindUploader.hpp
156+
include/snowflake/SecureStorage.hpp
153157
cpp/lib/Exceptions.cpp
154158
cpp/lib/Connection.cpp
155159
cpp/lib/Statement.cpp
@@ -173,14 +177,27 @@ set(SOURCE_FILES_CPP_WRAPPER
173177
cpp/lib/BindUploader.cpp
174178
cpp/lib/ClientBindUploader.hpp
175179
cpp/lib/ClientBindUploader.cpp
180+
cpp/lib/CacheFile.cpp
181+
cpp/lib/CacheFile.hpp
182+
cpp/platform/secure_storage.cpp
183+
cpp/platform/SecureStorage.cpp
184+
cpp/platform/SecureStorageApple.cpp
185+
cpp/platform/SecureStorageLinux.cpp
186+
cpp/platform/SecureStorageWin.cpp
187+
cpp/platform/FileLock.cpp
188+
cpp/platform/FileLock.hpp
176189
cpp/util/SnowflakeCommon.cpp
177190
cpp/util/SFURL.cpp
178191
cpp/util/CurlDesc.cpp
179192
cpp/util/CurlDescPool.cpp
193+
cpp/util/Sha256.cpp
194+
cpp/util/Sha256.hpp
195+
cpp/platform/SecureStorage.cpp
180196
lib/result_set.h
181197
lib/query_context_cache.h
182198
lib/curl_desc_pool.h
183-
lib/authenticator.h)
199+
lib/authenticator.h
200+
)
184201

185202
if (UNIX)
186203
if (LINUX)
@@ -284,6 +301,7 @@ if (WIN32)
284301
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)
285302
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)
286303
else()
304+
message(deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/boost/lib/)
287305
find_library(BOOST_FILESYSTEM_LIB boost_filesystem-vc140-mt.lib PATHS deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/boost/lib/ REQUIRED NO_DEFAULT_PATH)
288306
find_library(BOOST_REGEX_LIB boost_regex-vc140-mt.lib PATHS deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/boost/lib/ REQUIRED NO_DEFAULT_PATH)
289307
find_library(BOOST_SYSTEM_LIB boost_system-vc140-mt.lib PATHS deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/boost/lib/ REQUIRED NO_DEFAULT_PATH)
@@ -339,6 +357,7 @@ if (LINUX)
339357
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/azure/include
340358
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/cmocka/include
341359
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/uuid/include
360+
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/picojson/include
342361
include
343362
lib)
344363
endif()
@@ -354,6 +373,7 @@ if (APPLE)
354373
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/aws/include
355374
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/azure/include
356375
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/cmocka/include
376+
deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/picojson/include
357377
include
358378
lib)
359379
endif()
@@ -368,6 +388,7 @@ if (WIN32)
368388
deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/aws/include
369389
deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/azure/include
370390
deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/cmocka/include
391+
deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/picojson/include
371392
include
372393
lib)
373394
if (CMAKE_SIZEOF_VOID_P EQUAL 8)

cpp/lib/CacheFile.cpp

+262
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
/*
2+
* File: CacheFile.cpp *
3+
* Copyright (c) 2025 Snowflake Computing
4+
*/
5+
6+
#include "CacheFile.hpp"
7+
8+
#if defined(__linux__) || defined(__APPLE__)
9+
#include <sys/stat.h>
10+
#endif
11+
12+
#include <fstream>
13+
#include <string>
14+
#include <sstream>
15+
16+
#include <picojson.h>
17+
#include <boost/filesystem.hpp>
18+
19+
#include "snowflake/platform.h"
20+
#include "../logger/SFLogger.hpp"
21+
#include "../util/Sha256.hpp"
22+
23+
namespace {
24+
using namespace Snowflake::Client;
25+
const char* CREDENTIAL_FILE_NAME = "credential_cache_v1.json";
26+
27+
bool mkdirIfNotExists(const std::string& dir)
28+
{
29+
int result = sf_mkdir(dir.c_str());
30+
if (result == 0)
31+
{
32+
CXX_LOG_DEBUG("Created %s directory.", dir.c_str());
33+
return true;
34+
}
35+
36+
if (errno == EEXIST)
37+
{
38+
CXX_LOG_TRACE("Directory %s already exists.", dir.c_str());
39+
return true;
40+
}
41+
42+
CXX_LOG_ERROR("Failed to create %s directory. Error: %d", dir.c_str(), errno);
43+
return false;
44+
45+
}
46+
47+
boost::optional<std::string> getEnv(const std::string& envVar)
48+
{
49+
char *root = getenv(envVar.c_str());
50+
51+
if (root == nullptr)
52+
{
53+
return {};
54+
}
55+
56+
return std::string(root);
57+
}
58+
59+
void ensureObject(picojson::value &val)
60+
{
61+
if (!val.is<picojson::object>())
62+
{
63+
val = picojson::value(picojson::object());
64+
}
65+
}
66+
67+
picojson::object& getTokens(picojson::value& cache)
68+
{
69+
ensureObject(cache);
70+
auto &obj = cache.get<picojson::object>();
71+
auto pair = obj.emplace("tokens", picojson::value(picojson::object()));
72+
auto& tokens = pair.first->second;
73+
ensureObject(tokens);
74+
return tokens.get<picojson::object>();
75+
}
76+
77+
#if defined(__linux__) || defined(__APPLE__)
78+
bool ensurePermissions(const std::string& path, mode_t mode)
79+
{
80+
if (chmod(path.c_str(), mode) == -1)
81+
{
82+
CXX_LOG_ERROR("Cannot ensure permissions. chmod(%s, %o) failed with errno=%d", path.c_str(), mode, errno);
83+
return false;
84+
}
85+
86+
return true;
87+
}
88+
#else
89+
bool ensurePermissions(const std::string& path, unsigned mode)
90+
{
91+
CXX_LOG_ERROR("Cannot ensure permissions on current platform");
92+
return false;
93+
}
94+
#endif
95+
}
96+
97+
namespace Snowflake {
98+
99+
namespace Client {
100+
boost::optional<std::string> getCacheDir(const std::string& envVar, const std::vector<std::string>& subPathSegments)
101+
{
102+
#ifdef __linux__
103+
auto envVarValueOpt = getEnv(envVar);
104+
if (!envVarValueOpt)
105+
{
106+
return {};
107+
}
108+
109+
const std::string& envVarValue = envVarValueOpt.get();
110+
111+
struct stat s = {};
112+
int err = stat(envVarValue.c_str(), &s);
113+
114+
if (err != 0)
115+
{
116+
CXX_LOG_INFO("Failed to stat %s=%s, errno=%d. Skipping it in cache file location lookup.", envVar.c_str(), envVarValue.c_str(), errno);
117+
return {};
118+
}
119+
120+
if (!S_ISDIR(s.st_mode))
121+
{
122+
CXX_LOG_INFO("%s=%s is not a directory. Skipping it in cache file location lookup.", envVar.c_str(), envVarValue.c_str());
123+
return {};
124+
}
125+
126+
auto cacheDir = envVarValue;
127+
for (const auto& segment: subPathSegments)
128+
{
129+
cacheDir.append(PATH_SEP + segment);
130+
if (!mkdirIfNotExists(cacheDir))
131+
{
132+
CXX_LOG_INFO("Could not create cache dir=%s. Skipping it in cache file location lookup.", cacheDir.c_str());
133+
return {};
134+
}
135+
}
136+
137+
if (!subPathSegments.empty())
138+
{
139+
err = stat(cacheDir.c_str(), &s);
140+
if (err != 0)
141+
{
142+
CXX_LOG_INFO("Failed to stat %s, errno=%d. Skipping it in cache file location lookup.", cacheDir.c_str(), errno);
143+
return {};
144+
}
145+
}
146+
147+
if (s.st_uid != geteuid())
148+
{
149+
CXX_LOG_INFO("%s=%s is not owned by current user. Skipping it in cache file location lookup.", envVar.c_str(), envVarValue.c_str());
150+
return {};
151+
}
152+
153+
unsigned permissions = s.st_mode & 0777;
154+
if (permissions != 0700)
155+
{
156+
CXX_LOG_INFO("Incorrect permissions=%o for cache dir %s. Changing permissions to 700.", permissions, cacheDir.c_str())
157+
if (chmod(cacheDir.c_str(), 0700) != 0)
158+
{
159+
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);
160+
return {};
161+
}
162+
}
163+
return cacheDir;
164+
#else
165+
CXX_LOG_FATAL("Using NOOP implementation. This function is implemented only for linux.");
166+
return {};
167+
#endif
168+
}
169+
170+
boost::optional<std::string> getCredentialFilePath()
171+
{
172+
std::vector<std::function<boost::optional<std::string>()>> lookupFunctions =
173+
{
174+
[]() { return getCacheDir("SF_TEMPORARY_CREDENTIAL_CACHE_DIR", {}); },
175+
#ifdef __linux__
176+
[](){ return getCacheDir("XDG_CACHE_HOME", {"snowflake"}); },
177+
#endif
178+
[](){ return getCacheDir("HOME", {".cache", "snowflake"}); },
179+
};
180+
181+
for (const auto& lf: lookupFunctions) {
182+
boost::optional<std::string> directory = lf();
183+
if (directory)
184+
{
185+
auto path = directory.get() + PATH_SEP + CREDENTIAL_FILE_NAME;
186+
CXX_LOG_TRACE("Successfully found credential file path=%s", path.c_str());
187+
return path;
188+
}
189+
}
190+
191+
return {};
192+
};
193+
194+
std::string readFile(const std::string &path, picojson::value &result) {
195+
if (!boost::filesystem::exists(path))
196+
{
197+
result = picojson::value(picojson::object());
198+
return {};
199+
}
200+
201+
std::ifstream cacheFile(path);
202+
if (!cacheFile.is_open())
203+
{
204+
return "Failed to open the file(path=" + path + ")";
205+
}
206+
207+
std::string error = picojson::parse(result, cacheFile);
208+
if (!error.empty())
209+
{
210+
return "Failed to parse the file: " + error;
211+
}
212+
return {};
213+
}
214+
215+
std::string writeFile(const std::string &path, const picojson::value &result) {
216+
std::ofstream cacheFile(path, std::ios_base::trunc);
217+
if (!cacheFile.is_open())
218+
{
219+
return "Failed to open the file";
220+
}
221+
222+
if (!ensurePermissions(path, 0600))
223+
{
224+
return "Cannot ensure correct permissions on a file";
225+
}
226+
227+
cacheFile << result.serialize(true);
228+
return {};
229+
}
230+
231+
void cacheFileUpdate(picojson::value &cache, const std::string &key, const std::string &credential)
232+
{
233+
picojson::object& tokens = getTokens(cache);
234+
tokens.emplace(key, credential);
235+
}
236+
237+
void cacheFileRemove(picojson::value &cache, const std::string &key)
238+
{
239+
picojson::object& tokens = getTokens(cache);
240+
tokens.erase(key);
241+
}
242+
243+
boost::optional<std::string> cacheFileGet(picojson::value &cache, const std::string &key) {
244+
picojson::object& tokens = getTokens(cache);
245+
auto it = tokens.find(key);
246+
247+
if (it == tokens.end())
248+
{
249+
return {};
250+
}
251+
252+
if (!it->second.is<std::string>())
253+
{
254+
return {};
255+
}
256+
257+
return it->second.get<std::string>();
258+
}
259+
260+
}
261+
262+
}

cpp/lib/CacheFile.hpp

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#ifndef SNOWFLAKECLIENT_CACHEFILE_HPP
2+
#define SNOWFLAKECLIENT_CACHEFILE_HPP
3+
4+
#include <string>
5+
#include <fstream>
6+
7+
#include <boost/optional.hpp>
8+
#include <picojson.h>
9+
10+
namespace Snowflake {
11+
12+
namespace Client {
13+
14+
boost::optional<std::string> getCacheDir(const std::string& envVar, const std::vector<std::string>& subPathSegments);
15+
16+
boost::optional<std::string> getCredentialFilePath();
17+
18+
std::string readFile(const std::string &path, picojson::value &result);
19+
20+
std::string writeFile(const std::string &path, const picojson::value &result);
21+
22+
void cacheFileUpdate(picojson::value &cache, const std::string &key, const std::string &credential);
23+
24+
void cacheFileRemove(picojson::value &cache, const std::string &key);
25+
26+
boost::optional<std::string> cacheFileGet(picojson::value &cache, const std::string &key);
27+
28+
}
29+
30+
}
31+
32+
#endif // SNOWFLAKECLIENT_CACHEFILE_HPP

0 commit comments

Comments
 (0)