From bef2eb0b902fe0705731e9861f385a07495f4602 Mon Sep 17 00:00:00 2001 From: Richard Simpson Date: Wed, 11 Mar 2020 14:02:13 -0500 Subject: [PATCH] feat: add the ability to set extra headers (#27) * feat: add the ability to set extra headers * switch to more generic map solution for headers --- action.js | 36 ++++++++++++++++++++++++++- action.test.js | 66 +++++++++++++++++++++++++++++++++++++++++++++++++- action.yml | 3 +++ dist/index.js | 49 ++++++++++++++++++++++++++++++++++--- 4 files changed, 148 insertions(+), 6 deletions(-) diff --git a/action.js b/action.js index d40cc88..4745e61 100644 --- a/action.js +++ b/action.js @@ -12,6 +12,7 @@ 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 }); let enginePath = core.getInput('path', { required: false }); let kvVersion = core.getInput('kv-version', { required: false }); @@ -76,6 +77,10 @@ async function exportSecrets() { }, }; + for (const [headerName, headerValue] of extraHeaders) { + requestOptions.headers[headerName] = headerValue; + } + if (vaultNamespace != null) { requestOptions.headers["X-Vault-Namespace"] = vaultNamespace; } @@ -216,6 +221,9 @@ function normalizeOutputKey(dataKey) { } // @ts-ignore +/** + * @param {string} input + */ function parseBoolInput(input) { if (input === null || input === undefined || input.trim() === '') { return null; @@ -223,9 +231,35 @@ function parseBoolInput(input) { 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 + normalizeOutputKey, + parseHeadersInput }; diff --git a/action.test.js b/action.test.js index d1af937..5765441 100644 --- a/action.test.js +++ b/action.test.js @@ -7,7 +7,8 @@ const got = require('got'); const { exportSecrets, parseSecretsInput, - parseResponse + parseResponse, + parseHeadersInput } = require('./action'); const { when } = require('jest-when'); @@ -90,6 +91,46 @@ describe('parseSecretsInput', () => { }) }); +describe('parseHeaders', () => { + it('parses simple header', () => { + when(core.getInput) + .calledWith('extraHeaders') + .mockReturnValueOnce('TEST: 1'); + const result = parseHeadersInput('extraHeaders'); + expect(Array.from(result)).toContainEqual(['test', '1']); + }); + + it('parses simple header with whitespace', () => { + when(core.getInput) + .calledWith('extraHeaders') + .mockReturnValueOnce(` + TEST: 1 + `); + const result = parseHeadersInput('extraHeaders'); + expect(Array.from(result)).toContainEqual(['test', '1']); + }); + + it('parses multiple headers', () => { + when(core.getInput) + .calledWith('extraHeaders') + .mockReturnValueOnce(` + TEST: 1 + FOO: bAr + `); + const result = parseHeadersInput('extraHeaders'); + expect(Array.from(result)).toContainEqual(['test', '1']); + expect(Array.from(result)).toContainEqual(['foo', 'bAr']); + }); + + it('parses null response', () => { + when(core.getInput) + .calledWith('extraHeaders') + .mockReturnValueOnce(null); + const result = parseHeadersInput('extraHeaders'); + expect(Array.from(result)).toHaveLength(0); + }); +}) + describe('parseResponse', () => { // https://www.vaultproject.io/api/secret/kv/kv-v1.html#sample-response it('parses K/V version 1 response', () => { @@ -148,6 +189,12 @@ describe('exportSecrets', () => { .mockReturnValueOnce(version); } + function mockExtraHeaders(headerString) { + when(core.getInput) + .calledWith('extraHeaders') + .mockReturnValueOnce(headerString); + } + function mockVaultData(data, version='2') { switch(version) { case '1': @@ -196,6 +243,23 @@ describe('exportSecrets', () => { it('simple secret retrieval from K/V v1', async () => { const version = '1'; + mockInput('test key'); + mockExtraHeaders(` + TEST: 1 + `); + mockVaultData({ + key: 1 + }); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('KEY', '1'); + expect(core.setOutput).toBeCalledWith('key', '1'); + }); + + it('simple secret retrieval with extra headers', async () => { + const version = '1'; + mockInput('test key'); mockVersion(version); mockVaultData({ diff --git a/action.yml b/action.yml index e6f702a..278eb8f 100644 --- a/action.yml +++ b/action.yml @@ -13,6 +13,9 @@ inputs: namespace: description: 'The Vault namespace from which to query secrets. Vault Enterprise only, unset by default' required: false + extraHeaders: + description: 'A string of newline seperated extra headers to include on every request.' + required: false runs: using: 'node12' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index a232389..659fc7f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -648,6 +648,7 @@ const defaults = { maxRedirects: 10, prefixUrl: '', methodRewriting: true, + allowGetBody: false, ignoreInvalidCookies: false, context: {}, _pagination: { @@ -1019,6 +1020,7 @@ exports.preNormalizeArguments = (options, defaults) => { options.dnsCache = (_e = options.dnsCache, (_e !== null && _e !== void 0 ? _e : false)); options.useElectronNet = Boolean(options.useElectronNet); options.methodRewriting = Boolean(options.methodRewriting); + options.allowGetBody = Boolean(options.allowGetBody); options.context = (_f = options.context, (_f !== null && _f !== void 0 ? _f : {})); return options; }; @@ -1131,7 +1133,8 @@ exports.normalizeArguments = (url, options, defaults) => { } return normalizedOptions; }; -const withoutBody = new Set(['GET', 'HEAD']); +const withoutBody = new Set(['HEAD']); +const withoutBodyUnlessSpecified = 'GET'; exports.normalizeRequestArguments = async (options) => { var _a, _b, _c; options = exports.mergeOptions(options); @@ -1146,6 +1149,9 @@ exports.normalizeRequestArguments = async (options) => { if ((isBody || isForm || isJson) && withoutBody.has(options.method)) { throw new TypeError(`The \`${options.method}\` method cannot be used with a body`); } + if (!options.allowGetBody && (isBody || isForm || isJson) && withoutBodyUnlessSpecified === options.method) { + throw new TypeError(`The \`${options.method}\` method cannot be used with a body`); + } if ([isBody, isForm, isJson].filter(isTrue => isTrue).length > 1) { throw new TypeError('The `body`, `json` and `form` options are mutually exclusive'); } @@ -1192,7 +1198,7 @@ exports.normalizeRequestArguments = async (options) => { // a payload body and the method semantics do not anticipate such a // body. if (is_1.default.undefined(headers['content-length']) && is_1.default.undefined(headers['transfer-encoding'])) { - if ((options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH' || options.method === 'DELETE') && + if ((options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH' || options.method === 'DELETE' || (options.allowGetBody && options.method === 'GET')) && !is_1.default.undefined(uploadBodySize)) { // @ts-ignore We assign if it is undefined, so this IS correct headers['content-length'] = String(uploadBodySize); @@ -3146,7 +3152,7 @@ function asStream(options) { throw new Error('Got\'s stream is not writable when the `body`, `json` or `form` option is used'); }; } - else if (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH') { + else if (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH' || (options.allowGetBody && options.method === 'GET')) { options.body = input; } else { @@ -3551,6 +3557,13 @@ exports.setFailed = setFailed; //----------------------------------------------------------------------- // Logging Commands //----------------------------------------------------------------------- +/** + * Gets whether Actions Step Debug is on or not + */ +function isDebug() { + return process.env['RUNNER_DEBUG'] === '1'; +} +exports.isDebug = isDebug; /** * Writes debug message to user log * @param message debug message @@ -4853,6 +4866,7 @@ 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 }); let enginePath = core.getInput('path', { required: false }); let kvVersion = core.getInput('kv-version', { required: false }); @@ -4917,6 +4931,10 @@ async function exportSecrets() { }, }; + for (const [headerName, headerValue] of extraHeaders) { + requestOptions.headers[headerName] = headerValue; + } + if (vaultNamespace != null) { requestOptions.headers["X-Vault-Namespace"] = vaultNamespace; } @@ -5057,6 +5075,9 @@ function normalizeOutputKey(dataKey) { } // @ts-ignore +/** + * @param {string} input + */ function parseBoolInput(input) { if (input === null || input === undefined || input.trim() === '') { return null; @@ -5064,11 +5085,31 @@ function parseBoolInput(input) { 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 !== ''); + const pairs = headerStrings + .map(line => { + const seperator = line.indexOf(':'); + return [line.substring(0, seperator), line.substring(seperator + 1)]; + }); + return new Headers(pairs); +} + module.exports = { exportSecrets, parseSecretsInput, parseResponse: getResponseData, - normalizeOutputKey + normalizeOutputKey, + parseHeadersInput };