From 5c464962be8937589f883cf209d21b3982c92360 Mon Sep 17 00:00:00 2001 From: Richard Simpson Date: Sat, 4 Apr 2020 13:19:48 -0500 Subject: [PATCH] feat: add generic auth (#39) * feat: add generic auth Adds the ability to authenticate against any normal Vault endpoint by added the `authPayload` input. When an unrecognized method is provided, the action will attempt to hit `v1/auth//login` with the provided `authPayload and parse out the token in the response --- action.yml | 3 + dist/index.js | 671 +++++++++--------- integrationTests/basic/integration.test.js | 2 +- .../enterprise/enterprise.test.js | 2 +- package.json | 6 +- action.js => src/action.js | 69 +- action.test.js => src/action.test.js | 0 src/auth.js | 81 +++ index.js => src/index.js | 0 tsconfig.json | 12 + 10 files changed, 449 insertions(+), 397 deletions(-) rename action.js => src/action.js (76%) rename action.test.js => src/action.test.js (100%) create mode 100644 src/auth.js rename index.js => src/index.js (100%) create mode 100644 tsconfig.json diff --git a/action.yml b/action.yml index 9c1d7a9..bbb7281 100644 --- a/action.yml +++ b/action.yml @@ -33,6 +33,9 @@ inputs: githubToken: description: 'The Github Token to be used to authenticate with Vault' required: false + authPayload: + description: 'The JSON payload to be sent to Vault when using a custom authentication method.' + required: false extraHeaders: description: 'A string of newline separated extra headers to include on every request.' required: false diff --git a/dist/index.js b/dist/index.js index a658c33..bb3cd8c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -34,7 +34,7 @@ module.exports = /******/ // the startup function /******/ function startup() { /******/ // Load entry module and return exports -/******/ return __webpack_require__(104); +/******/ return __webpack_require__(676); /******/ }; /******/ // initialize runtime /******/ runtime(__webpack_require__); @@ -805,22 +805,6 @@ class Response extends Readable { module.exports = Response; -/***/ }), - -/***/ 104: -/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) { - -const core = __webpack_require__(470); -const { exportSecrets } = __webpack_require__(751); - -(async () => { - try { - await core.group('Get Vault Secrets', exportSecrets); - } catch (error) { - core.setFailed(error.message); - } -})(); - /***/ }), /***/ 110: @@ -1568,6 +1552,72 @@ module.exports.iterator = (emitter, event, options) => { }; +/***/ }), + +/***/ 151: +/***/ (function(module, __unusedexports, __webpack_require__) { + +const core = __webpack_require__(470); + +/*** + * Authentication with Vault and retrieve a vault token + * @param {string} method + * @param {import('got')} client + */ +async function retrieveToken(method, client) { + switch (method) { + case 'approle': { + const vaultRoleId = core.getInput('roleId', { required: true }); + const vaultSecretId = core.getInput('secretId', { required: true }); + return await getClientToken(client, method, { role_id: vaultRoleId, secret_id: vaultSecretId }); + } + case 'github': { + const githubToken = core.getInput('githubToken', { required: true }); + return await getClientToken(client, method, { token: githubToken }); + } + default: { + if (!method || method === 'token') { + return core.getInput('token', { required: true }); + } else { + /** @type {string} */ + const payload = core.getInput('authPayload', { required: true }); + if (!payload) { + throw Error('When using a custom authentication method, you must provide the payload'); + } + return await getClientToken(client, method, JSON.parse(payload.trim())); + } + } + } +} + +/*** + * Authentication with Vault and retrieve a vault token + * @param {import('got')} client + * @param {string} method + * @param {any} payload + */ +async function getClientToken(client, method, payload) { + /** @type {any} */ + var options = { + json: payload, + responseType: 'json' + }; + + core.debug(`Retrieving Vault Token from v1/auth/${method}/login endpoint`); + const response = await client.post(`v1/auth/${method}/login`, options); + if (response && response.body && response.body.auth && response.body.auth.client_token) { + core.debug('✔ Vault Token successfully retrieved'); + return response.body.auth.client_token; + } else { + throw Error(`Unable to retrieve token from ${method}'s login endpoint.`); + } +} + +module.exports = { + retrieveToken +} + + /***/ }), /***/ 154: @@ -4647,6 +4697,22 @@ module.exports = require("util"); /***/ }), +/***/ 676: +/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) { + +const core = __webpack_require__(470); +const { exportSecrets } = __webpack_require__(928); + +(async () => { + try { + await core.group('Get Vault Secrets', exportSecrets); + } catch (error) { + core.setFailed(error.message); + } +})(); + +/***/ }), + /***/ 678: /***/ (function(__unusedmodule, exports, __webpack_require__) { @@ -4872,316 +4938,6 @@ module.exports.DuplexWrapper = DuplexWrapper; module.exports = require("fs"); -/***/ }), - -/***/ 751: -/***/ (function(module, __unusedexports, __webpack_require__) { - -// @ts-check - -// @ts-ignore -const core = __webpack_require__(470); -// @ts-ignore -const command = __webpack_require__(431); -const got = __webpack_require__(77); - -const AUTH_METHODS = ['approle', 'token', 'github']; -const VALID_KV_VERSION = [-1, 1, 2]; - -async function exportSecrets() { - const vaultUrl = core.getInput('url', { required: true }); - const vaultNamespace = core.getInput('namespace', { required: false }); - const extraHeaders = parseHeadersInput('extraHeaders', { required: false }); - const exportEnv = core.getInput('exportEnv', { required: false }) != 'false'; - - let enginePath = core.getInput('path', { required: false }); - let kvVersion = core.getInput('kv-version', { required: false }); - - const secretsInput = core.getInput('secrets', { required: true }); - const secretRequests = parseSecretsInput(secretsInput); - - const vaultMethod = (core.getInput('method', { required: false }) || 'token').toLowerCase(); - if (!AUTH_METHODS.includes(vaultMethod)) { - throw Error(`Sorry, the authentication method ${vaultMethod} is not currently supported.`); - } - - const defaultOptions = { - prefixUrl: vaultUrl, - headers: {} - } - - for (const [headerName, headerValue] of extraHeaders) { - defaultOptions.headers[headerName] = headerValue; - } - - if (vaultNamespace != null) { - defaultOptions.headers["X-Vault-Namespace"] = vaultNamespace; - } - - const client = got.extend(defaultOptions); - const vaultToken = await retrieveToken(vaultMethod, /** @type {any} */ (client)); - - if (!enginePath) { - enginePath = 'secret'; - } - - if (!kvVersion) { - kvVersion = 2; - } - kvVersion = +kvVersion; - - if (Number.isNaN(kvVersion) || !VALID_KV_VERSION.includes(kvVersion)) { - throw Error(`You must provide a valid K/V version (${VALID_KV_VERSION.slice(1).join(', ')}). Input: "${kvVersion}"`); - } - - const responseCache = new Map(); - for (const secretRequest of secretRequests) { - const { secretPath, outputVarName, envVarName, secretSelector, isJSONPath } = secretRequest; - const requestOptions = { - headers: { - 'X-Vault-Token': vaultToken - }, - }; - - for (const [headerName, headerValue] of extraHeaders) { - requestOptions.headers[headerName] = headerValue; - } - - if (vaultNamespace != null) { - requestOptions.headers["X-Vault-Namespace"] = vaultNamespace; - } - - let requestPath = `v1`; - const kvRequest = !secretPath.startsWith('/') - if (!kvRequest) { - requestPath += secretPath; - } else { - requestPath += (kvVersion === 2) - ? `/${enginePath}/data/${secretPath}` - : `/${enginePath}/${secretPath}`; - } - - let body; - if (responseCache.has(requestPath)) { - body = responseCache.get(requestPath); - core.debug('ℹ using cached response'); - } else { - const result = await client.get(requestPath, requestOptions); - body = result.body; - responseCache.set(requestPath, body); - } - - let dataDepth = isJSONPath === true ? 0 : kvRequest === false ? 1 : kvVersion; - - const secretData = getResponseData(body, dataDepth); - const value = selectData(secretData, secretSelector, isJSONPath); - command.issue('add-mask', value); - if (exportEnv) { - core.exportVariable(envVarName, `${value}`); - } - core.setOutput(outputVarName, `${value}`); - core.debug(`✔ ${secretPath} => outputs.${outputVarName}${exportEnv ? ` | env.${envVarName}` : ''}`); - } -}; - -/** @typedef {Object} SecretRequest - * @property {string} secretPath - * @property {string} envVarName - * @property {string} outputVarName - * @property {string} secretSelector - * @property {boolean} isJSONPath -*/ - -/** - * Parses a secrets input string into key paths and their resulting environment variable name. - * @param {string} secretsInput - */ -function parseSecretsInput(secretsInput) { - const secrets = secretsInput - .split(';') - .filter(key => !!key) - .map(key => key.trim()) - .filter(key => key.length !== 0); - - /** @type {SecretRequest[]} */ - const output = []; - for (const secret of secrets) { - let path = secret; - let outputVarName = null; - - const renameSigilIndex = secret.lastIndexOf('|'); - if (renameSigilIndex > -1) { - path = secret.substring(0, renameSigilIndex).trim(); - outputVarName = secret.substring(renameSigilIndex + 1).trim(); - - if (outputVarName.length < 1) { - throw Error(`You must provide a value when mapping a secret to a name. Input: "${secret}"`); - } - } - - const pathParts = path - .split(/\s+/) - .map(part => part.trim()) - .filter(part => part.length !== 0); - - if (pathParts.length !== 2) { - throw Error(`You must provide a valid path and key. Input: "${secret}"`); - } - - const [secretPath, secretSelector] = pathParts; - - const isJSONPath = secretSelector.includes('.'); - - if (isJSONPath && !outputVarName) { - throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`); - } - - let envVarName = outputVarName; - if (!outputVarName) { - outputVarName = secretSelector; - envVarName = normalizeOutputKey(outputVarName); - } - - output.push({ - secretPath, - envVarName, - outputVarName, - secretSelector, - isJSONPath - }); - } - return output; -} - -/*** - * Authentication with Vault and retrieve a vault token - * @param {string} method - * @param {import('got')} client - */ -async function retrieveToken(method, client) { - switch (method) { - case 'approle': { - const vaultRoleId = core.getInput('roleId', { required: true }); - const vaultSecretId = core.getInput('secretId', { required: true }); - core.debug('Try to retrieve Vault Token from approle'); - - /** @type {any} */ - var options = { - json: { role_id: vaultRoleId, secret_id: vaultSecretId }, - responseType: 'json' - }; - - const result = await client.post(`v1/auth/approle/login`, options); - if (result && result.body && result.body.auth && result.body.auth.client_token) { - core.debug('✔ Vault Token has retrieved from approle'); - return result.body.auth.client_token; - } else { - throw Error(`No token was retrieved with the role_id and secret_id provided.`); - } - } - case 'github': { - const githubToken = core.getInput('githubToken', { required: true }); - core.debug('Try to retrieve Vault Token from approle'); - - /** @type {any} */ - var options = { - json: { token: githubToken }, - responseType: 'json' - }; - - const result = await client.post(`v1/auth/github/login`, options); - if (result && result.body && result.body.auth && result.body.auth.client_token) { - core.debug('✔ Vault Token has retrieved from approle'); - return result.body.auth.client_token; - } else { - throw Error(`No token was retrieved with the role_id and secret_id provided.`); - } - } - default: - return core.getInput('token', { required: true }); - } -} - -/** - * Parses a JSON response and returns the secret data - * @param {string} responseBody - * @param {number} dataLevel - */ -function getResponseData(responseBody, dataLevel) { - let secretData = JSON.parse(responseBody); - - for (let i = 0; i < dataLevel; i++) { - secretData = secretData['data']; - } - return secretData; -} - -/** - * Parses a JSON response and returns the secret data - * @param {Object} data - * @param {string} selector - */ -function selectData(data, selector, isJSONPath) { - if (!isJSONPath) { - return data[selector]; - } - - // TODO: JSONPath -} - -/** - * Replaces any forward-slash characters to - * @param {string} dataKey - */ -function normalizeOutputKey(dataKey) { - return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase(); -} - -// @ts-ignore -/** - * @param {string} input - */ -function parseBoolInput(input) { - if (input === null || input === undefined || input.trim() === '') { - return null; - } - return Boolean(input); -} - -/** - * @param {string} inputKey - * @param {any} inputOptions - */ -function parseHeadersInput(inputKey, inputOptions) { - /** @type {string}*/ - const rawHeadersString = core.getInput(inputKey, inputOptions) || ''; - const headerStrings = rawHeadersString - .split('\n') - .map(line => line.trim()) - .filter(line => line !== ''); - return headerStrings - .reduce((map, line) => { - const seperator = line.indexOf(':'); - const key = line.substring(0, seperator).trim().toLowerCase(); - const value = line.substring(seperator + 1).trim(); - if (map.has(key)) { - map.set(key, [map.get(key), value].join(', ')); - } else { - map.set(key, value); - } - return map; - }, new Map()); -} - -module.exports = { - exportSecrets, - parseSecretsInput, - parseResponse: getResponseData, - normalizeOutputKey, - parseHeadersInput -}; - - /***/ }), /***/ 753: @@ -5949,6 +5705,267 @@ exports.proxyEvents = (proxy, emitter) => { module.exports = require("dns"); +/***/ }), + +/***/ 928: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// @ts-check +// @ts-ignore +const core = __webpack_require__(470); +// @ts-ignore +const command = __webpack_require__(431); +const got = __webpack_require__(77); +const { retrieveToken } = __webpack_require__(151); + +const AUTH_METHODS = ['approle', 'token', 'github']; +const VALID_KV_VERSION = [-1, 1, 2]; + +async function exportSecrets() { + const vaultUrl = core.getInput('url', { required: true }); + const vaultNamespace = core.getInput('namespace', { required: false }); + const extraHeaders = parseHeadersInput('extraHeaders', { required: false }); + const exportEnv = core.getInput('exportEnv', { required: false }) != 'false'; + + let enginePath = core.getInput('path', { required: false }); + let kvVersion = core.getInput('kv-version', { required: false }); + + const secretsInput = core.getInput('secrets', { required: true }); + const secretRequests = parseSecretsInput(secretsInput); + + const vaultMethod = (core.getInput('method', { required: false }) || 'token').toLowerCase(); + if (!AUTH_METHODS.includes(vaultMethod)) { + throw Error(`Sorry, the authentication method ${vaultMethod} is not currently supported.`); + } + + const defaultOptions = { + prefixUrl: vaultUrl, + headers: {} + } + + for (const [headerName, headerValue] of extraHeaders) { + defaultOptions.headers[headerName] = headerValue; + } + + if (vaultNamespace != null) { + defaultOptions.headers["X-Vault-Namespace"] = vaultNamespace; + } + + const client = got.extend(defaultOptions); + const vaultToken = await retrieveToken(vaultMethod, /** @type {any} */ (client)); + + if (!enginePath) { + enginePath = 'secret'; + } + + if (!kvVersion) { + kvVersion = 2; + } + kvVersion = +kvVersion; + + if (Number.isNaN(kvVersion) || !VALID_KV_VERSION.includes(kvVersion)) { + throw Error(`You must provide a valid K/V version (${VALID_KV_VERSION.slice(1).join(', ')}). Input: "${kvVersion}"`); + } + + const responseCache = new Map(); + for (const secretRequest of secretRequests) { + const { secretPath, outputVarName, envVarName, secretSelector, isJSONPath } = secretRequest; + const requestOptions = { + headers: { + 'X-Vault-Token': vaultToken + }, + }; + + for (const [headerName, headerValue] of extraHeaders) { + requestOptions.headers[headerName] = headerValue; + } + + if (vaultNamespace != null) { + requestOptions.headers["X-Vault-Namespace"] = vaultNamespace; + } + + let requestPath = `v1`; + const kvRequest = !secretPath.startsWith('/') + if (!kvRequest) { + requestPath += secretPath; + } else { + requestPath += (kvVersion === 2) + ? `/${enginePath}/data/${secretPath}` + : `/${enginePath}/${secretPath}`; + } + + let body; + if (responseCache.has(requestPath)) { + body = responseCache.get(requestPath); + core.debug('ℹ using cached response'); + } else { + const result = await client.get(requestPath, requestOptions); + body = result.body; + responseCache.set(requestPath, body); + } + + let dataDepth = isJSONPath === true ? 0 : kvRequest === false ? 1 : kvVersion; + + const secretData = getResponseData(body, dataDepth); + const value = selectData(secretData, secretSelector, isJSONPath); + command.issue('add-mask', value); + if (exportEnv) { + core.exportVariable(envVarName, `${value}`); + } + core.setOutput(outputVarName, `${value}`); + core.debug(`✔ ${secretPath} => outputs.${outputVarName}${exportEnv ? ` | env.${envVarName}` : ''}`); + } +}; + +/** @typedef {Object} SecretRequest + * @property {string} secretPath + * @property {string} envVarName + * @property {string} outputVarName + * @property {string} secretSelector + * @property {boolean} isJSONPath +*/ + +/** + * Parses a secrets input string into key paths and their resulting environment variable name. + * @param {string} secretsInput + */ +function parseSecretsInput(secretsInput) { + const secrets = secretsInput + .split(';') + .filter(key => !!key) + .map(key => key.trim()) + .filter(key => key.length !== 0); + + /** @type {SecretRequest[]} */ + const output = []; + for (const secret of secrets) { + let path = secret; + let outputVarName = null; + + const renameSigilIndex = secret.lastIndexOf('|'); + if (renameSigilIndex > -1) { + path = secret.substring(0, renameSigilIndex).trim(); + outputVarName = secret.substring(renameSigilIndex + 1).trim(); + + if (outputVarName.length < 1) { + throw Error(`You must provide a value when mapping a secret to a name. Input: "${secret}"`); + } + } + + const pathParts = path + .split(/\s+/) + .map(part => part.trim()) + .filter(part => part.length !== 0); + + if (pathParts.length !== 2) { + throw Error(`You must provide a valid path and key. Input: "${secret}"`); + } + + const [secretPath, secretSelector] = pathParts; + + const isJSONPath = secretSelector.includes('.'); + + if (isJSONPath && !outputVarName) { + throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`); + } + + let envVarName = outputVarName; + if (!outputVarName) { + outputVarName = secretSelector; + envVarName = normalizeOutputKey(outputVarName); + } + + output.push({ + secretPath, + envVarName, + outputVarName, + secretSelector, + isJSONPath + }); + } + return output; +} + +/** + * Parses a JSON response and returns the secret data + * @param {string} responseBody + * @param {number} dataLevel + */ +function getResponseData(responseBody, dataLevel) { + let secretData = JSON.parse(responseBody); + + for (let i = 0; i < dataLevel; i++) { + secretData = secretData['data']; + } + return secretData; +} + +/** + * Parses a JSON response and returns the secret data + * @param {Object} data + * @param {string} selector + */ +function selectData(data, selector, isJSONPath) { + if (!isJSONPath) { + return data[selector]; + } + + // TODO: JSONPath +} + +/** + * Replaces any forward-slash characters to + * @param {string} dataKey + */ +function normalizeOutputKey(dataKey) { + return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase(); +} + +// @ts-ignore +/** + * @param {string} input + */ +function parseBoolInput(input) { + if (input === null || input === undefined || input.trim() === '') { + return null; + } + return Boolean(input); +} + +/** + * @param {string} inputKey + * @param {any} inputOptions + */ +function parseHeadersInput(inputKey, inputOptions) { + /** @type {string}*/ + const rawHeadersString = core.getInput(inputKey, inputOptions) || ''; + const headerStrings = rawHeadersString + .split('\n') + .map(line => line.trim()) + .filter(line => line !== ''); + return headerStrings + .reduce((map, line) => { + const seperator = line.indexOf(':'); + const key = line.substring(0, seperator).trim().toLowerCase(); + const value = line.substring(seperator + 1).trim(); + if (map.has(key)) { + map.set(key, [map.get(key), value].join(', ')); + } else { + map.set(key, value); + } + return map; + }, new Map()); +} + +module.exports = { + exportSecrets, + parseSecretsInput, + parseResponse: getResponseData, + normalizeOutputKey, + parseHeadersInput +}; + + /***/ }), /***/ 946: diff --git a/integrationTests/basic/integration.test.js b/integrationTests/basic/integration.test.js index 00c601a..a9a89e5 100644 --- a/integrationTests/basic/integration.test.js +++ b/integrationTests/basic/integration.test.js @@ -5,7 +5,7 @@ const core = require('@actions/core'); const got = require('got'); const { when } = require('jest-when'); -const { exportSecrets } = require('../../action'); +const { exportSecrets } = require('../../src/action'); const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`; diff --git a/integrationTests/enterprise/enterprise.test.js b/integrationTests/enterprise/enterprise.test.js index 6e5a2ba..90ad22c 100644 --- a/integrationTests/enterprise/enterprise.test.js +++ b/integrationTests/enterprise/enterprise.test.js @@ -5,7 +5,7 @@ const core = require('@actions/core'); const got = require('got'); const { when } = require('jest-when'); -const { exportSecrets } = require('../../action'); +const { exportSecrets } = require('../../src/action'); const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8201'}`; diff --git a/package.json b/package.json index 7c12b6f..c9de6e2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A Github Action that allows you to consume vault secrets as secure environment variables.", "main": "dist/index.js", "scripts": { - "build": "ncc build index.js -o dist", + "build": "ncc build src/index.js -o dist", "test": "jest", "test:integration:basic": "jest -c integrationTests/basic/jest.config.js", "test:integration:enterprise": "jest -c integrationTests/enterprise/jest.config.js", @@ -38,8 +38,8 @@ }, "homepage": "https://github.com/RichiCoder1/vault-action#readme", "dependencies": { - "@actions/core": "^1.2.2", - "got": "^10.2.2" + "got": "^10.2.2", + "@actions/core": "^1.2.3" }, "devDependencies": { "@types/got": "^9.6.9", diff --git a/action.js b/src/action.js similarity index 76% rename from action.js rename to src/action.js index 555c1e8..25e966d 100644 --- a/action.js +++ b/src/action.js @@ -1,10 +1,8 @@ // @ts-check - -// @ts-ignore const core = require('@actions/core'); -// @ts-ignore const command = require('@actions/core/lib/command'); -const got = require('got'); +const got = require('got').default; +const { retrieveToken } = require('./auth'); const AUTH_METHODS = ['approle', 'token', 'github']; const VALID_KV_VERSION = [-1, 1, 2]; @@ -16,6 +14,7 @@ async function exportSecrets() { const exportEnv = core.getInput('exportEnv', { required: false }) != 'false'; let enginePath = core.getInput('path', { required: false }); + /** @type {number | string} */ let kvVersion = core.getInput('kv-version', { required: false }); const secretsInput = core.getInput('secrets', { required: true }); @@ -40,7 +39,7 @@ async function exportSecrets() { } const client = got.extend(defaultOptions); - const vaultToken = await retrieveToken(vaultMethod, /** @type {any} */ (client)); + const vaultToken = await retrieveToken(vaultMethod, client); if (!enginePath) { enginePath = 'secret'; @@ -174,55 +173,6 @@ function parseSecretsInput(secretsInput) { return output; } -/*** - * Authentication with Vault and retrieve a vault token - * @param {string} method - * @param {import('got')} client - */ -async function retrieveToken(method, client) { - switch (method) { - case 'approle': { - const vaultRoleId = core.getInput('roleId', { required: true }); - const vaultSecretId = core.getInput('secretId', { required: true }); - core.debug('Try to retrieve Vault Token from approle'); - - /** @type {any} */ - var options = { - json: { role_id: vaultRoleId, secret_id: vaultSecretId }, - responseType: 'json' - }; - - const result = await client.post(`v1/auth/approle/login`, options); - if (result && result.body && result.body.auth && result.body.auth.client_token) { - core.debug('✔ Vault Token has retrieved from approle'); - return result.body.auth.client_token; - } else { - throw Error(`No token was retrieved with the role_id and secret_id provided.`); - } - } - case 'github': { - const githubToken = core.getInput('githubToken', { required: true }); - core.debug('Try to retrieve Vault Token from approle'); - - /** @type {any} */ - var options = { - json: { token: githubToken }, - responseType: 'json' - }; - - const result = await client.post(`v1/auth/github/login`, options); - if (result && result.body && result.body.auth && result.body.auth.client_token) { - core.debug('✔ Vault Token has retrieved from approle'); - return result.body.auth.client_token; - } else { - throw Error(`No token was retrieved with the role_id and secret_id provided.`); - } - } - default: - return core.getInput('token', { required: true }); - } -} - /** * Parses a JSON response and returns the secret data * @param {string} responseBody @@ -258,17 +208,6 @@ function normalizeOutputKey(dataKey) { return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase(); } -// @ts-ignore -/** - * @param {string} input - */ -function parseBoolInput(input) { - if (input === null || input === undefined || input.trim() === '') { - return null; - } - return Boolean(input); -} - /** * @param {string} inputKey * @param {any} inputOptions diff --git a/action.test.js b/src/action.test.js similarity index 100% rename from action.test.js rename to src/action.test.js diff --git a/src/auth.js b/src/auth.js new file mode 100644 index 0000000..484fd48 --- /dev/null +++ b/src/auth.js @@ -0,0 +1,81 @@ +// @ts-check +const core = require('@actions/core'); + +/*** + * Authenticate with Vault and retrieve a Vault token that can be used for requests. + * @param {string} method + * @param {import('got').Got} client + */ +async function retrieveToken(method, client) { + switch (method) { + case 'approle': { + const vaultRoleId = core.getInput('roleId', { required: true }); + const vaultSecretId = core.getInput('secretId', { required: true }); + return await getClientToken(client, method, { role_id: vaultRoleId, secret_id: vaultSecretId }); + } + case 'github': { + const githubToken = core.getInput('githubToken', { required: true }); + return await getClientToken(client, method, { token: githubToken }); + } + default: { + if (!method || method === 'token') { + return core.getInput('token', { required: true }); + } else { + /** @type {string} */ + const payload = core.getInput('authPayload', { required: true }); + if (!payload) { + throw Error('When using a custom authentication method, you must provide the payload'); + } + return await getClientToken(client, method, JSON.parse(payload.trim())); + } + } + } +} + +/*** + * Call the appropriate login endpoint and parse out the token in the response. + * @param {import('got').Got} client + * @param {string} method + * @param {any} payload + */ +async function getClientToken(client, method, payload) { + /** @type {'json'} */ + const responseType = 'json'; + var options = { + json: payload, + responseType, + }; + + core.debug(`Retrieving Vault Token from v1/auth/${method}/login endpoint`); + + /** @type {import('got').Response} */ + const response = await client.post(`v1/auth/${method}/login`, options); + if (response && response.body && response.body.auth && response.body.auth.client_token) { + core.debug('✔ Vault Token successfully retrieved'); + + core.startGroup('Token Info'); + core.debug(`Operating under policies: ${JSON.stringify(response.body.auth.policies)}`); + core.debug(`Token Metadata: ${JSON.stringify(response.body.auth.metadata)}`); + core.endGroup(); + + return response.body.auth.client_token; + } else { + throw Error(`Unable to retrieve token from ${method}'s login endpoint.`); + } +} + +/*** + * @typedef {Object} VaultLoginResponse + * @property {{ + * client_token: string; + * accessor: string; + * policies: string[]; + * metadata: unknown; + * lease_duration: number; + * renewable: boolean; + * }} auth + */ + +module.exports = { + retrieveToken, +}; diff --git a/index.js b/src/index.js similarity index 100% rename from index.js rename to src/index.js diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..91e905b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2019", + "moduleResolution": "node", + "allowJs": true, + "noEmit": true + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file