-
Notifications
You must be signed in to change notification settings - Fork 141
SNOW-1825789 Secure token cache #1012
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
base: master
Are you sure you want to change the base?
Changes from 3 commits
d72bac3
89e3b4b
ada2872
f638e60
c9c6bd6
a431cb8
770e3b1
7e3dc4f
dcc6d2e
d51750d
b0c0752
1c3dc35
f813df5
9cb1c16
03e376f
800a58a
47a684a
7f4afd4
ef6e428
d279ca6
52a088c
43e1040
6d49ccf
1778e9f
7a1b31d
c99010b
cdc5484
3ba209a
948cd27
8e9e85d
13e04df
90f758d
f1e8ebf
74c6d1c
5f2cf3c
d5898c9
2d65bbf
47e6449
3f66eb2
dc36c39
3097c53
c036882
f2ea3b3
a24a749
50e7e39
25f6ccb
86fd21b
3514cd7
81a56df
24995a4
3048378
0140865
76462a5
1d15e2e
380c2f0
cf9b613
55a6b4b
aed35d0
29c74f2
669e214
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 |
---|---|---|
|
@@ -4,7 +4,7 @@ const fs = require('node:fs/promises'); | |
const Util = require('../../util'); | ||
const os = require('os'); | ||
const crypto = require('crypto'); | ||
const { getSecureHandle } = require('../../file_util'); | ||
const { getSecureHandle, closeHandle } = require('../../file_util'); | ||
|
||
const defaultJsonTokenCachePaths = { | ||
'win32': ['AppData', 'Local', 'Snowflake', 'Caches'], | ||
|
@@ -21,68 +21,66 @@ function JsonCredentialManager(credentialCacheDir, timeoutMs = 60000) { | |
|
||
this.getTokenDirCandidates = function () { | ||
const candidates = []; | ||
if (Util.exists(credentialCacheDir)) { | ||
candidates.push({ folder: credentialCacheDir, subfolders: [] }); | ||
} | ||
const sfTemp = process.env.SF_TEMPORARY_CREDENTIAL_CACHE_DIR; | ||
if (Util.exists(sfTemp)) { | ||
candidates.push({ folder: sfTemp, subfolders: [] }); | ||
} | ||
const xdgCache = process.env.XDG_CACHE_HOME; | ||
if (Util.exists(xdgCache) && process.platform === 'linux') { | ||
candidates.push({ folder: xdgCache, subfolders: ['snowflake'] }); | ||
} | ||
const home = process.env.HOME; | ||
candidates.push({ folder: credentialCacheDir, subfolders: [] }); | ||
|
||
candidates.push({ folder: process.env.SF_TEMPORARY_CREDENTIAL_CACHE_DIR, subfolders: [] }); | ||
|
||
switch (process.platform) { | ||
case 'win32': | ||
candidates.push({ folder: os.homedir(), subfolders: module.exports.defaultJsonTokenCachePaths['win32'] }); | ||
candidates.push({ folder: os.homedir(), subfolders: defaultJsonTokenCachePaths['win32'] }); | ||
break; | ||
case 'linux': | ||
if (Util.exists(home)) { | ||
candidates.push({ folder: home, subfolders: defaultJsonTokenCachePaths['linux'] }); | ||
} | ||
candidates.push({ folder: process.env.XDG_CACHE_HOME, subfolders: ['snowflake'] }); | ||
candidates.push({ folder: process.env.HOME, subfolders: defaultJsonTokenCachePaths['linux'] }); | ||
break; | ||
case 'darwin': | ||
if (Util.exists(home)) { | ||
candidates.push({ folder: home, subfolders: defaultJsonTokenCachePaths['darwin'] }); | ||
} | ||
candidates.push({ folder: process.env.HOME, subfolders: defaultJsonTokenCachePaths['darwin'] }); | ||
} | ||
return candidates; | ||
}; | ||
|
||
this.createCacheDir = async function (cacheDir) { | ||
const options = { recursive: true }; | ||
if (process.platform !== 'win32') { | ||
options.mode = 0o755; | ||
} | ||
await fs.mkdir(cacheDir, options); | ||
if (process.platform !== 'win32') { | ||
await fs.chmod(cacheDir, 0o700); | ||
} | ||
}; | ||
|
||
this.tryTokenDir = async function (dir, subDirs) { | ||
if (!Util.exists(dir)) { | ||
return false; | ||
} | ||
const cacheDir = path.join(dir, ...subDirs); | ||
try { | ||
const stat = await fs.stat(dir); | ||
if (!stat.isDirectory()) { | ||
Logger.getInstance().info(`Path ${dir} is not a directory`); | ||
return false; | ||
} | ||
const cacheStat = await fs.lstat(cacheDir).catch((err) => { | ||
const cacheStat = await fs.stat(cacheDir).catch(async (err) => { | ||
if (err.code !== 'ENOENT') { | ||
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 create dir here when ENOENT? Then stat it again and recover from error? 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. Good point, changed that |
||
throw err; | ||
} | ||
await this.createCacheDir(cacheDir); | ||
return await fs.stat(cacheDir); | ||
}); | ||
if (!Util.exists(cacheStat)) { | ||
const options = { recursive: true }; | ||
if (process.platform !== 'win32') { | ||
options.mode = 0o700; | ||
} | ||
await fs.mkdir(cacheDir, options); | ||
if (!cacheStat.isDirectory()) { | ||
return false; | ||
} | ||
if (process.platform === 'win32') { | ||
return true; | ||
} else { | ||
if (!stat.isDirectory()) { | ||
return false; | ||
} | ||
if (process.platform === 'win32') { | ||
return true; | ||
} | ||
if ((cacheStat.mode & 0o777) === 0o700) { | ||
return true; | ||
} | ||
} | ||
if ((cacheStat.mode & 0o777) !== 0o700 && cacheStat.uid === os.userInfo().uid) { | ||
Logger.getInstance().warn(`Token cache directory ${cacheDir} has unsecure permissions.`); | ||
return true; | ||
} | ||
if (cacheStat.uid !== os.userInfo().uid) { | ||
Logger.getInstance().warn(`Token cache directory ${cacheDir} has unsecure owner.`); | ||
} | ||
return true; | ||
} catch (err) { | ||
Logger.getInstance().warn(`The path location ${cacheDir} is invalid. Please check this location is accessible or existing`); | ||
return false; | ||
|
@@ -95,26 +93,26 @@ function JsonCredentialManager(credentialCacheDir, timeoutMs = 60000) { | |
const { folder: dir, subfolders: subDirs } = candidate; | ||
if (await this.tryTokenDir(dir, subDirs)) { | ||
return path.join(dir, ...subDirs); | ||
} else { | ||
Logger.getInstance().info(`${path.join(dir, ...subDirs)} is not a valid cache directory`); | ||
} | ||
} | ||
return null; | ||
}; | ||
|
||
this.getTokenFile = async function () { | ||
this.getTokenFilePath = async function () { | ||
const tokenDir = await this.getTokenDir(); | ||
|
||
if (!Util.exists(tokenDir)) { | ||
throw new Error(`Temporary credential cache directory is invalid, and the driver is unable to use the default location. | ||
Please set 'credentialCacheDir' connection configuration option to enable the default credential manager.`); | ||
} | ||
|
||
const tokenCacheFile = path.join(tokenDir, 'credential_cache_v1.json'); | ||
return [await getSecureHandle(tokenCacheFile, fs.constants.O_RDWR | fs.constants.O_CREAT, fs), tokenCacheFile]; | ||
return path.join(tokenDir, 'credential_cache_v1.json'); | ||
}; | ||
|
||
this.readJsonCredentialFile = async function (fileHandle) { | ||
if (!Util.exists(fileHandle)) { | ||
return null; | ||
} | ||
try { | ||
const cred = await fileHandle.readFile('utf8'); | ||
return JSON.parse(cred); | ||
|
@@ -141,10 +139,8 @@ function JsonCredentialManager(credentialCacheDir, timeoutMs = 60000) { | |
|
||
}; | ||
|
||
|
||
this.withFileLocked = async function (fun) { | ||
const [fileHandle, file] = await this.getTokenFile(); | ||
const lckFile = file + '.lck'; | ||
this.lockFile = async function (filename) { | ||
const lckFile = filename + '.lck'; | ||
await this.removeStale(lckFile); | ||
let attempts = 1; | ||
let locked = false; | ||
|
@@ -153,7 +149,7 @@ function JsonCredentialManager(credentialCacheDir, timeoutMs = 60000) { | |
options.mode = 0o600; | ||
} | ||
while (attempts <= 10) { | ||
Logger.getInstance().debug('Attempting to get a lock on file %s, attempt: %d', file, attempts); | ||
Logger.getInstance().debug('Attempting to get a lock on file %s, attempt: %d', filename, attempts); | ||
attempts++; | ||
await fs.mkdir(lckFile, options).then(() => { | ||
sfc-gh-pmotacki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
locked = true; | ||
|
@@ -164,18 +160,24 @@ function JsonCredentialManager(credentialCacheDir, timeoutMs = 60000) { | |
await new Promise(resolve => setTimeout(resolve, 100)); | ||
sfc-gh-astachowski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
if (!locked) { | ||
if (Util.exists(fileHandle)) { | ||
await fileHandle.close(); | ||
} | ||
Logger.getInstance().warn('Could not acquire lock on cache file %s', file); | ||
return null; | ||
} | ||
const res = await fun(fileHandle, file); | ||
if (Util.exists(fileHandle)) { | ||
await fileHandle.close(); | ||
Logger.getInstance().warn('Could not acquire lock on cache file %s', filename); | ||
} | ||
return locked; | ||
}; | ||
|
||
this.unlockFile = async function (filename) { | ||
const lckFile = filename + '.lck'; | ||
await fs.rmdir(lckFile); | ||
return res; | ||
}; | ||
|
||
this.withFileLocked = async function (fun) { | ||
const filename = await this.getTokenFilePath(); | ||
if (await this.lockFile(filename)) { | ||
const res = await fun(filename); | ||
await this.unlockFile(filename); | ||
return res; | ||
} | ||
return null; | ||
}; | ||
|
||
this.write = async function (key, token) { | ||
sfc-gh-jszczerbinski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
@@ -184,18 +186,20 @@ function JsonCredentialManager(credentialCacheDir, timeoutMs = 60000) { | |
} | ||
const keyHash = this.hashKey(key); | ||
|
||
await this.withFileLocked(async (fileHandle, filename) => { | ||
await this.withFileLocked(async (filename) => { | ||
const fileHandle = await getSecureHandle(filename, fs.constants.O_RDWR, fs); | ||
sfc-gh-astachowski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const jsonCredential = await this.readJsonCredentialFile(fileHandle) || {}; | ||
await closeHandle(fileHandle); | ||
if (!Util.exists(jsonCredential[tokenMapKey])) { | ||
jsonCredential[tokenMapKey] = {}; | ||
} | ||
jsonCredential[tokenMapKey][keyHash] = token; | ||
|
||
try { | ||
const flag = Util.exists(fileHandle) ? fs.constants.O_RDWR | fs.constants.O_CREAT : fs.constants.O_WRONLY; | ||
const flag = Util.exists(fileHandle) ? fs.constants.O_WRONLY : fs.constants.O_RDWR | fs.constants.O_CREAT; | ||
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. Why ternary? isn't it always O_RDWR | O_CREAT? |
||
const writeFileHandle = await getSecureHandle(filename, flag, fs); | ||
await writeFileHandle.writeFile(JSON.stringify(jsonCredential), { mode: 0o600 }); | ||
await writeFileHandle.close(); | ||
await closeHandle(writeFileHandle); | ||
} catch (err) { | ||
Logger.getInstance().warn(`Failed to write token data in ${filename}. Please check the permission or the file format of the token. ${err.message}`); | ||
} | ||
|
@@ -209,8 +213,10 @@ function JsonCredentialManager(credentialCacheDir, timeoutMs = 60000) { | |
|
||
const keyHash = this.hashKey(key); | ||
|
||
return await this.withFileLocked(async (fileHandle) => { | ||
return await this.withFileLocked(async (filename) => { | ||
const fileHandle = await getSecureHandle(filename, fs.constants.O_RDWR, fs); | ||
sfc-gh-astachowski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const jsonCredential = await this.readJsonCredentialFile(fileHandle); | ||
await closeHandle(fileHandle); | ||
if (!!jsonCredential && jsonCredential[tokenMapKey] && jsonCredential[tokenMapKey][keyHash]) { | ||
return jsonCredential[tokenMapKey][keyHash]; | ||
} else { | ||
|
@@ -226,16 +232,18 @@ function JsonCredentialManager(credentialCacheDir, timeoutMs = 60000) { | |
|
||
const keyHash = this.hashKey(key); | ||
|
||
await this.withFileLocked(async (fileHandle, filename) => { | ||
await this.withFileLocked(async (filename) => { | ||
const fileHandle = await getSecureHandle(filename, fs.constants.O_RDWR, fs); | ||
sfc-gh-astachowski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const jsonCredential = await this.readJsonCredentialFile(fileHandle); | ||
await closeHandle(fileHandle); | ||
|
||
if (jsonCredential && jsonCredential[tokenMapKey] && jsonCredential[tokenMapKey][keyHash]) { | ||
try { | ||
jsonCredential[tokenMapKey][keyHash] = null; | ||
const flag = Util.exists(fileHandle) ? fs.constants.O_RDWR | fs.constants.O_CREAT : fs.constants.O_WRONLY; | ||
const flag = Util.exists(fileHandle) ? fs.constants.O_WRONLY : fs.constants.O_RDWR | fs.constants.O_CREAT; | ||
sfc-gh-astachowski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const writeFileHandle = await getSecureHandle(filename, flag, fs); | ||
await writeFileHandle.writeFile(JSON.stringify(jsonCredential), { mode: 0o600 }); | ||
await writeFileHandle.close(); | ||
await closeHandle(writeFileHandle); | ||
} catch (err) { | ||
Logger.getInstance().warn(`Failed to remove token data from the file in ${filename}. Please check the permission or the file format of the token. ${err.message}`); | ||
} | ||
|
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.
Nit: it's hard to see what's returned for each system. Can you do it like this
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.
Didn't do exactly that, but without the null/undefined checks the function is much more readable now
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.
Looks good