From 2ae5b6c884a88fb96b2f75794479bd8d4a931694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20FAUVART?= Date: Wed, 20 Nov 2019 22:51:41 +0100 Subject: [PATCH] feat(authenticate): add approle auth method --- README.md | 8 + action.js | 38 ++++- dist/index.js | 31 +++- docker-compose.yml | 2 +- .../enterprise/enterprise.test.js | 148 ++++++++++++++++++ 5 files changed, 222 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8d3e5fe..1dc0e96 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ jobs: uses: RichiCoder1/vault-action with: url: https://vault.mycompany.com:8200 + method: token token: ${{ secrets.VaultToken }} secrets: | ci/aws accessKey | AWS_ACCESS_KEY_ID ; @@ -22,6 +23,12 @@ jobs: # ... ``` +## Authentication method + +The `method` parameter can have these value : +- **token**: you must provide a token parameter +- **approle**: you must provide a roleId & secretId parameter + ## Key Syntax The `secrets` parameter is a set of multiple secret requests separated by the `;` character. @@ -84,6 +91,7 @@ steps: uses: RichiCoder1/vault-action with: url: https://vault-enterprise.mycompany.com:8200 + method: token token: ${{ secrets.VaultToken }} namespace: ns1 secrets: | diff --git a/action.js b/action.js index 5b136cf..cf22219 100644 --- a/action.js +++ b/action.js @@ -3,13 +3,45 @@ const command = require('@actions/core/lib/command'); const got = require('got'); async function exportSecrets() { + const _methods = ['approle', 'token']; + const vaultUrl = core.getInput('url', { required: true }); - const vaultToken = core.getInput('token', { required: true }); + var vaultMethod = core.getInput('method', { required: false }); + const vaultRoleId = core.getInput('roleId', { required: false }); + const vaultSecretId = core.getInput('secretId', { required: false }); + var vaultToken = core.getInput('token', { required: false }); const vaultNamespace = core.getInput('namespace', { required: false }); const secretsInput = core.getInput('secrets', { required: true }); const secrets = parseSecretsInput(secretsInput); + if (!vaultMethod){ + vaultMethod = 'token' + } + + if (!_methods.includes(vaultMethod)) { + throw Error(`Sorry, method ${vaultMethod} currently not implemented.`); + } + + switch (vaultMethod) { + case 'approle': + core.debug('Try to retrieve Vault Token from approle') + var options = { headers: { }, json: true, body: { role_id: vaultRoleId, secret_id: vaultSecretId }, responseType: 'json' }; + if (vaultNamespace != null){ + options.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: + break; + } + for (const secret of secrets) { const { secretPath, outputName, secretKey } = secret; const requestOptions = { @@ -35,7 +67,7 @@ async function exportSecrets() { /** * Parses a secrets input string into key paths and their resulting environment variable name. - * @param {string} secretsInput + * @param {string} secretsInput */ function parseSecretsInput(secretsInput) { const secrets = secretsInput @@ -86,7 +118,7 @@ function parseSecretsInput(secretsInput) { } /** - * Replaces any forward-slash characters to + * Replaces any forward-slash characters to * @param {string} dataKey */ function normalizeOutputKey(dataKey) { diff --git a/dist/index.js b/dist/index.js index 647a6b0..4aaee41 100644 --- a/dist/index.js +++ b/dist/index.js @@ -2875,7 +2875,7 @@ module.exports = function (obj) { /***/ 482: /***/ (function(module) { -module.exports = {"_from":"got","_id":"got@9.6.0","_inBundle":false,"_integrity":"sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==","_location":"/got","_phantomChildren":{},"_requested":{"type":"tag","registry":true,"raw":"got","name":"got","escapedName":"got","rawSpec":"","saveSpec":null,"fetchSpec":"latest"},"_requiredBy":["#USER","/"],"_resolved":"https://registry.npmjs.org/got/-/got-9.6.0.tgz","_shasum":"edf45e7d67f99545705de1f7bbeeeb121765ed85","_spec":"got","_where":"C:\\src\\richicoder1\\vault-action","ava":{"concurrency":4},"browser":{"decompress-response":false,"electron":false},"bugs":{"url":"https://github.com/sindresorhus/got/issues"},"bundleDependencies":false,"dependencies":{"@sindresorhus/is":"^0.14.0","@szmarczak/http-timer":"^1.1.2","cacheable-request":"^6.0.0","decompress-response":"^3.3.0","duplexer3":"^0.1.4","get-stream":"^4.1.0","lowercase-keys":"^1.0.1","mimic-response":"^1.0.1","p-cancelable":"^1.0.0","to-readable-stream":"^1.0.0","url-parse-lax":"^3.0.0"},"deprecated":false,"description":"Simplified HTTP requests","devDependencies":{"ava":"^1.1.0","coveralls":"^3.0.0","delay":"^4.1.0","form-data":"^2.3.3","get-port":"^4.0.0","np":"^3.1.0","nyc":"^13.1.0","p-event":"^2.1.0","pem":"^1.13.2","proxyquire":"^2.0.1","sinon":"^7.2.2","slow-stream":"0.0.4","tempfile":"^2.0.0","tempy":"^0.2.1","tough-cookie":"^3.0.0","xo":"^0.24.0"},"engines":{"node":">=8.6"},"files":["source"],"homepage":"https://github.com/sindresorhus/got#readme","keywords":["http","https","get","got","url","uri","request","util","utility","simple","curl","wget","fetch","net","network","electron"],"license":"MIT","main":"source","name":"got","repository":{"type":"git","url":"git+https://github.com/sindresorhus/got.git"},"scripts":{"release":"np","test":"xo && nyc ava"},"version":"9.6.0"}; +module.exports = {"_args":[["got@9.6.0","/Users/20012243/Dev/vault-action"]],"_from":"got@9.6.0","_id":"got@9.6.0","_inBundle":false,"_integrity":"sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==","_location":"/got","_phantomChildren":{},"_requested":{"type":"version","registry":true,"raw":"got@9.6.0","name":"got","escapedName":"got","rawSpec":"9.6.0","saveSpec":null,"fetchSpec":"9.6.0"},"_requiredBy":["/"],"_resolved":"https://registry.npmjs.org/got/-/got-9.6.0.tgz","_spec":"9.6.0","_where":"/Users/20012243/Dev/vault-action","ava":{"concurrency":4},"browser":{"decompress-response":false,"electron":false},"bugs":{"url":"https://github.com/sindresorhus/got/issues"},"dependencies":{"@sindresorhus/is":"^0.14.0","@szmarczak/http-timer":"^1.1.2","cacheable-request":"^6.0.0","decompress-response":"^3.3.0","duplexer3":"^0.1.4","get-stream":"^4.1.0","lowercase-keys":"^1.0.1","mimic-response":"^1.0.1","p-cancelable":"^1.0.0","to-readable-stream":"^1.0.0","url-parse-lax":"^3.0.0"},"description":"Simplified HTTP requests","devDependencies":{"ava":"^1.1.0","coveralls":"^3.0.0","delay":"^4.1.0","form-data":"^2.3.3","get-port":"^4.0.0","np":"^3.1.0","nyc":"^13.1.0","p-event":"^2.1.0","pem":"^1.13.2","proxyquire":"^2.0.1","sinon":"^7.2.2","slow-stream":"0.0.4","tempfile":"^2.0.0","tempy":"^0.2.1","tough-cookie":"^3.0.0","xo":"^0.24.0"},"engines":{"node":">=8.6"},"files":["source"],"homepage":"https://github.com/sindresorhus/got#readme","keywords":["http","https","get","got","url","uri","request","util","utility","simple","curl","wget","fetch","net","network","electron"],"license":"MIT","main":"source","name":"got","repository":{"type":"git","url":"git+https://github.com/sindresorhus/got.git"},"scripts":{"release":"np","test":"xo && nyc ava"},"version":"9.6.0"}; /***/ }), @@ -3747,12 +3747,41 @@ const command = __webpack_require__(431); const got = __webpack_require__(798); async function exportSecrets() { + const _methods = ['approle', 'token']; + const vaultUrl = core.getInput('url', { required: true }); const vaultToken = core.getInput('token', { required: true }); + const vaultNamespace = core.getInput('namespace', { required: false }); + const vaultMethod = core.getInput('method', { required: true }); + const vaultRoleId = core.getInput('roleId', { required: false }); + const vaultSecretId = core.getInput('secretId', { required: false }); const secretsInput = core.getInput('secrets', { required: true }); const secrets = parseSecretsInput(secretsInput); + if (!_methods.includes(vaultMethod)) { + throw Error(`Sorry, method ${vaultMethod} currently not implemented.`); + } + + switch (vaultMethod) { + case 'approle': + core.debug('Try to retrieve Vault Token from approle') + var options = { headers: { }, json: true, body: { role_id: vaultRoleId, secret_id: vaultSecretId }, responseType: 'json' }; + if (vaultNamespace != null){ + options.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: + break; + } + for (const secret of secrets) { const { secretPath, outputName, secretKey } = secret; const result = await got(`${vaultUrl}/v1/secret/data/${secretPath}`, { diff --git a/docker-compose.yml b/docker-compose.yml index ef01eca..bc5de24 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,5 +13,5 @@ services: environment: VAULT_DEV_ROOT_TOKEN_ID: testtoken ports: - - 8201:8201 + - 8201:8200 privileged: true \ No newline at end of file diff --git a/integrationTests/enterprise/enterprise.test.js b/integrationTests/enterprise/enterprise.test.js index a77c95d..4761c54 100644 --- a/integrationTests/enterprise/enterprise.test.js +++ b/integrationTests/enterprise/enterprise.test.js @@ -128,3 +128,151 @@ describe('integration', () => { expect(core.exportVariable).toBeCalledWith('OTHERSECRET', 'OTHERSUPERSECRET_IN_NAMESPACE'); }); }); + +describe('authenticate with approle', () => { + let roleId; + let secretId; + beforeAll(async () => { + try { + // Verify Connection + await got(`${vaultUrl}/v1/secret/config`, { + headers: { + 'X-Vault-Token': 'testtoken', + }, + }); + + // Create namespace + await got(`${vaultUrl}/v1/sys/namespaces/ns2`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + }, + json: true, + }); + + // Enable secret engine + await got(`${vaultUrl}/v1/sys/mounts/secret`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + 'X-Vault-Namespace': 'ns2', + }, + body: { path: 'secret', type: 'kv', config: {}, options: { version: 2 }, generate_signing_key: true }, + json: true, + }); + + // Add secret + await got(`${vaultUrl}/v1/secret/data/test`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + 'X-Vault-Namespace': 'ns2', + }, + body: { + data: { + secret: 'SUPERSECRET_WITH_APPROLE', + }, + }, + json: true, + }); + + // Enable approle + await got(`${vaultUrl}/v1/sys/auth/approle`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + 'X-Vault-Namespace': 'ns2', + }, + body: { + type: 'approle' + }, + json: true, + }); + + // Create policies + await got(`${vaultUrl}/v1/sys/policies/acl/test`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + 'X-Vault-Namespace': 'ns2', + }, + body: { + "name":"test", + "policy":"path \"auth/approle/*\" {\n capabilities = [\"read\", \"list\"]\n}\npath \"auth/approle/role/my-role/role-id\"\n{\n capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\"]\n}\npath \"auth/approle/role/my-role/secret-id\"\n{\n capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\"]\n}\n\npath \"secret/data/*\" {\n capabilities = [\"list\"]\n}\npath \"secret/metadata/*\" {\n capabilities = [\"list\"]\n}\n\npath \"secret/data/test\" {\n capabilities = [\"read\", \"list\"]\n}\npath \"secret/metadata/test\" {\n capabilities = [\"read\", \"list\"]\n}\npath \"secret/data/test/*\" {\n capabilities = [\"read\", \"list\"]\n}\npath \"secret/metadata/test/*\" {\n capabilities = [\"read\", \"list\"]\n}\n" + }, + json: true, + }); + + // Create approle + await got(`${vaultUrl}/v1/auth/approle/role/my-role`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + 'X-Vault-Namespace': 'ns2', + }, + body: { + policies: 'test' + }, + json: true, + }); + + // Get role-id + const roldIdResponse = await got(`${vaultUrl}/v1/auth/approle/role/my-role/role-id`, { + headers: { + 'X-Vault-Token': 'testtoken', + 'X-Vault-Namespace': 'ns2', + }, + json: true, + }); + roleId = roldIdResponse.body.data.role_id; + + // Get secret-id + const secretIdResponse = await got(`${vaultUrl}/v1/auth/approle/role/my-role/secret-id`, { + method: 'POST', + headers: { + 'X-Vault-Token': 'testtoken', + 'X-Vault-Namespace': 'ns2', + }, + json: true, + }); + secretId = secretIdResponse.body.data.secret_id; + } catch(err) { + console.warn('Create approle',err); + throw err + } + }); + + beforeEach(() => { + jest.resetAllMocks(); + + when(core.getInput) + .calledWith('method') + .mockReturnValue('approle'); + when(core.getInput) + .calledWith('roleId') + .mockReturnValue(roleId); + when(core.getInput) + .calledWith('secretId') + .mockReturnValue(secretId); + when(core.getInput) + .calledWith('url') + .mockReturnValue(`${vaultUrl}`); + when(core.getInput) + .calledWith('namespace') + .mockReturnValue('ns2'); + }); + + function mockInput(secrets) { + when(core.getInput) + .calledWith('secrets') + .mockReturnValue(secrets); + } + + it('authenticate with approle', async()=> { + mockInput('test secret'); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET_WITH_APPROLE'); + }) +}); \ No newline at end of file