const jsonata = require("jsonata"); /** * @typedef {Object} SecretRequest * @property {string} path * @property {string} selector */ /** * @template {SecretRequest} TRequest * @typedef {Object} SecretResponse * @property {TRequest} request * @property {string} value * @property {boolean} cachedResponse */ /** * @template TRequest * @param {Array} secretRequests * @param {import('got').Got} client * @return {Promise[]>} */ async function getSecrets(secretRequests, client) { const responseCache = new Map(); const results = []; for (const secretRequest of secretRequests) { let { path, selector } = secretRequest; const requestPath = `v1/${path}`; let body; let cachedResponse = false; if (responseCache.has(requestPath)) { body = responseCache.get(requestPath); cachedResponse = true; } else { try { const result = await client.get(requestPath); body = result.body; responseCache.set(requestPath, body); } catch (error) { const {response} = error; if (response?.statusCode === 404) { throw Error(`Unable to retrieve result for "${path}" because it was not found: ${response.body.trim()}`) } throw error } } if (!selector.match(/.*[\.].*/)) { selector = '"' + selector + '"' } selector = "data." + selector body = JSON.parse(body) if (body.data["data"] != undefined) { selector = "data." + selector } const value = await selectData(body, selector); results.push({ request: secretRequest, value, cachedResponse }); } return results; } /** * Uses a Jsonata selector retrieve a bit of data from the result * @param {object} data * @param {string} selector */ async function selectData(data, selector) { const ata = jsonata(selector); let d = await ata.evaluate(data); console.log(selector) // If we have a Javascript Object, then this data was stored in Vault as // pure JSON (not a JSON string) const storedAsJSONData = isObject(d); // if (isJSONString(d)) { // If we already have a JSON string we will not "stringify" it yet so // that we don't end up calling JSON.parse. This would break the // secrets that are stored as pure JSON. See: https://github.com/hashicorp/vault-action/issues/194 // result = d; // } else { result = jsonstringify(d, 0); // } // Compat for custom engines if (!result && ((ata.ast().type === "path" && ata.ast()['steps'].length === 1) || ata.ast().type === "string") && selector !== 'data' && 'data' in data) { result = jsonstringify(await jsonata(`data.${selector}`).evaluate(data), 1); } else if (!result) { throw Error(`Unable to retrieve result for ${selector}. No match data was found. Double check your Key or Selector.`); } if (result.startsWith(`"`)) { // we need to strip the beginning and ending quotes otherwise it will // always successfully parse as a JSON string result = result.substring(1, result.length - 1); if (!isJSONString(result)) { // add the quotes back so we can parse it into a Javascript object // to allow support for multi-line secrets. See https://github.com/hashicorp/vault-action/issues/160 result = `"${result}"` result = JSON.parse(result); } } else if (isJSONString(result)) { if (storedAsJSONData) { // Support secrets stored in Vault as pure JSON. // See https://github.com/hashicorp/vault-action/issues/194 and https://github.com/hashicorp/vault-action/pull/173 result = jsonstringify(result, 2); result = result.substring(1, result.length - 1); } else { // Support secrets stored in Vault as JSON Strings result = jsonstringify(result, 3); result = JSON.parse(result); } } console.log() return result; } function jsonstringify(input, call) { console.log('stringify', call); return JSON.stringify(input) } /** * isOjbect returns true if target is a Javascript object * @param {Type} target */ function isObject(target) { console.log('isObject: ', typeof target === 'object' && target !== null) return typeof target === 'object' && target !== null; } /** * isJSONString returns true if target parses as a valid JSON string * @param {Type} target */ function isJSONString(target) { if (typeof target !== "string"){ console.log('isJSONString: false, not string') return false; } try { JSON.parse(target); } catch (e) { console.log('isJSONString: false, failed to parse') return false; } console.log('isJSONString: true') return true; } module.exports = { getSecrets, selectData }