Compare commits

...

32 Commits

Author SHA1 Message Date
Richard Simpson 5c464962be feat: add generic auth (#39)
* feat: add generic auth

Adds the ability to authenticate against any normal Vault endpoint by added the `authPayload` input.

When an unrecognized method is provided, the action will attempt to hit `v1/auth/<method>/login` with the provided `authPayload and parse out the token in the response
2020-04-04 13:19:48 -05:00
dependabot-preview[bot] ea29244204 chore(deps): [security] bump minimist from 1.2.0 to 1.2.5 (#38)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.0 to 1.2.5. **This update includes security fixes.**
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.0...1.2.5)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-04-03 20:49:29 -05:00
Richard Simpson 5de02d5a14 docs: fix type 2020-04-03 12:34:34 -05:00
Richard Simpson 8845b5c724 docs: Actually mark required as required 2020-04-03 12:33:08 -05:00
Richard Simpson c80d0b130c docs: Clean up output a bit 2020-04-03 12:30:03 -05:00
Richard Simpson 01bb0f9bc9 docs: add reference 2020-04-03 12:24:42 -05:00
dependabot-preview[bot] 5a70924133 chore(deps-dev): bump semantic-release from 15.13.24 to 17.0.4 (#18)
Bumps [semantic-release](https://github.com/semantic-release/semantic-release) from 15.13.24 to 17.0.4.
- [Release notes](https://github.com/semantic-release/semantic-release/releases)
- [Commits](https://github.com/semantic-release/semantic-release/compare/v15.13.24...v17.0.4)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-04-03 11:00:18 -05:00
dependabot-preview[bot] b5fdbf352b chore(deps-dev): bump jest-when from 2.7.0 to 2.7.1 (#34)
Bumps [jest-when](https://github.com/timkindberg/jest-when) from 2.7.0 to 2.7.1.
- [Release notes](https://github.com/timkindberg/jest-when/releases)
- [Commits](https://github.com/timkindberg/jest-when/commits)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-04-03 10:53:49 -05:00
dependabot-preview[bot] 268a32f886 chore(deps-dev): bump @types/jest from 25.1.4 to 25.1.5 (#36)
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 25.1.4 to 25.1.5.
- [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-04-03 10:53:43 -05:00
dependabot-preview[bot] f2b4d0b645 chore(deps-dev): bump jest from 25.2.1 to 25.2.7 (#37)
Bumps [jest](https://github.com/facebook/jest) from 25.2.1 to 25.2.7.
- [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.2.1...v25.2.7)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-04-03 10:53:38 -05:00
Richard Simpson 675d33e3da docs: add note about exportEnv option 2020-04-03 10:52:40 -05:00
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
Richard Simpson 4edbc9a77a chore: fix step output syntax 2020-02-20 11:38:11 -06:00
Richard Simpson 5357098084 fix(tests): fix unit tests and improve e2e 2020-02-20 11:24:04 -06:00
dependabot-preview[bot] 3dfe7ff808 chore(deps): bump got from 10.5.7 to 10.6.0
Bumps [got](https://github.com/sindresorhus/got) from 10.5.7 to 10.6.0.
- [Release notes](https://github.com/sindresorhus/got/releases)
- [Commits](https://github.com/sindresorhus/got/compare/v10.5.7...v10.6.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-02-20 17:14:12 +00:00
dependabot-preview[bot] 1049321b7a chore(deps-dev): bump @types/jest from 24.0.18 to 25.1.3
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 24.0.18 to 25.1.3.
- [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>
2020-02-20 17:14:05 +00:00
Richard Simpson ec10b5e257 feat: add ability to retrieve secrets via ouputs 2020-02-20 11:13:47 -06:00
Richard Simpson 7d1d7d26ad fix(build): bump build file 2020-02-20 01:48:49 -06:00
15 changed files with 4134 additions and 2537 deletions
+3
View File
@@ -112,6 +112,7 @@ jobs:
VAULT_PORT: ${{ job.services.vault.ports[8200] }}
- name: use vault action (default K/V version 2)
uses: ./
id: kv-secrets
with:
url: http://localhost:${{ job.services.vault.ports[8200] }}
token: testtoken
@@ -140,6 +141,8 @@ jobs:
/cubbyhole/test zip | NAMED_CUBBYSECRET ;
- name: verify
run: npm run test:e2e
env:
OTHER_SECRET_OUTPUT: ${{ steps.kv-secrets.outputs.otherSecret }}
publish:
if: github.event_name == 'push' && contains(github.ref, 'master')
+3
View File
@@ -0,0 +1,3 @@
{
"markdown-toc.depthFrom": 2
}
+113 -11
View File
@@ -4,6 +4,25 @@ 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).
<!-- 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)
- [Reference](#reference)
- [Masking - Hiding Secrets from Logs](#masking---hiding-secrets-from-logs)
<!-- /TOC -->
## Example Usage
```yaml
@@ -28,14 +47,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:
- **token**: (by default) you must provide a token parameter
- **token**: (by default) you must provide a `token` parameter
```yaml
...
with:
url: https://vault.mycompany.com:8200
token: ${{ secrets.VaultToken }}
```
- **approle**: you must provide a roleId & secretId parameter
- **approle**: you must provide a `roleId` & `secretId` parameter
```yaml
...
with:
@@ -44,6 +63,14 @@ with:
roleId: ${{ secrets.roleId }}
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
@@ -52,7 +79,7 @@ The `secrets` parameter is a set of multiple secret requests separated by the `;
Each secret request is comprised of the `path` and the `key` of the desired secret, and optionally the desired Env Var output name.
```raw
{{ Secret Path }} {{ Secret Key }} | {{ Output Environment Variable Name }}
{{ Secret Path }} {{ Secret Key }} | {{ Output Variable Name }}
```
### Simple Key
@@ -64,15 +91,30 @@ with:
secrets: ci npmToken
```
`vault-action` will automatically normalize the given data key, and output:
`vault-action` will automatically normalize the given secret selector key, and set the follow as environment variables for the following steps in the current job:
```bash
NPMTOKEN=somelongtoken
```
### Set Environment Variable Name
You can also access the secret via ouputs:
However, if you want to set it to a specific environmental variable, say `NPM_TOKEN`, you could do this instead:
```yaml
steps:
# ...
- name: Import Secrets
id: secrets
# Import config...
- name: Sensitive Operation
run: "my-cli --token '${{ steps.secrets.outputs.npmToken }}'"
```
_**Note:** If you'd like to only use outputs and disable automatic environment variables, you can set the `exportEnv` option to `false`._
### Set Output Variable Name
However, if you want to set it to a specific name, say `NPM_TOKEN`, you could do this instead:
```yaml
with:
@@ -85,6 +127,17 @@ With that, `vault-action` will now use your requested name and output:
NPM_TOKEN=somelongtoken
```
```yaml
steps:
# ...
- name: Import Secrets
id: secrets
# Import config...
- name: Sensitive Operation
run: "my-cli --token '${{ steps.secrets.outputs.NPM_TOKEN }}'"
```
### Multiple Secrets
This action can take multi-line input, so say you had your AWS keys stored in a path and wanted to retrieve both of them. You can do:
@@ -107,7 +160,7 @@ with:
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
can pass it as follows:
@@ -120,7 +173,7 @@ with:
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.
@@ -147,7 +200,20 @@ with:
Resulting in:
```bash
FOO=bar MY_KEY=zap
FOO=bar
MY_KEY=zap
```
```yaml
steps:
# ...
- name: Import Secrets
id: secrets
# Import config...
- name: Sensitive Operation
run: "my-cli --token '${{ steps.secrets.outputs.foo }}'"
- name: Another Sensitive Operation
run: "my-cli --token '${{ steps.secrets.outputs.MY_KEY }}'"
```
Secrets pulled from the same `Secret Path` are cached by default. So if you, for example, are using the `aws` engine and retrieve a key, only a single key for a given path is returned.
@@ -165,6 +231,22 @@ would work fine.
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
### Namespace
@@ -187,6 +269,26 @@ steps:
ci npm_token
```
## Masking
## Reference
This action uses Github Action's built in masking, so all variables will automatically be masked if printed to the console or to logs.
Here is all the inputs available through `with`:
| Input | Description | Default | Required |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- |
| `url` | The URL for the vault endpoint | | ✔ |
| `secrets` | A semicolon-separated list of secrets to retrieve. These will automatically be converted to environmental variable keys. See README for more details | | ✔ |
| `namespace` | The Vault namespace from which to query secrets. Vault Enterprise only, unset by default | | |
| `path` | The path of a non-default K/V engine | | |
| `kv-version` | The version of the K/V engine to use. | `2` | |
| `method` | The method to use to authenticate with Vault. | `token` | |
| `token` | The Vault Token to be used to authenticate with Vault | | |
| `roleId` | The Role Id for App Role authentication | | |
| `secretId` | The Secret Id for App Role authentication | | |
| `githubToken` | The Github Token to be used to authenticate with Vault | | |
| `extraHeaders` | A string of newline separated extra headers to include on every request. | | |
| `exportEnv` | Whether or not export secrets as environment variables. | `true` | |
## Masking - Hiding Secrets from 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.
+34 -4
View File
@@ -1,18 +1,48 @@
name: 'Vault Secrets'
description: 'A Github Action that allows you to consume the v2 K/V backend of HashiCorp Vault as secure environment variables'
description: 'A Github Action that allows you to consume the v2 K/V backend of HashiCorp Vault as secure environment variables'
inputs:
url:
description: 'The URL for the vault endpoint'
required: true
token:
description: 'The Vault Token to be used to authenticate with Vault'
required: true
secrets:
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
namespace:
description: 'The Vault namespace from which to query secrets. Vault Enterprise only, unset by default'
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
authPayload:
description: 'The JSON payload to be sent to Vault when using a custom authentication method.'
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:
using: 'node12'
main: 'dist/index.js'
+1417 -546
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -5,7 +5,7 @@ const core = require('@actions/core');
const got = require('got');
const { when } = require('jest-when');
const { exportSecrets } = require('../../action');
const { exportSecrets } = require('../../src/action');
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
+1
View File
@@ -3,6 +3,7 @@ describe('e2e', () => {
expect(process.env.SECRET).toBe("SUPERSECRET");
expect(process.env.NAMED_SECRET).toBe("SUPERSECRET");
expect(process.env.OTHERSECRET).toBe("OTHERSUPERSECRET");
expect(process.env.OTHER_SECRET_OUTPUT).toBe("OTHERSUPERSECRET");
expect(process.env.ALTSECRET).toBe("CUSTOMSECRET");
expect(process.env.NAMED_ALTSECRET).toBe("CUSTOMSECRET");
expect(process.env.OTHERALTSECRET).toBe("OTHERCUSTOMSECRET");
@@ -5,7 +5,7 @@ const core = require('@actions/core');
const got = require('got');
const { when } = require('jest-when');
const { exportSecrets } = require('../../action');
const { exportSecrets } = require('../../src/action');
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8201'}`;
@@ -212,7 +212,7 @@ describe('authenticate with approle', () => {
jest.resetAllMocks();
when(core.getInput)
.calledWith('method')
.calledWith('method', expect.anything())
.mockReturnValueOnce('approle');
when(core.getInput)
.calledWith('roleId')
+2271 -1881
View File
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -4,7 +4,7 @@
"description": "A Github Action that allows you to consume vault secrets as secure environment variables.",
"main": "dist/index.js",
"scripts": {
"build": "ncc build index.js -o dist",
"build": "ncc build src/index.js -o dist",
"test": "jest",
"test:integration:basic": "jest -c integrationTests/basic/jest.config.js",
"test:integration:enterprise": "jest -c integrationTests/enterprise/jest.config.js",
@@ -38,14 +38,15 @@
},
"homepage": "https://github.com/RichiCoder1/vault-action#readme",
"dependencies": {
"@actions/core": "^1.2.2",
"got": "^10.2.2"
"got": "^10.2.2",
"@actions/core": "^1.2.3"
},
"devDependencies": {
"@types/jest": "^24.0.18",
"@zeit/ncc": "^0.21.1",
"@types/got": "^9.6.9",
"@types/jest": "^25.1.3",
"@zeit/ncc": "^0.22.0",
"jest": "^25.1.0",
"jest-when": "^2.7.0",
"semantic-release": "^15.13.24"
"semantic-release": "^17.0.4"
}
}
+83 -49
View File
@@ -1,53 +1,45 @@
// @ts-check
const core = require('@actions/core');
const command = require('@actions/core/lib/command');
const got = require('got');
const got = require('got').default;
const { retrieveToken } = require('./auth');
const AUTH_METHODS = ['approle', 'token'];
const AUTH_METHODS = ['approle', 'token', 'github'];
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 });
const exportEnv = core.getInput('exportEnv', { required: false }) != 'false';
let enginePath = core.getInput('path', { required: false });
/** @type {number | string} */
let kvVersion = core.getInput('kv-version', { required: false });
const secretsInput = core.getInput('secrets', { required: true });
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)) {
throw Error(`Sorry, the authentication method ${vaultMethod} is not currently supported.`);
}
let vaultToken = null;
switch (vaultMethod) {
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');
var options = {
headers: {},
json: { role_id: vaultRoleId, secret_id: vaultSecretId },
responseType: 'json'
};
const defaultOptions = {
prefixUrl: vaultUrl,
headers: {}
}
for (const [headerName, headerValue] of extraHeaders) {
defaultOptions.headers[headerName] = headerValue;
}
if (vaultNamespace != null) {
options.headers["X-Vault-Namespace"] = vaultNamespace;
defaultOptions.headers["X-Vault-Namespace"] = vaultNamespace;
}
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;
}
const client = got.extend(defaultOptions);
const vaultToken = await retrieveToken(vaultMethod, client);
if (!enginePath) {
enginePath = 'secret';
@@ -64,18 +56,22 @@ async function exportSecrets() {
const responseCache = new Map();
for (const secretRequest of secretRequests) {
const { secretPath, outputName, secretSelector, isJSONPath } = secretRequest;
const { secretPath, outputVarName, envVarName, secretSelector, isJSONPath } = secretRequest;
const requestOptions = {
headers: {
'X-Vault-Token': vaultToken
},
};
for (const [headerName, headerValue] of extraHeaders) {
requestOptions.headers[headerName] = headerValue;
}
if (vaultNamespace != null) {
requestOptions.headers["X-Vault-Namespace"] = vaultNamespace;
}
let requestPath = `${vaultUrl}/v1`;
let requestPath = `v1`;
const kvRequest = !secretPath.startsWith('/')
if (!kvRequest) {
requestPath += secretPath;
@@ -90,7 +86,7 @@ async function exportSecrets() {
body = responseCache.get(requestPath);
core.debug(' using cached response');
} else {
const result = await got(requestPath, requestOptions);
const result = await client.get(requestPath, requestOptions);
body = result.body;
responseCache.set(requestPath, body);
}
@@ -98,13 +94,24 @@ async function exportSecrets() {
let dataDepth = isJSONPath === true ? 0 : kvRequest === false ? 1 : kvVersion;
const secretData = getResponseData(body, dataDepth);
const value = selectData(secretData, secretSelector);
const value = selectData(secretData, secretSelector, isJSONPath);
command.issue('add-mask', value);
core.exportVariable(outputName, `${value}`);
core.debug(`${secretPath} => ${outputName}`);
if (exportEnv) {
core.exportVariable(envVarName, `${value}`);
}
core.setOutput(outputVarName, `${value}`);
core.debug(`${secretPath} => outputs.${outputVarName}${exportEnv ? ` | env.${envVarName}` : ''}`);
}
};
/** @typedef {Object} SecretRequest
* @property {string} secretPath
* @property {string} envVarName
* @property {string} outputVarName
* @property {string} secretSelector
* @property {boolean} isJSONPath
*/
/**
* Parses a secrets input string into key paths and their resulting environment variable name.
* @param {string} secretsInput
@@ -116,18 +123,18 @@ function parseSecretsInput(secretsInput) {
.map(key => key.trim())
.filter(key => key.length !== 0);
/** @type {{ secretPath: string; outputName: string; dataKey: string; }[]} */
/** @type {SecretRequest[]} */
const output = [];
for (const secret of secrets) {
let path = secret;
let outputName = null;
let outputVarName = null;
const renameSigilIndex = secret.lastIndexOf('|');
if (renameSigilIndex > -1) {
path = secret.substring(0, renameSigilIndex).trim();
outputName = secret.substring(renameSigilIndex + 1).trim();
outputVarName = secret.substring(renameSigilIndex + 1).trim();
if (outputName.length < 1) {
if (outputVarName.length < 1) {
throw Error(`You must provide a value when mapping a secret to a name. Input: "${secret}"`);
}
}
@@ -138,21 +145,29 @@ function parseSecretsInput(secretsInput) {
.filter(part => part.length !== 0);
if (pathParts.length !== 2) {
throw Error(`You must provide a valid path and key. Input: "${secret}"`)
throw Error(`You must provide a valid path and key. Input: "${secret}"`);
}
const [secretPath, secretSelector] = pathParts;
// If we're not using a mapped name, normalize the key path into a variable name.
if (!outputName) {
outputName = normalizeOutputKey(secretSelector);
const isJSONPath = secretSelector.includes('.');
if (isJSONPath && !outputVarName) {
throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`);
}
let envVarName = outputVarName;
if (!outputVarName) {
outputVarName = secretSelector;
envVarName = normalizeOutputKey(outputVarName);
}
output.push({
secretPath,
outputName,
envVarName,
outputVarName,
secretSelector,
isJSONPath: secretSelector.startsWith('$')
isJSONPath
});
}
return output;
@@ -161,7 +176,7 @@ function parseSecretsInput(secretsInput) {
/**
* Parses a JSON response and returns the secret data
* @param {string} responseBody
* @param {number} kvVersion
* @param {number} dataLevel
*/
function getResponseData(responseBody, dataLevel) {
let secretData = JSON.parse(responseBody);
@@ -193,16 +208,35 @@ function normalizeOutputKey(dataKey) {
return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase();
}
function parseBoolInput(input) {
if (input === null || input === undefined || input.trim() === '') {
return null;
/**
* @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 Boolean(input);
return map;
}, new Map());
}
module.exports = {
exportSecrets,
parseSecretsInput,
parseResponse: getResponseData,
normalizeOutputKey
normalizeOutputKey,
parseHeadersInput
};
+85 -16
View File
@@ -7,7 +7,8 @@ const got = require('got');
const {
exportSecrets,
parseSecretsInput,
parseResponse
parseResponse,
parseHeadersInput
} = require('./action');
const { when } = require('jest-when');
@@ -18,7 +19,8 @@ describe('parseSecretsInput', () => {
expect(output).toContainEqual({
secretPath: 'test',
secretSelector: 'key',
outputName: 'KEY',
outputVarName: 'key',
envVarName: 'KEY',
isJSONPath: false
});
});
@@ -27,7 +29,8 @@ describe('parseSecretsInput', () => {
const output = parseSecretsInput('test key|testName');
expect(output).toHaveLength(1);
expect(output[0]).toMatchObject({
outputName: 'testName',
outputVarName: 'testName',
envVarName: 'testName',
});
});
@@ -58,10 +61,12 @@ describe('parseSecretsInput', () => {
expect(output).toHaveLength(2);
expect(output[0]).toMatchObject({
outputName: 'A',
outputVarName: 'a',
envVarName: 'A',
});
expect(output[1]).toMatchObject({
outputName: 'secondName',
outputVarName: 'secondName',
envVarName: 'secondName'
});
});
@@ -76,14 +81,56 @@ describe('parseSecretsInput', () => {
secretPath: 'first',
});
expect(output[1]).toMatchObject({
outputName: 'B',
outputVarName: 'b',
envVarName: 'B'
});
expect(output[2]).toMatchObject({
outputName: 'SOME_C',
outputVarName: 'SOME_C',
envVarName: 'SOME_C',
});
})
});
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', () => {
@@ -142,22 +189,24 @@ describe('exportSecrets', () => {
.mockReturnValueOnce(version);
}
function mockExtraHeaders(headerString) {
when(core.getInput)
.calledWith('extraHeaders')
.mockReturnValueOnce(headerString);
}
function mockVaultData(data, version='2') {
switch(version) {
case '1':
got.mockResolvedValue({
body: JSON.stringify({
data
})
got.extend.mockReturnValue({
get: async () => ({ body: JSON.stringify({ data }) })
});
break;
case '2':
got.mockResolvedValue({
body: JSON.stringify({
data: {
got.extend.mockReturnValue({
get: async () => ({ body: JSON.stringify({ data: {
data
}
})
} }) })
});
break;
}
@@ -172,6 +221,7 @@ describe('exportSecrets', () => {
await exportSecrets();
expect(core.exportVariable).toBeCalledWith('KEY', '1');
expect(core.setOutput).toBeCalledWith('key', '1');
});
it('mapped secret retrieval', async () => {
@@ -183,11 +233,29 @@ describe('exportSecrets', () => {
await exportSecrets();
expect(core.exportVariable).toBeCalledWith('TEST_NAME', '1');
expect(core.setOutput).toBeCalledWith('TEST_NAME', '1');
});
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({
@@ -197,5 +265,6 @@ describe('exportSecrets', () => {
await exportSecrets();
expect(core.exportVariable).toBeCalledWith('KEY', '1');
expect(core.setOutput).toBeCalledWith('key', '1');
});
});
+81
View File
@@ -0,0 +1,81 @@
// @ts-check
const core = require('@actions/core');
/***
* Authenticate with Vault and retrieve a Vault token that can be used for requests.
* @param {string} method
* @param {import('got').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 });
return await getClientToken(client, method, { role_id: vaultRoleId, secret_id: vaultSecretId });
}
case 'github': {
const githubToken = core.getInput('githubToken', { required: true });
return await getClientToken(client, method, { token: githubToken });
}
default: {
if (!method || method === 'token') {
return core.getInput('token', { required: true });
} else {
/** @type {string} */
const payload = core.getInput('authPayload', { required: true });
if (!payload) {
throw Error('When using a custom authentication method, you must provide the payload');
}
return await getClientToken(client, method, JSON.parse(payload.trim()));
}
}
}
}
/***
* Call the appropriate login endpoint and parse out the token in the response.
* @param {import('got').Got} client
* @param {string} method
* @param {any} payload
*/
async function getClientToken(client, method, payload) {
/** @type {'json'} */
const responseType = 'json';
var options = {
json: payload,
responseType,
};
core.debug(`Retrieving Vault Token from v1/auth/${method}/login endpoint`);
/** @type {import('got').Response<VaultLoginResponse>} */
const response = await client.post(`v1/auth/${method}/login`, options);
if (response && response.body && response.body.auth && response.body.auth.client_token) {
core.debug('✔ Vault Token successfully retrieved');
core.startGroup('Token Info');
core.debug(`Operating under policies: ${JSON.stringify(response.body.auth.policies)}`);
core.debug(`Token Metadata: ${JSON.stringify(response.body.auth.metadata)}`);
core.endGroup();
return response.body.auth.client_token;
} else {
throw Error(`Unable to retrieve token from ${method}'s login endpoint.`);
}
}
/***
* @typedef {Object} VaultLoginResponse
* @property {{
* client_token: string;
* accessor: string;
* policies: string[];
* metadata: unknown;
* lease_duration: number;
* renewable: boolean;
* }} auth
*/
module.exports = {
retrieveToken,
};
View File
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2019",
"moduleResolution": "node",
"allowJs": true,
"noEmit": true
},
"exclude": [
"node_modules"
]
}