Compare commits

..

11 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
9 changed files with 1280 additions and 1130 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.
+72 -34
View File
@@ -6,13 +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 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 });
@@ -20,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';
} }
@@ -85,7 +72,7 @@ async function exportSecrets() {
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;
@@ -100,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);
} }
@@ -110,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}` : ''}`);
} }
}; };
@@ -185,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
+6 -10
View File
@@ -198,19 +198,15 @@ describe('exportSecrets', () => {
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;
} }
+25 -4
View File
@@ -4,17 +4,38 @@ 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: extraHeaders:
description: 'A string of newline seperated extra headers to include on every request.' 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 required: false
runs: runs:
using: 'node12' using: 'node12'
+134 -71
View File
@@ -653,6 +653,9 @@ const defaults = {
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 => {
@@ -1573,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,
@@ -1585,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,
@@ -1603,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
@@ -1616,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,
@@ -1624,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;
@@ -1660,7 +1685,6 @@ module.exports = class CachePolicy {
cacheHeuristic, cacheHeuristic,
immutableMinTimeToLive, immutableMinTimeToLive,
ignoreCargoCult, ignoreCargoCult,
trustServerDate,
_fromObject, _fromObject,
} = {} } = {}
) { ) {
@@ -1676,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 =
@@ -1738,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
@@ -1757,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))
); );
} }
@@ -1904,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;
} }
@@ -1933,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;
} }
/** /**
@@ -1981,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").
@@ -2016,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 });
} }
@@ -2140,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');
} }
@@ -2217,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,
@@ -2887,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;
} }
@@ -4860,13 +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 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 });
@@ -4874,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';
} }
@@ -4939,7 +4951,7 @@ async function exportSecrets() {
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;
@@ -4954,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);
} }
@@ -4964,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}` : ''}`);
} }
}; };
@@ -5039,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
@@ -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();
+987 -1002
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"