-
Notifications
You must be signed in to change notification settings - Fork 28
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
base: master
Are you sure you want to change the base?
Changes from all commits
5fcb08c
13a5835
26b6dfe
b6fe929
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
}; | ||
|
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it ok that for WINDOWS it will be always false? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
typedef std::map<std::string, value> object;
/* 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>(); | ||
} | ||
|
||
} | ||
|
||
} |
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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