Skip to content

Commit 3ff74a3

Browse files
committed
fix secrets stored in JSON format
1 parent e926631 commit 3ff74a3

8 files changed

Lines changed: 236 additions & 16 deletions

File tree

.github/workflows/local-test.yaml

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,32 +30,57 @@ jobs:
3030
- name: NPM Build
3131
run: npm run build
3232

33-
- name: Setup Vault
34-
run: node ./integrationTests/e2e/setup.js
35-
env:
36-
VAULT_HOST: localhost
37-
VAULT_PORT: 8200
33+
# - name: Setup Vault
34+
# run: node ./integrationTests/e2e/setup.js
35+
# env:
36+
# VAULT_HOST: localhost
37+
# VAULT_PORT: 8200
3838

3939
- name: Import Secrets
4040
id: import-secrets
4141
# use the local changes
4242
uses: ./
4343
# run against a specific version of vault-action
44-
# uses: hashicorp/vault-action@v2.1.2
44+
# uses: hashicorp/vault-action@v2.6.0
45+
# uses: hashicorp/vault-action@v2.1.1
4546
with:
4647
url: http://localhost:8200
4748
method: token
4849
token: testtoken
50+
# secret/data/test-json-string jsonString;
51+
# secret/data/test-json-big jsonBig;
52+
# secret/data/test-json-string-big jsonStringBig;
4953
secrets: |
5054
secret/data/test-json-string jsonString;
55+
secret/data/test-json-string-multiline jsonStringMultiline;
56+
secret/data/test-json-data jsonData;
57+
secret/data/singleline singleline;
58+
secret/data/multiline multiline;
5159
5260
- name: Check Secrets
5361
run: |
54-
touch secrets.json
55-
echo "${{ steps.import-secrets.outputs.jsonString }}" >> secrets.json
62+
echo "test-json-string"
63+
echo "${{ steps.import-secrets.outputs.jsonString }}"
64+
echo
65+
echo "test-json-string-multiline"
66+
echo "${{ steps.import-secrets.outputs.jsonStringMultiline }}"
67+
echo
68+
echo "test-json-data"
69+
echo "${{ steps.import-secrets.outputs.jsonData }}"
70+
echo
71+
echo "singleline"
72+
echo "${{ steps.import-secrets.outputs.singleline }}"
73+
echo
74+
echo "multiline"
75+
echo "${{ steps.import-secrets.outputs.multiline }}"
76+
echo
5677
57-
- name: Check json file format
78+
- name: test parse
5879
run: |
59-
echo
60-
cat secrets.json
61-
jq -c . < secrets.json
80+
node ./scripts/parse.js
81+
82+
- name: Check jq
83+
run: |
84+
echo "test-json-data"
85+
echo "${{ steps.import-secrets.outputs.jsonData }}" | jq -c . || true
86+

.github/workflows/test.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
name: 'test'
2+
inputs:
3+
json:
4+
required: true
5+
outputs:
6+
out:
7+
description: 'The time we greeted you'
8+
runs:
9+
using: 'node16'
10+
main: './scripts/parse.js'

integrationTests/e2e/setup.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const got = require('got');
33
const vaultUrl = `${process.env.VAULT_HOST}:${process.env.VAULT_PORT}`;
44
const vaultToken = `${process.env.VAULT_TOKEN}` === undefined ? `${process.env.VAULT_TOKEN}` : "testtoken";
55

6+
const jsonStringMultiline = '{"x": 1, "y": "q\\nux"}';
7+
68
(async () => {
79
try {
810
// Verify Connection
@@ -36,6 +38,44 @@ const vaultToken = `${process.env.VAULT_TOKEN}` === undefined ? `${process.env.V
3638
}
3739
});
3840

41+
await got(`http://${vaultUrl}/v1/secret/data/test-json-string`, {
42+
method: 'POST',
43+
headers: {
44+
'X-Vault-Token': vaultToken,
45+
},
46+
json: {
47+
data: {
48+
// this is stored in Vault as a string
49+
jsonString: '{"x":1,"y":"qux"}',
50+
},
51+
},
52+
});
53+
54+
await got(`http://${vaultUrl}/v1/secret/data/test-json-data`, {
55+
method: 'POST',
56+
headers: {
57+
'X-Vault-Token': vaultToken,
58+
},
59+
json: {
60+
data: {
61+
// this is stored in Vault as a map
62+
jsonData: {"x":1,"y":"qux"},
63+
},
64+
},
65+
});
66+
67+
await got(`http://${vaultUrl}/v1/secret/data/test-json-string-multiline`, {
68+
method: 'POST',
69+
headers: {
70+
'X-Vault-Token': vaultToken,
71+
},
72+
json: {
73+
data: {
74+
jsonStringMultiline,
75+
},
76+
},
77+
});
78+
3979
await got(`http://${vaultUrl}/v1/sys/mounts/my-secret`, {
4080
method: 'POST',
4181
headers: {

integrationTests/e2e/testdata.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"x": 1,
3+
"y": "q\nux"
4+
}

scripts/parse.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// const core = require('@actions/core');
2+
3+
try {
4+
let inputs = [
5+
process.env.JSONSTRING,
6+
process.env.JSONSTRINGMULTILINE,
7+
process.env.JSONDATA,
8+
process.env.SINGLELINE,
9+
process.env.MULTILINE,
10+
];
11+
12+
let names = [
13+
"test-json-string",
14+
"test-json-string-multiline",
15+
"test-json-data",
16+
"singleline",
17+
"multiline",
18+
];
19+
20+
let i = 0;
21+
inputs.forEach(input => {
22+
console.log(`processing: ${names[i]}`)
23+
i++;
24+
input = (input || '').trim();
25+
if (!input) {
26+
throw new Error(`Missing service account key JSON (got empty value)`);
27+
}
28+
29+
// If the string doesn't start with a JSON object character, it is probably
30+
// base64-encoded.
31+
if (!input.startsWith('{')) {
32+
let str = input.replace(/-/g, '+').replace(/_/g, '/');
33+
while (str.length % 4) str += '=';
34+
input = Buffer.from(str, 'base64').toString('utf8');
35+
}
36+
37+
try {
38+
const creds = JSON.parse(input);
39+
console.log('success!')
40+
return creds;
41+
} catch (err) {
42+
console.log('error parsing')
43+
console.log(err)
44+
}
45+
})
46+
47+
} catch (error) {
48+
console.log(error)
49+
}
50+

src/action.test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,38 @@ describe('exportSecrets', () => {
220220
expect(core.setOutput).toBeCalledWith('key', '1');
221221
});
222222

223+
it('json secret retrieval', async () => {
224+
const jsonString = '{"x":1,"y":2}';
225+
226+
mockInput('test key');
227+
mockVaultData({
228+
key: jsonString,
229+
});
230+
231+
await exportSecrets();
232+
233+
expect(core.exportVariable).toBeCalledWith('KEY', jsonString);
234+
expect(core.setOutput).toBeCalledWith('key', jsonString);
235+
});
236+
237+
it('multi-line json secret retrieval', async () => {
238+
const jsonString = `
239+
{
240+
"x":1,
241+
"y":"bar"
242+
}
243+
`;
244+
mockInput('test key');
245+
mockVaultData({
246+
key: jsonString,
247+
});
248+
249+
await exportSecrets();
250+
251+
expect(core.exportVariable).toBeCalledWith('KEY', jsonString);
252+
expect(core.setOutput).toBeCalledWith('key', jsonString);
253+
});
254+
223255
it('intl secret retrieval', async () => {
224256
mockInput('测试 测试');
225257
mockVaultData({

src/retries.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,4 @@ describe('exportSecrets retries', () => {
6666
done();
6767
});
6868
});
69-
});
69+
});

src/secrets.js

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,21 @@ async function getSecrets(secretRequests, client) {
7272
*/
7373
async function selectData(data, selector) {
7474
const ata = jsonata(selector);
75-
let result = JSON.stringify(await ata.evaluate(data));
75+
let d = await ata.evaluate(data);
76+
77+
// If we have a Javascript Object, then this data was stored in Vault as
78+
// pure JSON (not a JSON string)
79+
const storedAsJSONData = isObject(d);
80+
81+
if (isJSONString(d)) {
82+
// If we already have a JSON string we will not "stringify" it yet so
83+
// that we don't end up calling JSON.parse. This would break the
84+
// secrets that are stored as pure JSON. See: https://github.com/hashicorp/vault-action/issues/194
85+
result = d;
86+
} else {
87+
result = JSON.stringify(d);
88+
}
89+
7690
// Compat for custom engines
7791
if (!result && ((ata.ast().type === "path" && ata.ast()['steps'].length === 1) || ata.ast().type === "string") && selector !== 'data' && 'data' in data) {
7892
result = JSON.stringify(await jsonata(`data.${selector}`).evaluate(data));
@@ -81,12 +95,57 @@ async function selectData(data, selector) {
8195
}
8296

8397
if (result.startsWith(`"`)) {
84-
result = JSON.parse(result);
98+
// we need to strip the beginning and ending quotes otherwise it will
99+
// always successfully parse as a JSON string
100+
result = result.substring(1, result.length - 1);
101+
if (!isJSONString(result)) {
102+
// add the quotes back so we can parse it into a Javascript object
103+
// to allow support for multi-line secrets. See https://github.com/hashicorp/vault-action/issues/160
104+
result = `"${result}"`
105+
result = JSON.parse(result);
106+
}
107+
} else if (isJSONString(result)) {
108+
if (storedAsJSONData) {
109+
// Support secrets stored in Vault as pure JSON.
110+
// See https://github.com/hashicorp/vault-action/issues/194 and https://github.com/hashicorp/vault-action/pull/173
111+
result = JSON.stringify(result);
112+
result = result.substring(1, result.length - 1);
113+
} else {
114+
// Support secrets stored in Vault as JSON Strings
115+
result = JSON.stringify(result);
116+
result = JSON.parse(result);
117+
}
85118
}
86119
return result;
87120
}
88121

122+
/**
123+
* isOjbect returns true if target is a Javascript object
124+
* @param {Type} target
125+
*/
126+
function isObject(target) {
127+
return typeof target === 'object' && target !== null;
128+
}
129+
130+
/**
131+
* isJSONString returns true if target parses as a valid JSON string
132+
* @param {Type} target
133+
*/
134+
function isJSONString(target) {
135+
if (typeof target !== "string"){
136+
return false;
137+
}
138+
139+
try {
140+
let o = JSON.parse(target);
141+
} catch (e) {
142+
return false;
143+
}
144+
145+
return true;
146+
}
147+
89148
module.exports = {
90149
getSecrets,
91150
selectData
92-
}
151+
}

0 commit comments

Comments
 (0)