Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): adding userpass and ldap auth #440

Merged
merged 4 commits into from
Mar 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
6 changes: 6 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
134 changes: 134 additions & 0 deletions integrationTests/basic/approle_auth.test.js
Original file line number Diff line number Diff line change
@@ -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');
})
});
116 changes: 116 additions & 0 deletions integrationTests/basic/userpass_auth.test.js
Original file line number Diff line number Diff line change
@@ -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 userpass
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');
})
});
2 changes: 1 addition & 1 deletion src/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
14 changes: 11 additions & 3 deletions src/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
robmonte marked this conversation as resolved.
Show resolved Hide resolved

switch (method) {
case 'approle': {
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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<VaultLoginResponse>} */
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)}`)
Expand Down