diff --git a/README.md b/README.md index 9b3d087..3f398b9 100644 --- a/README.md +++ b/README.md @@ -46,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: -- **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: @@ -62,6 +62,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 diff --git a/action.js b/action.js index 4745e61..0082a4e 100644 --- a/action.js +++ b/action.js @@ -6,13 +6,14 @@ const core = require('@actions/core'); const command = require('@actions/core/lib/command'); const got = require('got'); -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 }); let kvVersion = core.getInput('kv-version', { required: false }); @@ -20,41 +21,28 @@ async function exportSecrets() { 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' - }; - - 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; + const defaultOptions = { + baseUrl: vaultUrl, + throwHttpErrors: true, + headers: {} } + 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, client); + if (!enginePath) { enginePath = 'secret'; } @@ -110,9 +98,11 @@ async function exportSecrets() { const secretData = getResponseData(body, dataDepth); const value = selectData(secretData, secretSelector, isJSONPath); command.issue('add-mask', value); - core.exportVariable(envVarName, `${value}`); + if (exportEnv) { + core.exportVariable(envVarName, `${value}`); + } core.setOutput(outputVarName, `${value}`); - core.debug(`✔ ${secretPath} => outputs.${outputVarName} | env.${envVarName}`); + core.debug(`✔ ${secretPath} => outputs.${outputVarName}${exportEnv ? ` | env.${envVarName}` : ''}`); } }; @@ -185,6 +175,55 @@ function parseSecretsInput(secretsInput) { 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 * @param {string} responseBody diff --git a/action.yml b/action.yml index 278eb8f..ac882f1 100644 --- a/action.yml +++ b/action.yml @@ -4,17 +4,38 @@ 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 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 runs: using: 'node12' diff --git a/package-lock.json b/package-lock.json index 6e78306..baa1101 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1929,6 +1929,30 @@ "@types/node": "*" } }, + "@types/got": { + "version": "9.6.9", + "resolved": "https://registry.npmjs.org/@types/got/-/got-9.6.9.tgz", + "integrity": "sha512-w+ZE+Ovp6fM+1sHwJB7RN3f3pTJHZkyABuULqbtknqezQyWadFEp5BzOXaZzRqAw2md6/d3ybxQJt+BNgpvzOg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + }, + "dependencies": { + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } + } + }, "@types/http-cache-semantics": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", @@ -2141,6 +2165,12 @@ "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", "dev": true }, + "@types/tough-cookie": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.6.tgz", + "integrity": "sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ==", + "dev": true + }, "@types/yargs": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.2.tgz", diff --git a/package.json b/package.json index 55a270e..8b09ec8 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "got": "^10.2.2" }, "devDependencies": { + "@types/got": "^9.6.9", "@types/jest": "^25.1.3", "@zeit/ncc": "^0.22.0", "jest": "^25.1.0",