5
0
Fork 0
mirror of https://github.com/hashicorp/vault-action.git synced 2025-11-07 15:16:56 +00:00

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
This commit is contained in:
Richard Simpson 2020-03-11 14:02:13 -05:00 committed by GitHub
parent 0ece1da433
commit bef2eb0b90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 148 additions and 6 deletions

View file

@ -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
};

View file

@ -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({

View file

@ -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'

49
dist/index.js vendored
View file

@ -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
};