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:
parent
0ece1da433
commit
bef2eb0b90
4 changed files with 148 additions and 6 deletions
36
action.js
36
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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
49
dist/index.js
vendored
|
|
@ -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
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue