Compare commits

..

15 Commits

Author SHA1 Message Date
Richard Simpson 50ece41861 chore: I really should PR changes 2020-03-31 12:20:05 -05:00
Richard Simpson ed8303ca53 fix: fix got not being use correctly and path prefix 2020-03-31 12:15:30 -05:00
Richard Simpson 0e719ef42a chore: compile release 2020-03-31 12:01:15 -05:00
Richard Simpson 795c9eddca feat: add github auth and cleanup docs a little
Adds the GitHub Authentication method
2020-03-31 11:56:54 -05:00
dependabot-preview[bot] 9e8c0bad27 chore(deps): bump got from 10.6.0 to 10.7.0 (#30)
Bumps [got](https://github.com/sindresorhus/got) from 10.6.0 to 10.7.0.
- [Release notes](https://github.com/sindresorhus/got/releases)
- [Commits](https://github.com/sindresorhus/got/compare/v10.6.0...v10.7.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-03-26 14:47:12 -05:00
dependabot-preview[bot] 751a3d19ad chore(deps-dev): bump jest from 25.1.0 to 25.2.1 (#31)
Bumps [jest](https://github.com/facebook/jest) from 25.1.0 to 25.2.1.
- [Release notes](https://github.com/facebook/jest/releases)
- [Changelog](https://github.com/facebook/jest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/facebook/jest/compare/v25.1.0...v25.2.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-03-26 14:47:05 -05:00
dependabot-preview[bot] cc7ceeef06 chore(deps-dev): bump @zeit/ncc from 0.21.1 to 0.22.0 (#29)
Bumps [@zeit/ncc](https://github.com/zeit/ncc) from 0.21.1 to 0.22.0.
- [Release notes](https://github.com/zeit/ncc/releases)
- [Commits](https://github.com/zeit/ncc/compare/0.21.1...0.22.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-03-24 10:52:12 -05:00
dependabot-preview[bot] fa23030c10 chore(deps): [security] bump acorn from 6.4.0 to 6.4.1 (#28)
Bumps [acorn](https://github.com/acornjs/acorn) from 6.4.0 to 6.4.1. **This update includes a security fix.**
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/6.4.0...6.4.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-03-13 20:07:36 -05:00
Richard Simpson 4561f9e26e docs: fix typo and clarify masking 2020-03-11 14:16:21 -05:00
Richard Simpson cb2908ac94 docs: promote non-key syntax sections 2020-03-11 14:12:53 -05:00
Richard Simpson 198a7ed7d2 docs: add headers docs and toc 2020-03-11 14:11:13 -05:00
Richard Simpson 4ab6f6070f fix: release new action bundle 2020-03-11 14:02:39 -05:00
Richard Simpson bef2eb0b90 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
2020-03-11 14:02:13 -05:00
dependabot-preview[bot] 0ece1da433 chore(deps-dev): bump @types/jest from 25.1.3 to 25.1.4 (#26)
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 25.1.3 to 25.1.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-03-10 16:25:50 -05:00
dependabot-preview[bot] 7fb0d673d1 chore(deps): bump @actions/core from 1.2.2 to 1.2.3 (#25)
Bumps [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) from 1.2.2 to 1.2.3.
- [Release notes](https://github.com/actions/toolkit/releases)
- [Changelog](https://github.com/actions/toolkit/blob/master/packages/core/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-03-04 10:20:55 -06:00
9 changed files with 1445 additions and 1147 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"markdown-toc.depthFrom": 2
}
+49 -6
View File
@@ -4,6 +4,24 @@ A helper action for easily pulling secrets from HashiCorp Vault™.
By default, this action pulls from [Version 2](https://www.vaultproject.io/docs/secrets/kv/kv-v2/) of the K/V Engine. See examples below for how to [use v1](#using-kv-version-1) as well as [other non-K/V engines](#other-secret-engines). By default, this action pulls from [Version 2](https://www.vaultproject.io/docs/secrets/kv/kv-v2/) of the K/V Engine. See examples below for how to [use v1](#using-kv-version-1) as well as [other non-K/V engines](#other-secret-engines).
<!-- TOC -->
- [Example Usage](#example-usage)
- [Authentication method](#authentication-method)
- [Key Syntax](#key-syntax)
- [Simple Key](#simple-key)
- [Set Output Variable Name](#set-output-variable-name)
- [Multiple Secrets](#multiple-secrets)
- [Using K/V version 1](#using-kv-version-1)
- [Custom K/V Engine Path](#custom-kv-engine-path)
- [Other Secret Engines](#other-secret-engines)
- [Adding Extra Headers](#adding-extra-headers)
- [Vault Enterprise Features](#vault-enterprise-features)
- [Namespace](#namespace)
- [Masking - Hidding Secrets from Logs](#masking---hidding-secrets-from-logs)
<!-- /TOC -->
## Example Usage ## Example Usage
```yaml ```yaml
@@ -28,14 +46,14 @@ jobs:
While most workflows will likely use a vault token, you can also use an `approle` to authenticate with vaule. You can configure which by using the `method` parameter: While most workflows will likely use a vault token, you can also use an `approle` to authenticate with vaule. You can configure which by using the `method` parameter:
- **token**: (by default) you must provide a token parameter - **token**: (by default) you must provide a `token` parameter
```yaml ```yaml
... ...
with: with:
url: https://vault.mycompany.com:8200 url: https://vault.mycompany.com:8200
token: ${{ secrets.VaultToken }} token: ${{ secrets.VaultToken }}
``` ```
- **approle**: you must provide a roleId & secretId parameter - **approle**: you must provide a `roleId` & `secretId` parameter
```yaml ```yaml
... ...
with: with:
@@ -44,6 +62,14 @@ with:
roleId: ${{ secrets.roleId }} roleId: ${{ secrets.roleId }}
secretId: ${{ secrets.secretId }} secretId: ${{ secrets.secretId }}
``` ```
- **github**: you must provide the github token as `githubToken`
```yaml
...
with:
url: https://vault.mycompany.com:8200
method: github
githubToken: ${{ secrets.GITHUB_TOKEN }}
```
## Key Syntax ## Key Syntax
@@ -131,7 +157,7 @@ with:
kv-version: 1 kv-version: 1
``` ```
### Custom K/V Engine Path ## Custom K/V Engine Path
When you enable the K/V Engine, by default it's placed at the path `secret`, so a secret named `ci` will be accessed from `secret/ci`. However, [if you enabled the secrets engine using a custom `path`](https://www.vaultproject.io/docs/commands/secrets/enable/#inlinecode--path-4), you When you enable the K/V Engine, by default it's placed at the path `secret`, so a secret named `ci` will be accessed from `secret/ci`. However, [if you enabled the secrets engine using a custom `path`](https://www.vaultproject.io/docs/commands/secrets/enable/#inlinecode--path-4), you
can pass it as follows: can pass it as follows:
@@ -144,7 +170,7 @@ with:
This way, the `ci` secret in the example above will be retrieved from `my-secrets/ci`. This way, the `ci` secret in the example above will be retrieved from `my-secrets/ci`.
### Other Secret Engines ## Other Secret Engines
While this action primarily supports the K/V engine, it is possible to request secrets from other engines in Vault. While this action primarily supports the K/V engine, it is possible to request secrets from other engines in Vault.
@@ -202,6 +228,22 @@ would work fine.
NOTE: The `Secret Key` is pulled from the `data` property of the response. NOTE: The `Secret Key` is pulled from the `data` property of the response.
## Adding Extra Headers
If you ever need to add extra headers to the vault request, say if you need to authenticate with a firewall, all you need to do is set `extraHeaders`:
```yaml
with:
secrets: |
ci/aws accessKey | AWS_ACCESS_KEY_ID ;
ci/aws secretKey | AWS_SECRET_ACCESS_KEY
extraHeaders: |
X-Secure-Id: ${{ secrets.SECURE_ID }}
X-Secure-Secret: ${{ secrets.SECURE_SECRET }}
```
This will automatically add the `x-secure-id` and `x-secure-secret` headers to every request to vault.
## Vault Enterprise Features ## Vault Enterprise Features
### Namespace ### Namespace
@@ -224,6 +266,7 @@ steps:
ci npm_token ci npm_token
``` ```
## Masking ## Masking - Hidding Secrets from Logs
This action uses Github Action's built in masking, so all variables will automatically be masked if printed to the console or to logs. This action uses GitHub Action's built-in masking, so all variables will automatically be masked (aka hidden) if printed to the console or to logs.
**This only obscures secrets from output logs.** If someone has the ability to edit your workflows, then they are able to read and therefore write secrets to somewhere else just like normal GitHub Secrets.
+107 -35
View File
@@ -6,12 +6,14 @@ const core = require('@actions/core');
const command = require('@actions/core/lib/command'); const command = require('@actions/core/lib/command');
const got = require('got'); const got = require('got');
const AUTH_METHODS = ['approle', 'token']; const AUTH_METHODS = ['approle', 'token', 'github'];
const VALID_KV_VERSION = [-1, 1, 2]; const VALID_KV_VERSION = [-1, 1, 2];
async function exportSecrets() { async function exportSecrets() {
const vaultUrl = core.getInput('url', { required: true }); const vaultUrl = core.getInput('url', { required: true });
const vaultNamespace = core.getInput('namespace', { required: false }); 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 enginePath = core.getInput('path', { required: false });
let kvVersion = core.getInput('kv-version', { required: false }); let kvVersion = core.getInput('kv-version', { required: false });
@@ -19,41 +21,27 @@ async function exportSecrets() {
const secretsInput = core.getInput('secrets', { required: true }); const secretsInput = core.getInput('secrets', { required: true });
const secretRequests = parseSecretsInput(secretsInput); const secretRequests = parseSecretsInput(secretsInput);
const vaultMethod = core.getInput('method', { required: false }) || 'token'; const vaultMethod = (core.getInput('method', { required: false }) || 'token').toLowerCase();
if (!AUTH_METHODS.includes(vaultMethod)) { if (!AUTH_METHODS.includes(vaultMethod)) {
throw Error(`Sorry, the authentication method ${vaultMethod} is not currently supported.`); throw Error(`Sorry, the authentication method ${vaultMethod} is not currently supported.`);
} }
let vaultToken = null; const defaultOptions = {
switch (vaultMethod) { prefixUrl: vaultUrl,
case 'approle': headers: {}
const vaultRoleId = core.getInput('roleId', { required: true });
const vaultSecretId = core.getInput('secretId', { required: true });
core.debug('Try to retrieve Vault Token from approle');
var options = {
headers: {},
json: { role_id: vaultRoleId, secret_id: vaultSecretId },
responseType: 'json'
};
if (vaultNamespace != null) {
options.headers["X-Vault-Namespace"] = vaultNamespace;
}
/** @type {any} */
const result = await got.post(`${vaultUrl}/v1/auth/approle/login`, options);
if (result && result.body && result.body.auth && result.body.auth.client_token) {
vaultToken = result.body.auth.client_token;
core.debug('✔ Vault Token has retrieved from approle');
} else {
throw Error(`No token was retrieved with the role_id and secret_id provided.`);
}
break;
default:
vaultToken = core.getInput('token', { required: true });
break;
} }
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) { if (!enginePath) {
enginePath = 'secret'; enginePath = 'secret';
} }
@@ -76,11 +64,15 @@ async function exportSecrets() {
}, },
}; };
for (const [headerName, headerValue] of extraHeaders) {
requestOptions.headers[headerName] = headerValue;
}
if (vaultNamespace != null) { if (vaultNamespace != null) {
requestOptions.headers["X-Vault-Namespace"] = vaultNamespace; requestOptions.headers["X-Vault-Namespace"] = vaultNamespace;
} }
let requestPath = `${vaultUrl}/v1`; let requestPath = `v1`;
const kvRequest = !secretPath.startsWith('/') const kvRequest = !secretPath.startsWith('/')
if (!kvRequest) { if (!kvRequest) {
requestPath += secretPath; requestPath += secretPath;
@@ -95,7 +87,7 @@ async function exportSecrets() {
body = responseCache.get(requestPath); body = responseCache.get(requestPath);
core.debug(' using cached response'); core.debug(' using cached response');
} else { } else {
const result = await got(requestPath, requestOptions); const result = await client.get(requestPath, requestOptions);
body = result.body; body = result.body;
responseCache.set(requestPath, body); responseCache.set(requestPath, body);
} }
@@ -105,9 +97,11 @@ async function exportSecrets() {
const secretData = getResponseData(body, dataDepth); const secretData = getResponseData(body, dataDepth);
const value = selectData(secretData, secretSelector, isJSONPath); const value = selectData(secretData, secretSelector, isJSONPath);
command.issue('add-mask', value); command.issue('add-mask', value);
core.exportVariable(envVarName, `${value}`); if (exportEnv) {
core.exportVariable(envVarName, `${value}`);
}
core.setOutput(outputVarName, `${value}`); core.setOutput(outputVarName, `${value}`);
core.debug(`${secretPath} => outputs.${outputVarName} | env.${envVarName}`); core.debug(`${secretPath} => outputs.${outputVarName}${exportEnv ? ` | env.${envVarName}` : ''}`);
} }
}; };
@@ -180,6 +174,55 @@ function parseSecretsInput(secretsInput) {
return output; 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 * Parses a JSON response and returns the secret data
* @param {string} responseBody * @param {string} responseBody
@@ -216,6 +259,9 @@ function normalizeOutputKey(dataKey) {
} }
// @ts-ignore // @ts-ignore
/**
* @param {string} input
*/
function parseBoolInput(input) { function parseBoolInput(input) {
if (input === null || input === undefined || input.trim() === '') { if (input === null || input === undefined || input.trim() === '') {
return null; return null;
@@ -223,9 +269,35 @@ function parseBoolInput(input) {
return Boolean(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 = { module.exports = {
exportSecrets, exportSecrets,
parseSecretsInput, parseSecretsInput,
parseResponse: getResponseData, parseResponse: getResponseData,
normalizeOutputKey normalizeOutputKey,
parseHeadersInput
}; };
+71 -11
View File
@@ -7,7 +7,8 @@ const got = require('got');
const { const {
exportSecrets, exportSecrets,
parseSecretsInput, parseSecretsInput,
parseResponse parseResponse,
parseHeadersInput
} = require('./action'); } = require('./action');
const { when } = require('jest-when'); 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', () => { describe('parseResponse', () => {
// https://www.vaultproject.io/api/secret/kv/kv-v1.html#sample-response // https://www.vaultproject.io/api/secret/kv/kv-v1.html#sample-response
it('parses K/V version 1 response', () => { it('parses K/V version 1 response', () => {
@@ -148,22 +189,24 @@ describe('exportSecrets', () => {
.mockReturnValueOnce(version); .mockReturnValueOnce(version);
} }
function mockExtraHeaders(headerString) {
when(core.getInput)
.calledWith('extraHeaders')
.mockReturnValueOnce(headerString);
}
function mockVaultData(data, version='2') { function mockVaultData(data, version='2') {
switch(version) { switch(version) {
case '1': case '1':
got.mockResolvedValue({ got.extend.mockReturnValue({
body: JSON.stringify({ get: async () => ({ body: JSON.stringify({ data }) })
data
})
}); });
break; break;
case '2': case '2':
got.mockResolvedValue({ got.extend.mockReturnValue({
body: JSON.stringify({ get: async () => ({ body: JSON.stringify({ data: {
data: { data
data } }) })
}
})
}); });
break; break;
} }
@@ -196,6 +239,23 @@ describe('exportSecrets', () => {
it('simple secret retrieval from K/V v1', async () => { it('simple secret retrieval from K/V v1', async () => {
const version = '1'; 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'); mockInput('test key');
mockVersion(version); mockVersion(version);
mockVaultData({ mockVaultData({
+27 -3
View File
@@ -4,15 +4,39 @@ inputs:
url: url:
description: 'The URL for the vault endpoint' description: 'The URL for the vault endpoint'
required: true required: true
token:
description: 'The Vault Token to be used to authenticate with Vault'
required: true
secrets: secrets:
description: 'A semicolon-separated list of secrets to retrieve. These will automatically be converted to environmental variable keys. See README for more details' description: 'A semicolon-separated list of secrets to retrieve. These will automatically be converted to environmental variable keys. See README for more details'
required: true required: true
namespace: namespace:
description: 'The Vault namespace from which to query secrets. Vault Enterprise only, unset by default' description: 'The Vault namespace from which to query secrets. Vault Enterprise only, unset by default'
required: false required: false
path:
description: 'The path of a non-default K/V engine'
required: false
kv-version:
description: 'The version of the K/V engine to use. Default: 2'
required: false
method:
description: 'The method to use to authenticate with Vault. Default: token'
required: false
token:
description: 'The Vault Token to be used to authenticate with Vault'
required: false
roleId:
description: 'The Role Id for App Role authentication'
required: false
secretId:
description: 'The Secret Id for App Role authentication'
required: false
githubToken:
description: 'The Github Token to be used to authenticate with Vault'
required: false
extraHeaders:
description: 'A string of newline separated extra headers to include on every request.'
required: false
exportEnv:
description: 'Whether or not export secrets as environment variables. Default: true'
required: false
runs: runs:
using: 'node12' using: 'node12'
main: 'dist/index.js' main: 'dist/index.js'
+185 -75
View File
@@ -648,10 +648,14 @@ const defaults = {
maxRedirects: 10, maxRedirects: 10,
prefixUrl: '', prefixUrl: '',
methodRewriting: true, methodRewriting: true,
allowGetBody: false,
ignoreInvalidCookies: false, ignoreInvalidCookies: false,
context: {}, context: {},
_pagination: { _pagination: {
transform: (response) => { transform: (response) => {
if (response.request.options.responseType === 'json') {
return response.body;
}
return JSON.parse(response.body); return JSON.parse(response.body);
}, },
paginate: response => { paginate: response => {
@@ -1019,6 +1023,7 @@ exports.preNormalizeArguments = (options, defaults) => {
options.dnsCache = (_e = options.dnsCache, (_e !== null && _e !== void 0 ? _e : false)); options.dnsCache = (_e = options.dnsCache, (_e !== null && _e !== void 0 ? _e : false));
options.useElectronNet = Boolean(options.useElectronNet); options.useElectronNet = Boolean(options.useElectronNet);
options.methodRewriting = Boolean(options.methodRewriting); options.methodRewriting = Boolean(options.methodRewriting);
options.allowGetBody = Boolean(options.allowGetBody);
options.context = (_f = options.context, (_f !== null && _f !== void 0 ? _f : {})); options.context = (_f = options.context, (_f !== null && _f !== void 0 ? _f : {}));
return options; return options;
}; };
@@ -1131,7 +1136,8 @@ exports.normalizeArguments = (url, options, defaults) => {
} }
return normalizedOptions; return normalizedOptions;
}; };
const withoutBody = new Set(['GET', 'HEAD']); const withoutBody = new Set(['HEAD']);
const withoutBodyUnlessSpecified = 'GET';
exports.normalizeRequestArguments = async (options) => { exports.normalizeRequestArguments = async (options) => {
var _a, _b, _c; var _a, _b, _c;
options = exports.mergeOptions(options); options = exports.mergeOptions(options);
@@ -1146,6 +1152,9 @@ exports.normalizeRequestArguments = async (options) => {
if ((isBody || isForm || isJson) && withoutBody.has(options.method)) { if ((isBody || isForm || isJson) && withoutBody.has(options.method)) {
throw new TypeError(`The \`${options.method}\` method cannot be used with a body`); 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) { if ([isBody, isForm, isJson].filter(isTrue => isTrue).length > 1) {
throw new TypeError('The `body`, `json` and `form` options are mutually exclusive'); throw new TypeError('The `body`, `json` and `form` options are mutually exclusive');
} }
@@ -1192,7 +1201,7 @@ exports.normalizeRequestArguments = async (options) => {
// a payload body and the method semantics do not anticipate such a // a payload body and the method semantics do not anticipate such a
// body. // body.
if (is_1.default.undefined(headers['content-length']) && is_1.default.undefined(headers['transfer-encoding'])) { 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)) { !is_1.default.undefined(uploadBodySize)) {
// @ts-ignore We assign if it is undefined, so this IS correct // @ts-ignore We assign if it is undefined, so this IS correct
headers['content-length'] = String(uploadBodySize); headers['content-length'] = String(uploadBodySize);
@@ -1567,7 +1576,7 @@ module.exports.iterator = (emitter, event, options) => {
"use strict"; "use strict";
// rfc7231 6.1 // rfc7231 6.1
const statusCodeCacheableByDefault = [ const statusCodeCacheableByDefault = new Set([
200, 200,
203, 203,
204, 204,
@@ -1579,10 +1588,10 @@ const statusCodeCacheableByDefault = [
410, 410,
414, 414,
501, 501,
]; ]);
// This implementation does not understand partial responses (206) // This implementation does not understand partial responses (206)
const understoodStatuses = [ const understoodStatuses = new Set([
200, 200,
203, 203,
204, 204,
@@ -1597,7 +1606,14 @@ const understoodStatuses = [
410, 410,
414, 414,
501, 501,
]; ]);
const errorStatusCodes = new Set([
500,
502,
503,
504,
]);
const hopByHopHeaders = { const hopByHopHeaders = {
date: true, // included, because we add Age update Date date: true, // included, because we add Age update Date
@@ -1610,6 +1626,7 @@ const hopByHopHeaders = {
'transfer-encoding': true, 'transfer-encoding': true,
upgrade: true, upgrade: true,
}; };
const excludedFromRevalidationUpdate = { const excludedFromRevalidationUpdate = {
// Since the old body is reused, it doesn't make sense to change properties of the body // Since the old body is reused, it doesn't make sense to change properties of the body
'content-length': true, 'content-length': true,
@@ -1618,6 +1635,20 @@ const excludedFromRevalidationUpdate = {
'content-range': true, 'content-range': true,
}; };
function toNumberOrZero(s) {
const n = parseInt(s, 10);
return isFinite(n) ? n : 0;
}
// RFC 5861
function isErrorResponse(response) {
// consider undefined response as faulty
if(!response) {
return true
}
return errorStatusCodes.has(response.status);
}
function parseCacheControl(header) { function parseCacheControl(header) {
const cc = {}; const cc = {};
if (!header) return cc; if (!header) return cc;
@@ -1654,7 +1685,6 @@ module.exports = class CachePolicy {
cacheHeuristic, cacheHeuristic,
immutableMinTimeToLive, immutableMinTimeToLive,
ignoreCargoCult, ignoreCargoCult,
trustServerDate,
_fromObject, _fromObject,
} = {} } = {}
) { ) {
@@ -1670,8 +1700,6 @@ module.exports = class CachePolicy {
this._responseTime = this.now(); this._responseTime = this.now();
this._isShared = shared !== false; this._isShared = shared !== false;
this._trustServerDate =
undefined !== trustServerDate ? trustServerDate : true;
this._cacheHeuristic = this._cacheHeuristic =
undefined !== cacheHeuristic ? cacheHeuristic : 0.1; // 10% matches IE undefined !== cacheHeuristic ? cacheHeuristic : 0.1; // 10% matches IE
this._immutableMinTtl = this._immutableMinTtl =
@@ -1732,7 +1760,7 @@ module.exports = class CachePolicy {
'HEAD' === this._method || 'HEAD' === this._method ||
('POST' === this._method && this._hasExplicitExpiration())) && ('POST' === this._method && this._hasExplicitExpiration())) &&
// the response status code is understood by the cache, and // the response status code is understood by the cache, and
understoodStatuses.indexOf(this._status) !== -1 && understoodStatuses.has(this._status) &&
// the "no-store" cache directive does not appear in request or response header fields, and // the "no-store" cache directive does not appear in request or response header fields, and
!this._rescc['no-store'] && !this._rescc['no-store'] &&
// the "private" response directive does not appear in the response, if the cache is shared, and // the "private" response directive does not appear in the response, if the cache is shared, and
@@ -1751,7 +1779,7 @@ module.exports = class CachePolicy {
(this._isShared && this._rescc['s-maxage']) || (this._isShared && this._rescc['s-maxage']) ||
this._rescc.public || this._rescc.public ||
// has a status code that is defined as cacheable by default // has a status code that is defined as cacheable by default
statusCodeCacheableByDefault.indexOf(this._status) !== -1) statusCodeCacheableByDefault.has(this._status))
); );
} }
@@ -1898,24 +1926,13 @@ module.exports = class CachePolicy {
} }
/** /**
* Value of the Date response header or current time if Date was demed invalid * Value of the Date response header or current time if Date was invalid
* @return timestamp * @return timestamp
*/ */
date() { date() {
if (this._trustServerDate) {
return this._serverDate();
}
return this._responseTime;
}
_serverDate() {
const serverDate = Date.parse(this._resHeaders.date); const serverDate = Date.parse(this._resHeaders.date);
if (isFinite(serverDate)) { if (isFinite(serverDate)) {
const maxClockDrift = 8 * 3600 * 1000; return serverDate;
const clockDrift = Math.abs(this._responseTime - serverDate);
if (clockDrift < maxClockDrift) {
return serverDate;
}
} }
return this._responseTime; return this._responseTime;
} }
@@ -1927,19 +1944,14 @@ module.exports = class CachePolicy {
* @return Number * @return Number
*/ */
age() { age() {
let age = Math.max(0, (this._responseTime - this.date()) / 1000); let age = this._ageValue();
if (this._resHeaders.age) {
let ageValue = this._ageValue();
if (ageValue > age) age = ageValue;
}
const residentTime = (this.now() - this._responseTime) / 1000; const residentTime = (this.now() - this._responseTime) / 1000;
return age + residentTime; return age + residentTime;
} }
_ageValue() { _ageValue() {
const ageValue = parseInt(this._resHeaders.age); return toNumberOrZero(this._resHeaders.age);
return isFinite(ageValue) ? ageValue : 0;
} }
/** /**
@@ -1975,18 +1987,18 @@ module.exports = class CachePolicy {
} }
// if a response includes the s-maxage directive, a shared cache recipient MUST ignore the Expires field. // if a response includes the s-maxage directive, a shared cache recipient MUST ignore the Expires field.
if (this._rescc['s-maxage']) { if (this._rescc['s-maxage']) {
return parseInt(this._rescc['s-maxage'], 10); return toNumberOrZero(this._rescc['s-maxage']);
} }
} }
// If a response includes a Cache-Control field with the max-age directive, a recipient MUST ignore the Expires field. // If a response includes a Cache-Control field with the max-age directive, a recipient MUST ignore the Expires field.
if (this._rescc['max-age']) { if (this._rescc['max-age']) {
return parseInt(this._rescc['max-age'], 10); return toNumberOrZero(this._rescc['max-age']);
} }
const defaultMinTtl = this._rescc.immutable ? this._immutableMinTtl : 0; const defaultMinTtl = this._rescc.immutable ? this._immutableMinTtl : 0;
const serverDate = this._serverDate(); const serverDate = this.date();
if (this._resHeaders.expires) { if (this._resHeaders.expires) {
const expires = Date.parse(this._resHeaders.expires); const expires = Date.parse(this._resHeaders.expires);
// A cache recipient MUST interpret invalid date formats, especially the value "0", as representing a time in the past (i.e., "already expired"). // A cache recipient MUST interpret invalid date formats, especially the value "0", as representing a time in the past (i.e., "already expired").
@@ -2010,13 +2022,24 @@ module.exports = class CachePolicy {
} }
timeToLive() { timeToLive() {
return Math.max(0, this.maxAge() - this.age()) * 1000; const age = this.maxAge() - this.age();
const staleIfErrorAge = age + toNumberOrZero(this._rescc['stale-if-error']);
const staleWhileRevalidateAge = age + toNumberOrZero(this._rescc['stale-while-revalidate']);
return Math.max(0, age, staleIfErrorAge, staleWhileRevalidateAge) * 1000;
} }
stale() { stale() {
return this.maxAge() <= this.age(); return this.maxAge() <= this.age();
} }
_useStaleIfError() {
return this.maxAge() + toNumberOrZero(this._rescc['stale-if-error']) > this.age();
}
useStaleWhileRevalidate() {
return this.maxAge() + toNumberOrZero(this._rescc['stale-while-revalidate']) > this.age();
}
static fromObject(obj) { static fromObject(obj) {
return new this(undefined, undefined, { _fromObject: obj }); return new this(undefined, undefined, { _fromObject: obj });
} }
@@ -2134,6 +2157,13 @@ module.exports = class CachePolicy {
*/ */
revalidatedPolicy(request, response) { revalidatedPolicy(request, response) {
this._assertRequestHasHeaders(request); this._assertRequestHasHeaders(request);
if(this._useStaleIfError() && isErrorResponse(response)) { // I consider the revalidation request unsuccessful
return {
modified: false,
matches: false,
policy: this,
};
}
if (!response || !response.headers) { if (!response || !response.headers) {
throw Error('Response headers missing'); throw Error('Response headers missing');
} }
@@ -2211,7 +2241,6 @@ module.exports = class CachePolicy {
shared: this._isShared, shared: this._isShared,
cacheHeuristic: this._cacheHeuristic, cacheHeuristic: this._cacheHeuristic,
immutableMinTimeToLive: this._immutableMinTtl, immutableMinTimeToLive: this._immutableMinTtl,
trustServerDate: this._trustServerDate,
}), }),
modified: false, modified: false,
matches: true, matches: true,
@@ -2881,19 +2910,21 @@ const create = (defaults) => {
const result = await got(normalizedOptions); const result = await got(normalizedOptions);
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const parsed = await pagination.transform(result); const parsed = await pagination.transform(result);
const current = [];
for (const item of parsed) { for (const item of parsed) {
if (pagination.filter(item, all)) { if (pagination.filter(item, all, current)) {
if (!pagination.shouldContinue(item, all)) { if (!pagination.shouldContinue(item, all, current)) {
return; return;
} }
yield item; yield item;
all.push(item); all.push(item);
current.push(item);
if (all.length === pagination.countLimit) { if (all.length === pagination.countLimit) {
return; return;
} }
} }
} }
const optionsToMerge = pagination.paginate(result); const optionsToMerge = pagination.paginate(result, all, current);
if (optionsToMerge === false) { if (optionsToMerge === false) {
return; return;
} }
@@ -3146,7 +3177,7 @@ function asStream(options) {
throw new Error('Got\'s stream is not writable when the `body`, `json` or `form` option is used'); 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; options.body = input;
} }
else { else {
@@ -3551,6 +3582,13 @@ exports.setFailed = setFailed;
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
// Logging Commands // 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 * Writes debug message to user log
* @param message debug message * @param message debug message
@@ -4847,12 +4885,14 @@ const core = __webpack_require__(470);
const command = __webpack_require__(431); const command = __webpack_require__(431);
const got = __webpack_require__(77); const got = __webpack_require__(77);
const AUTH_METHODS = ['approle', 'token']; const AUTH_METHODS = ['approle', 'token', 'github'];
const VALID_KV_VERSION = [-1, 1, 2]; const VALID_KV_VERSION = [-1, 1, 2];
async function exportSecrets() { async function exportSecrets() {
const vaultUrl = core.getInput('url', { required: true }); const vaultUrl = core.getInput('url', { required: true });
const vaultNamespace = core.getInput('namespace', { required: false }); 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 enginePath = core.getInput('path', { required: false });
let kvVersion = core.getInput('kv-version', { required: false }); let kvVersion = core.getInput('kv-version', { required: false });
@@ -4860,41 +4900,27 @@ async function exportSecrets() {
const secretsInput = core.getInput('secrets', { required: true }); const secretsInput = core.getInput('secrets', { required: true });
const secretRequests = parseSecretsInput(secretsInput); const secretRequests = parseSecretsInput(secretsInput);
const vaultMethod = core.getInput('method', { required: false }) || 'token'; const vaultMethod = (core.getInput('method', { required: false }) || 'token').toLowerCase();
if (!AUTH_METHODS.includes(vaultMethod)) { if (!AUTH_METHODS.includes(vaultMethod)) {
throw Error(`Sorry, the authentication method ${vaultMethod} is not currently supported.`); throw Error(`Sorry, the authentication method ${vaultMethod} is not currently supported.`);
} }
let vaultToken = null; const defaultOptions = {
switch (vaultMethod) { prefixUrl: vaultUrl,
case 'approle': headers: {}
const vaultRoleId = core.getInput('roleId', { required: true });
const vaultSecretId = core.getInput('secretId', { required: true });
core.debug('Try to retrieve Vault Token from approle');
var options = {
headers: {},
json: { role_id: vaultRoleId, secret_id: vaultSecretId },
responseType: 'json'
};
if (vaultNamespace != null) {
options.headers["X-Vault-Namespace"] = vaultNamespace;
}
/** @type {any} */
const result = await got.post(`${vaultUrl}/v1/auth/approle/login`, options);
if (result && result.body && result.body.auth && result.body.auth.client_token) {
vaultToken = result.body.auth.client_token;
core.debug('✔ Vault Token has retrieved from approle');
} else {
throw Error(`No token was retrieved with the role_id and secret_id provided.`);
}
break;
default:
vaultToken = core.getInput('token', { required: true });
break;
} }
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) { if (!enginePath) {
enginePath = 'secret'; enginePath = 'secret';
} }
@@ -4917,11 +4943,15 @@ async function exportSecrets() {
}, },
}; };
for (const [headerName, headerValue] of extraHeaders) {
requestOptions.headers[headerName] = headerValue;
}
if (vaultNamespace != null) { if (vaultNamespace != null) {
requestOptions.headers["X-Vault-Namespace"] = vaultNamespace; requestOptions.headers["X-Vault-Namespace"] = vaultNamespace;
} }
let requestPath = `${vaultUrl}/v1`; let requestPath = `v1`;
const kvRequest = !secretPath.startsWith('/') const kvRequest = !secretPath.startsWith('/')
if (!kvRequest) { if (!kvRequest) {
requestPath += secretPath; requestPath += secretPath;
@@ -4936,7 +4966,7 @@ async function exportSecrets() {
body = responseCache.get(requestPath); body = responseCache.get(requestPath);
core.debug(' using cached response'); core.debug(' using cached response');
} else { } else {
const result = await got(requestPath, requestOptions); const result = await client.get(requestPath, requestOptions);
body = result.body; body = result.body;
responseCache.set(requestPath, body); responseCache.set(requestPath, body);
} }
@@ -4946,9 +4976,11 @@ async function exportSecrets() {
const secretData = getResponseData(body, dataDepth); const secretData = getResponseData(body, dataDepth);
const value = selectData(secretData, secretSelector, isJSONPath); const value = selectData(secretData, secretSelector, isJSONPath);
command.issue('add-mask', value); command.issue('add-mask', value);
core.exportVariable(envVarName, `${value}`); if (exportEnv) {
core.exportVariable(envVarName, `${value}`);
}
core.setOutput(outputVarName, `${value}`); core.setOutput(outputVarName, `${value}`);
core.debug(`${secretPath} => outputs.${outputVarName} | env.${envVarName}`); core.debug(`${secretPath} => outputs.${outputVarName}${exportEnv ? ` | env.${envVarName}` : ''}`);
} }
}; };
@@ -5021,6 +5053,55 @@ function parseSecretsInput(secretsInput) {
return output; 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 * Parses a JSON response and returns the secret data
* @param {string} responseBody * @param {string} responseBody
@@ -5057,6 +5138,9 @@ function normalizeOutputKey(dataKey) {
} }
// @ts-ignore // @ts-ignore
/**
* @param {string} input
*/
function parseBoolInput(input) { function parseBoolInput(input) {
if (input === null || input === undefined || input.trim() === '') { if (input === null || input === undefined || input.trim() === '') {
return null; return null;
@@ -5064,11 +5148,37 @@ function parseBoolInput(input) {
return Boolean(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 = { module.exports = {
exportSecrets, exportSecrets,
parseSecretsInput, parseSecretsInput,
parseResponse: getResponseData, parseResponse: getResponseData,
normalizeOutputKey normalizeOutputKey,
parseHeadersInput
}; };
@@ -212,7 +212,7 @@ describe('authenticate with approle', () => {
jest.resetAllMocks(); jest.resetAllMocks();
when(core.getInput) when(core.getInput)
.calledWith('method') .calledWith('method', expect.anything())
.mockReturnValueOnce('approle'); .mockReturnValueOnce('approle');
when(core.getInput) when(core.getInput)
.calledWith('roleId') .calledWith('roleId')
@@ -228,7 +228,7 @@ describe('authenticate with approle', () => {
.mockReturnValueOnce('ns2'); .mockReturnValueOnce('ns2');
}); });
it('authenticate with approle', async()=> { it('authenticate with approle', async() => {
mockInput('test secret'); mockInput('test secret');
await exportSecrets(); await exportSecrets();
+999 -1014
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -42,8 +42,9 @@
"got": "^10.2.2" "got": "^10.2.2"
}, },
"devDependencies": { "devDependencies": {
"@types/got": "^9.6.9",
"@types/jest": "^25.1.3", "@types/jest": "^25.1.3",
"@zeit/ncc": "^0.21.1", "@zeit/ncc": "^0.22.0",
"jest": "^25.1.0", "jest": "^25.1.0",
"jest-when": "^2.7.0", "jest-when": "^2.7.0",
"semantic-release": "^15.13.24" "semantic-release": "^15.13.24"