From aee8834d656df8a15281a90d14599b022620fdeb Mon Sep 17 00:00:00 2001 From: Sai Pranav Date: Wed, 15 Mar 2023 17:07:59 -0500 Subject: [PATCH 1/4] fix(auth): added approle test in basic integration --- integrationTests/basic/approle_auth.test.js | 134 ++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 integrationTests/basic/approle_auth.test.js diff --git a/integrationTests/basic/approle_auth.test.js b/integrationTests/basic/approle_auth.test.js new file mode 100644 index 00000000..39e9c8ca --- /dev/null +++ b/integrationTests/basic/approle_auth.test.js @@ -0,0 +1,134 @@ +jest.mock('@actions/core'); +jest.mock('@actions/core/lib/command'); +const core = require('@actions/core'); + +const got = require('got'); +const { when } = require('jest-when'); + +const { exportSecrets } = require('../../src/action'); + +const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`; +const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}` + +describe('authenticate with approle', () => { + let roleId; + let secretId; + beforeAll(async () => { + try { + // Verify Connection + await got(`${vaultUrl}/v1/secret/config`, { + headers: { + 'X-Vault-Token': vaultToken, + }, + }); + + await got(`${vaultUrl}/v1/secret/data/approle-test`, { + method: 'POST', + headers: { + 'X-Vault-Token': vaultToken, + }, + json: { + data: { + secret: 'SUPERSECRET_WITH_APPROLE', + }, + }, + }); + + // Enable approle + try { + await got(`${vaultUrl}/v1/sys/auth/approle`, { + method: 'POST', + headers: { + 'X-Vault-Token': vaultToken + }, + json: { + type: 'approle' + }, + }); + } catch (error) { + const {response} = error; + if (response.statusCode === 400 && response.body.includes("path is already in use")) { + // Approle might already be enabled from previous test runs + } else { + throw error; + } + } + + // Create policies + await got(`${vaultUrl}/v1/sys/policies/acl/test`, { + method: 'POST', + headers: { + 'X-Vault-Token': vaultToken + }, + json: { + "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/approle-test\" {\n capabilities = [\"read\", \"list\"]\n}\npath \"secret/metadata/approle-test\" {\n capabilities = [\"read\", \"list\"]\n}\n" + }, + }); + + // Create approle + await got(`${vaultUrl}/v1/auth/approle/role/my-role`, { + method: 'POST', + headers: { + 'X-Vault-Token': vaultToken + }, + json: { + policies: 'test' + }, + }); + + // Get role-id + const roldIdResponse = await got(`${vaultUrl}/v1/auth/approle/role/my-role/role-id`, { + headers: { + 'X-Vault-Token': vaultToken + }, + responseType: 'json', + }); + 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': vaultToken + }, + responseType: 'json', + }); + secretId = secretIdResponse.body.data.secret_id; + } catch(err) { + console.warn('Create approle', err.response.body); + throw err; + } + }); + + beforeEach(() => { + jest.resetAllMocks(); + + when(core.getInput) + .calledWith('method', expect.anything()) + .mockReturnValueOnce('approle'); + when(core.getInput) + .calledWith('roleId', expect.anything()) + .mockReturnValueOnce(roleId); + when(core.getInput) + .calledWith('secretId', expect.anything()) + .mockReturnValueOnce(secretId); + when(core.getInput) + .calledWith('url', expect.anything()) + .mockReturnValueOnce(`${vaultUrl}`); + }); + + function mockInput(secrets) { + when(core.getInput) + .calledWith('secrets', expect.anything()) + .mockReturnValueOnce(secrets); + } + + it('authenticate with approle', async() => { + mockInput('secret/data/approle-test secret'); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET_WITH_APPROLE'); + }) +}); From 946e87c4c48a65faa28a9b4f9bb714e6a298b834 Mon Sep 17 00:00:00 2001 From: Sai Pranav Date: Wed, 15 Mar 2023 17:18:34 -0500 Subject: [PATCH 2/4] feat(auth): adding userpass and and ldap auth --- integrationTests/basic/userpass_auth.test.js | 116 +++++++++++++++++++ src/action.js | 2 +- src/auth.js | 14 ++- 3 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 integrationTests/basic/userpass_auth.test.js diff --git a/integrationTests/basic/userpass_auth.test.js b/integrationTests/basic/userpass_auth.test.js new file mode 100644 index 00000000..9ab51713 --- /dev/null +++ b/integrationTests/basic/userpass_auth.test.js @@ -0,0 +1,116 @@ +jest.mock('@actions/core'); +jest.mock('@actions/core/lib/command'); +const core = require('@actions/core'); + +const got = require('got'); +const { when } = require('jest-when'); + +const { exportSecrets } = require('../../src/action'); + +const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`; +const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}` + +describe('authenticate with userpass', () => { + const username = `testUsername`; + const password = `testPassword`; + beforeAll(async () => { + try { + // Verify Connection + await got(`${vaultUrl}/v1/secret/config`, { + headers: { + 'X-Vault-Token': vaultToken, + }, + }); + + await got(`${vaultUrl}/v1/secret/data/userpass-test`, { + method: 'POST', + headers: { + 'X-Vault-Token': vaultToken, + }, + json: { + data: { + secret: 'SUPERSECRET_WITH_USERPASS', + }, + }, + }); + + // Enable approle + try { + await got(`${vaultUrl}/v1/sys/auth/userpass`, { + method: 'POST', + headers: { + 'X-Vault-Token': vaultToken + }, + json: { + type: 'userpass' + }, + }); + } catch (error) { + const {response} = error; + if (response.statusCode === 400 && response.body.includes("path is already in use")) { + // Userpass might already be enabled from previous test runs + } else { + throw error; + } + } + + // Create policies + await got(`${vaultUrl}/v1/sys/policies/acl/userpass-test`, { + method: 'POST', + headers: { + 'X-Vault-Token': vaultToken + }, + json: { + "name":"userpass-test", + "policy":`path \"auth/userpass/*\" {\n capabilities = [\"read\", \"list\"]\n}\npath \"auth/userpass/users/${username}\"\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/userpass-test\" {\n capabilities = [\"read\", \"list\"]\n}\npath \"secret/metadata/userpass-test\" {\n capabilities = [\"read\", \"list\"]\n}\n` + }, + }); + + // Create user + await got(`${vaultUrl}/v1/auth/userpass/users/${username}`, { + method: 'POST', + headers: { + 'X-Vault-Token': vaultToken + }, + json: { + password: `${password}`, + policies: 'userpass-test' + }, + }); + } catch(err) { + console.warn('Create user in userpass', err.response.body); + throw err; + } + }); + + beforeEach(() => { + jest.resetAllMocks(); + + when(core.getInput) + .calledWith('method', expect.anything()) + .mockReturnValueOnce('userpass'); + when(core.getInput) + .calledWith('username', expect.anything()) + .mockReturnValueOnce(username); + when(core.getInput) + .calledWith('password', expect.anything()) + .mockReturnValueOnce(password); + when(core.getInput) + .calledWith('url', expect.anything()) + .mockReturnValueOnce(`${vaultUrl}`); + }); + + function mockInput(secrets) { + when(core.getInput) + .calledWith('secrets', expect.anything()) + .mockReturnValueOnce(secrets); + } + + it('authenticate with userpass', async() => { + mockInput('secret/data/userpass-test secret'); + + await exportSecrets(); + + expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET_WITH_USERPASS'); + }) +}); diff --git a/src/action.js b/src/action.js index b898005d..836a11b1 100644 --- a/src/action.js +++ b/src/action.js @@ -5,7 +5,7 @@ const got = require('got').default; const jsonata = require('jsonata'); const { auth: { retrieveToken }, secrets: { getSecrets } } = require('./index'); -const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes']; +const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes', 'ldap', 'userpass']; const ENCODING_TYPES = ['base64', 'hex', 'utf8']; async function exportSecrets() { diff --git a/src/auth.js b/src/auth.js index c421d5ff..331083ab 100644 --- a/src/auth.js +++ b/src/auth.js @@ -11,7 +11,8 @@ const defaultKubernetesTokenPath = '/var/run/secrets/kubernetes.io/serviceaccoun * @param {import('got').Got} client */ async function retrieveToken(method, client) { - const path = core.getInput('path', { required: false }) || method; + let path = core.getInput('path', { required: false }) || method; + path = `v1/auth/${path}/login` switch (method) { case 'approle': { @@ -50,6 +51,13 @@ async function retrieveToken(method, client) { } return await getClientToken(client, method, path, { jwt: data, role: role }) } + case 'userpass': + case 'ldap': { + const username = core.getInput('username', { required: true }); + const password = core.getInput('password', { required: true }); + path = path + `/${username}` + return await getClientToken(client, method, path, { password: password }) + } default: { if (!method || method === 'token') { @@ -107,12 +115,12 @@ async function getClientToken(client, method, path, payload) { responseType, }; - core.debug(`Retrieving Vault Token from v1/auth/${path}/login endpoint`); + core.debug(`Retrieving Vault Token from ${path} endpoint`); /** @type {import('got').Response} */ let response; try { - response = await client.post(`v1/auth/${path}/login`, options); + response = await client.post(`${path}`, options); } catch (err) { if (err instanceof got.HTTPError) { throw Error(`failed to retrieve vault token. code: ${err.code}, message: ${err.message}, vaultResponse: ${JSON.stringify(err.response.body)}`) From c91dd2cae2e2e8678a9fe2ac0612d3179763eef5 Mon Sep 17 00:00:00 2001 From: Sai Pranav Date: Fri, 31 Mar 2023 12:11:42 -0500 Subject: [PATCH 3/4] changes from review --- action.yml | 6 ++++++ integrationTests/basic/userpass_auth.test.js | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/action.yml b/action.yml index 073f5794..6f1d26e9 100644 --- a/action.yml +++ b/action.yml @@ -36,6 +36,12 @@ inputs: description: 'The path to the Kubernetes service account secret' required: false default: '/var/run/secrets/kubernetes.io/serviceaccount/token' + username: + description: 'The username of the user to log in to Vault as. Available to both Userpass and LDAP auth methods' + required: false + password: + description: 'The password of the user to log in to Vault as. Available to both Userpass and LDAP auth methods' + required: false authPayload: description: 'The JSON payload to be sent to Vault when using a custom authentication method.' required: false diff --git a/integrationTests/basic/userpass_auth.test.js b/integrationTests/basic/userpass_auth.test.js index 9ab51713..a8a9933b 100644 --- a/integrationTests/basic/userpass_auth.test.js +++ b/integrationTests/basic/userpass_auth.test.js @@ -34,7 +34,7 @@ describe('authenticate with userpass', () => { }, }); - // Enable approle + // Enable userpass try { await got(`${vaultUrl}/v1/sys/auth/userpass`, { method: 'POST', From 7a7d84b0b19988da769d7913c33de4d8673d500f Mon Sep 17 00:00:00 2001 From: Sai Pranav Date: Fri, 31 Mar 2023 12:15:02 -0500 Subject: [PATCH 4/4] chore(changelog): added support for userpass and ldap auth --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b700914..9a52b6b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +Features: + +* Added support for userpass and ldap authentication methods [GH-440](https://github.com/hashicorp/vault-action/pull/440) + ## 2.5.0 (Jan 26th, 2023) Features: