Skip to content

Commit 1d767e3

Browse files
authored
Add userpass auth and ldap auth support (#440)
* fix(auth): added approle test in basic integration * feat(auth): adding userpass and and ldap auth * chore(changelog): added support for userpass and ldap auth
1 parent c253c15 commit 1d767e3

6 files changed

Lines changed: 272 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## Unreleased
22

3+
Features:
4+
5+
* Added support for userpass and ldap authentication methods [GH-440](https://github.com/hashicorp/vault-action/pull/440)
6+
37
## 2.5.0 (Jan 26th, 2023)
48

59
Features:

action.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ inputs:
3636
description: 'The path to the Kubernetes service account secret'
3737
required: false
3838
default: '/var/run/secrets/kubernetes.io/serviceaccount/token'
39+
username:
40+
description: 'The username of the user to log in to Vault as. Available to both Userpass and LDAP auth methods'
41+
required: false
42+
password:
43+
description: 'The password of the user to log in to Vault as. Available to both Userpass and LDAP auth methods'
44+
required: false
3945
authPayload:
4046
description: 'The JSON payload to be sent to Vault when using a custom authentication method.'
4147
required: false
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
jest.mock('@actions/core');
2+
jest.mock('@actions/core/lib/command');
3+
const core = require('@actions/core');
4+
5+
const got = require('got');
6+
const { when } = require('jest-when');
7+
8+
const { exportSecrets } = require('../../src/action');
9+
10+
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
11+
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
12+
13+
describe('authenticate with approle', () => {
14+
let roleId;
15+
let secretId;
16+
beforeAll(async () => {
17+
try {
18+
// Verify Connection
19+
await got(`${vaultUrl}/v1/secret/config`, {
20+
headers: {
21+
'X-Vault-Token': vaultToken,
22+
},
23+
});
24+
25+
await got(`${vaultUrl}/v1/secret/data/approle-test`, {
26+
method: 'POST',
27+
headers: {
28+
'X-Vault-Token': vaultToken,
29+
},
30+
json: {
31+
data: {
32+
secret: 'SUPERSECRET_WITH_APPROLE',
33+
},
34+
},
35+
});
36+
37+
// Enable approle
38+
try {
39+
await got(`${vaultUrl}/v1/sys/auth/approle`, {
40+
method: 'POST',
41+
headers: {
42+
'X-Vault-Token': vaultToken
43+
},
44+
json: {
45+
type: 'approle'
46+
},
47+
});
48+
} catch (error) {
49+
const {response} = error;
50+
if (response.statusCode === 400 && response.body.includes("path is already in use")) {
51+
// Approle might already be enabled from previous test runs
52+
} else {
53+
throw error;
54+
}
55+
}
56+
57+
// Create policies
58+
await got(`${vaultUrl}/v1/sys/policies/acl/test`, {
59+
method: 'POST',
60+
headers: {
61+
'X-Vault-Token': vaultToken
62+
},
63+
json: {
64+
"name":"test",
65+
"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"
66+
},
67+
});
68+
69+
// Create approle
70+
await got(`${vaultUrl}/v1/auth/approle/role/my-role`, {
71+
method: 'POST',
72+
headers: {
73+
'X-Vault-Token': vaultToken
74+
},
75+
json: {
76+
policies: 'test'
77+
},
78+
});
79+
80+
// Get role-id
81+
const roldIdResponse = await got(`${vaultUrl}/v1/auth/approle/role/my-role/role-id`, {
82+
headers: {
83+
'X-Vault-Token': vaultToken
84+
},
85+
responseType: 'json',
86+
});
87+
roleId = roldIdResponse.body.data.role_id;
88+
89+
// Get secret-id
90+
const secretIdResponse = await got(`${vaultUrl}/v1/auth/approle/role/my-role/secret-id`, {
91+
method: 'POST',
92+
headers: {
93+
'X-Vault-Token': vaultToken
94+
},
95+
responseType: 'json',
96+
});
97+
secretId = secretIdResponse.body.data.secret_id;
98+
} catch(err) {
99+
console.warn('Create approle', err.response.body);
100+
throw err;
101+
}
102+
});
103+
104+
beforeEach(() => {
105+
jest.resetAllMocks();
106+
107+
when(core.getInput)
108+
.calledWith('method', expect.anything())
109+
.mockReturnValueOnce('approle');
110+
when(core.getInput)
111+
.calledWith('roleId', expect.anything())
112+
.mockReturnValueOnce(roleId);
113+
when(core.getInput)
114+
.calledWith('secretId', expect.anything())
115+
.mockReturnValueOnce(secretId);
116+
when(core.getInput)
117+
.calledWith('url', expect.anything())
118+
.mockReturnValueOnce(`${vaultUrl}`);
119+
});
120+
121+
function mockInput(secrets) {
122+
when(core.getInput)
123+
.calledWith('secrets', expect.anything())
124+
.mockReturnValueOnce(secrets);
125+
}
126+
127+
it('authenticate with approle', async() => {
128+
mockInput('secret/data/approle-test secret');
129+
130+
await exportSecrets();
131+
132+
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET_WITH_APPROLE');
133+
})
134+
});
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
jest.mock('@actions/core');
2+
jest.mock('@actions/core/lib/command');
3+
const core = require('@actions/core');
4+
5+
const got = require('got');
6+
const { when } = require('jest-when');
7+
8+
const { exportSecrets } = require('../../src/action');
9+
10+
const vaultUrl = `http://${process.env.VAULT_HOST || 'localhost'}:${process.env.VAULT_PORT || '8200'}`;
11+
const vaultToken = `${process.env.VAULT_TOKEN || 'testtoken'}`
12+
13+
describe('authenticate with userpass', () => {
14+
const username = `testUsername`;
15+
const password = `testPassword`;
16+
beforeAll(async () => {
17+
try {
18+
// Verify Connection
19+
await got(`${vaultUrl}/v1/secret/config`, {
20+
headers: {
21+
'X-Vault-Token': vaultToken,
22+
},
23+
});
24+
25+
await got(`${vaultUrl}/v1/secret/data/userpass-test`, {
26+
method: 'POST',
27+
headers: {
28+
'X-Vault-Token': vaultToken,
29+
},
30+
json: {
31+
data: {
32+
secret: 'SUPERSECRET_WITH_USERPASS',
33+
},
34+
},
35+
});
36+
37+
// Enable userpass
38+
try {
39+
await got(`${vaultUrl}/v1/sys/auth/userpass`, {
40+
method: 'POST',
41+
headers: {
42+
'X-Vault-Token': vaultToken
43+
},
44+
json: {
45+
type: 'userpass'
46+
},
47+
});
48+
} catch (error) {
49+
const {response} = error;
50+
if (response.statusCode === 400 && response.body.includes("path is already in use")) {
51+
// Userpass might already be enabled from previous test runs
52+
} else {
53+
throw error;
54+
}
55+
}
56+
57+
// Create policies
58+
await got(`${vaultUrl}/v1/sys/policies/acl/userpass-test`, {
59+
method: 'POST',
60+
headers: {
61+
'X-Vault-Token': vaultToken
62+
},
63+
json: {
64+
"name":"userpass-test",
65+
"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`
66+
},
67+
});
68+
69+
// Create user
70+
await got(`${vaultUrl}/v1/auth/userpass/users/${username}`, {
71+
method: 'POST',
72+
headers: {
73+
'X-Vault-Token': vaultToken
74+
},
75+
json: {
76+
password: `${password}`,
77+
policies: 'userpass-test'
78+
},
79+
});
80+
} catch(err) {
81+
console.warn('Create user in userpass', err.response.body);
82+
throw err;
83+
}
84+
});
85+
86+
beforeEach(() => {
87+
jest.resetAllMocks();
88+
89+
when(core.getInput)
90+
.calledWith('method', expect.anything())
91+
.mockReturnValueOnce('userpass');
92+
when(core.getInput)
93+
.calledWith('username', expect.anything())
94+
.mockReturnValueOnce(username);
95+
when(core.getInput)
96+
.calledWith('password', expect.anything())
97+
.mockReturnValueOnce(password);
98+
when(core.getInput)
99+
.calledWith('url', expect.anything())
100+
.mockReturnValueOnce(`${vaultUrl}`);
101+
});
102+
103+
function mockInput(secrets) {
104+
when(core.getInput)
105+
.calledWith('secrets', expect.anything())
106+
.mockReturnValueOnce(secrets);
107+
}
108+
109+
it('authenticate with userpass', async() => {
110+
mockInput('secret/data/userpass-test secret');
111+
112+
await exportSecrets();
113+
114+
expect(core.exportVariable).toBeCalledWith('SECRET', 'SUPERSECRET_WITH_USERPASS');
115+
})
116+
});

src/action.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const got = require('got').default;
55
const jsonata = require('jsonata');
66
const { auth: { retrieveToken }, secrets: { getSecrets } } = require('./index');
77

8-
const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes'];
8+
const AUTH_METHODS = ['approle', 'token', 'github', 'jwt', 'kubernetes', 'ldap', 'userpass'];
99
const ENCODING_TYPES = ['base64', 'hex', 'utf8'];
1010

1111
async function exportSecrets() {

src/auth.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ const defaultKubernetesTokenPath = '/var/run/secrets/kubernetes.io/serviceaccoun
1111
* @param {import('got').Got} client
1212
*/
1313
async function retrieveToken(method, client) {
14-
const path = core.getInput('path', { required: false }) || method;
14+
let path = core.getInput('path', { required: false }) || method;
15+
path = `v1/auth/${path}/login`
1516

1617
switch (method) {
1718
case 'approle': {
@@ -50,6 +51,13 @@ async function retrieveToken(method, client) {
5051
}
5152
return await getClientToken(client, method, path, { jwt: data, role: role })
5253
}
54+
case 'userpass':
55+
case 'ldap': {
56+
const username = core.getInput('username', { required: true });
57+
const password = core.getInput('password', { required: true });
58+
path = path + `/${username}`
59+
return await getClientToken(client, method, path, { password: password })
60+
}
5361

5462
default: {
5563
if (!method || method === 'token') {
@@ -107,12 +115,12 @@ async function getClientToken(client, method, path, payload) {
107115
responseType,
108116
};
109117

110-
core.debug(`Retrieving Vault Token from v1/auth/${path}/login endpoint`);
118+
core.debug(`Retrieving Vault Token from ${path} endpoint`);
111119

112120
/** @type {import('got').Response<VaultLoginResponse>} */
113121
let response;
114122
try {
115-
response = await client.post(`v1/auth/${path}/login`, options);
123+
response = await client.post(`${path}`, options);
116124
} catch (err) {
117125
if (err instanceof got.HTTPError) {
118126
throw Error(`failed to retrieve vault token. code: ${err.code}, message: ${err.message}, vaultResponse: ${JSON.stringify(err.response.body)}`)

0 commit comments

Comments
 (0)