ac-awssecrets 2.5.7 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CODEOWNERS +9 -0
- package/.github/workflows/node.js.yml +5 -2
- package/CHANGELOG.md +47 -0
- package/Makefile +2 -2
- package/SECURITY.md +22 -0
- package/eslint.config.js +42 -26
- package/index.js +183 -230
- package/package.json +11 -7
- package/test/config.js +58 -156
- package/test/test.js +417 -86
package/test/test.js
CHANGED
|
@@ -1,158 +1,489 @@
|
|
|
1
1
|
const { expect } = require('chai')
|
|
2
|
+
const { mockClient } = require('aws-sdk-client-mock')
|
|
3
|
+
const { SSMClient, GetParametersCommand, GetParameterCommand, GetParametersByPathCommand } = require('@aws-sdk/client-ssm')
|
|
4
|
+
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager')
|
|
2
5
|
|
|
6
|
+
const awsSecrets = require('../index')
|
|
7
|
+
const fixture = require('./config')
|
|
3
8
|
|
|
4
|
-
|
|
9
|
+
const ssmMock = mockClient(SSMClient)
|
|
10
|
+
const secretsMock = mockClient(SecretsManagerClient)
|
|
5
11
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const multisecrets = testConfig?.multisecrets
|
|
9
|
-
const config = testConfig.config
|
|
10
|
-
let secrets = testConfig.secrets
|
|
11
|
-
|
|
12
|
-
const parameterStore = testConfig.parameterStore
|
|
13
|
-
let secretParameters = testConfig.secretParameters
|
|
14
|
-
|
|
15
|
-
// HELPER for console.log checks
|
|
12
|
+
// Helper to capture stderr output
|
|
16
13
|
const captureStream = (stream) => {
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const oldWrite = stream.write
|
|
15
|
+
let buf = ''
|
|
19
16
|
stream.write = (chunk) => {
|
|
20
17
|
buf += chunk.toString()
|
|
21
18
|
oldWrite.apply(stream, arguments)
|
|
22
19
|
}
|
|
23
|
-
|
|
24
20
|
return {
|
|
25
|
-
unhook: () => {
|
|
26
|
-
|
|
27
|
-
},
|
|
28
|
-
captured: () => {
|
|
29
|
-
return buf
|
|
30
|
-
}
|
|
21
|
+
unhook: () => { stream.write = oldWrite },
|
|
22
|
+
captured: () => buf
|
|
31
23
|
}
|
|
32
24
|
}
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
// Build SSM mock responses from fixture parameterStore
|
|
27
|
+
const buildSsmMocks = () => {
|
|
28
|
+
// GetParametersCommand returns batches by name - matches any environment prefix
|
|
29
|
+
ssmMock.on(GetParametersCommand).callsFake((input) => {
|
|
30
|
+
const Parameters = input.Names
|
|
31
|
+
.map(name => {
|
|
32
|
+
// Match by exact name or by stripping the environment prefix
|
|
33
|
+
const found = fixture.parameterStore.find(p => p.name === name) ||
|
|
34
|
+
fixture.parameterStore.find(p => name.endsWith(p.name.replace(/^\/test/, '')))
|
|
35
|
+
if (!found) return null
|
|
36
|
+
return { Name: name, Value: found.value, Type: 'SecureString' }
|
|
37
|
+
})
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
|
|
40
|
+
const InvalidParameters = input.Names.filter(name => {
|
|
41
|
+
const found = fixture.parameterStore.find(p => p.name === name) ||
|
|
42
|
+
fixture.parameterStore.find(p => name.endsWith(p.name.replace(/^\/test/, '')))
|
|
43
|
+
return !found
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return { Parameters, InvalidParameters }
|
|
37
47
|
})
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
// GetParametersByPathCommand supports pagination via NextToken
|
|
50
|
+
ssmMock.on(GetParametersByPathCommand).callsFake((input) => {
|
|
51
|
+
const Parameters = fixture.parameterStore
|
|
52
|
+
.filter(p => p.name.startsWith(input.Path))
|
|
53
|
+
.map(p => ({ Name: p.name, Value: p.value, Type: 'SecureString' }))
|
|
54
|
+
|
|
55
|
+
return { Parameters, NextToken: undefined }
|
|
56
|
+
})
|
|
45
57
|
|
|
58
|
+
// GetParameterCommand (fallback for individual fetches)
|
|
59
|
+
ssmMock.on(GetParameterCommand).callsFake((input) => {
|
|
60
|
+
const found = fixture.parameterStore.find(p => p.name === input.Name)
|
|
61
|
+
if (!found) throw new Error(`ParameterNotFound: ${input.Name}`)
|
|
62
|
+
return { Parameter: { Name: found.name, Value: found.value, Type: 'SecureString' } }
|
|
46
63
|
})
|
|
64
|
+
}
|
|
47
65
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
// Build Secrets Manager mock responses from fixture availableSecrets
|
|
67
|
+
const buildSecretsMocks = () => {
|
|
68
|
+
secretsMock.on(GetSecretValueCommand).callsFake((input) => {
|
|
69
|
+
// Strip 'test.' prefix to find the fixture entry
|
|
70
|
+
const name = input.SecretId.replace(/^test\./, '')
|
|
71
|
+
const found = fixture.availableSecrets.find(s => s.name === name)
|
|
72
|
+
if (!found) throw new Error(`ResourceNotFoundException: ${input.SecretId}`)
|
|
73
|
+
return { SecretString: JSON.stringify(found.value) }
|
|
52
74
|
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── secretParameters (SSM) ─────────────────────────────────────────────────
|
|
53
78
|
|
|
54
|
-
|
|
79
|
+
describe('loadSecretParameters', () => {
|
|
80
|
+
let config
|
|
81
|
+
|
|
82
|
+
before(() => {
|
|
83
|
+
ssmMock.reset()
|
|
84
|
+
buildSsmMocks()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
// Fresh config for each test
|
|
89
|
+
config = {
|
|
90
|
+
environment: 'test',
|
|
91
|
+
configVar1: { c1: true },
|
|
92
|
+
configVar2: { servers: [{ server: 'main', port: 3000 }] },
|
|
93
|
+
configVar5: { path: {} },
|
|
94
|
+
configVar7: { level: 'info' },
|
|
95
|
+
aws: { account: '123', accessKeys: [] }
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('loads and parses JSON parameters', async() => {
|
|
100
|
+
await awsSecrets.loadSecretParameters({
|
|
101
|
+
secretParameters: [{ name: 'configVar1', json: true }],
|
|
102
|
+
config
|
|
103
|
+
})
|
|
104
|
+
const expected = JSON.parse(fixture.parameterStore.find(p => p.name === '/test/configVar1').value)
|
|
105
|
+
expect(config.configVar1).to.deep.equal(expected)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('merges nested object parameters', async() => {
|
|
109
|
+
await awsSecrets.loadSecretParameters({
|
|
110
|
+
secretParameters: [{ name: 'configVar1', json: true, merge: true }],
|
|
111
|
+
config
|
|
112
|
+
})
|
|
113
|
+
expect(config.configVar1).to.have.property('c1', true) // original value preserved
|
|
114
|
+
expect(config.configVar1).to.have.property('c2')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('sets a parameter at a specific path', async() => {
|
|
118
|
+
await awsSecrets.loadSecretParameters({
|
|
119
|
+
secretParameters: [{ name: 'configVar4/api/url', path: 'configVar4.api.url' }],
|
|
120
|
+
config
|
|
121
|
+
})
|
|
55
122
|
expect(config.configVar4.api).to.have.property('url', 'https://api.admiralcloud.com')
|
|
56
123
|
})
|
|
57
124
|
|
|
58
|
-
it('
|
|
59
|
-
|
|
125
|
+
it('sets a nested path parameter', async() => {
|
|
126
|
+
await awsSecrets.loadSecretParameters({
|
|
127
|
+
secretParameters: [{ name: 'configVar5/path/cookie', path: 'configVar5.path.cookie' }],
|
|
128
|
+
config
|
|
129
|
+
})
|
|
130
|
+
expect(config.configVar5.path).to.have.property('cookie', 'true')
|
|
60
131
|
})
|
|
61
132
|
|
|
62
|
-
it('
|
|
63
|
-
|
|
133
|
+
it('creates missing config keys on the fly', async() => {
|
|
134
|
+
await awsSecrets.loadSecretParameters({
|
|
135
|
+
secretParameters: [
|
|
136
|
+
{ name: 'configVar6/prop1', path: 'configVar6.prop1' },
|
|
137
|
+
{ name: 'configVar6/prop2', path: 'configVar6.prop2' }
|
|
138
|
+
],
|
|
139
|
+
config
|
|
140
|
+
})
|
|
141
|
+
expect(config.configVar6).to.have.property('prop1', '123')
|
|
64
142
|
expect(config.configVar6).to.have.property('prop2', 'abc')
|
|
65
143
|
})
|
|
66
144
|
|
|
67
|
-
it('
|
|
145
|
+
it('silently skips non-existing parameters without error', async() => {
|
|
146
|
+
await awsSecrets.loadSecretParameters({
|
|
147
|
+
secretParameters: [{ name: 'configVar7/nonExisting', path: 'configVar7.level' }],
|
|
148
|
+
config
|
|
149
|
+
})
|
|
150
|
+
// Value was not found in SSM, so original value should be untouched
|
|
68
151
|
expect(config.configVar7).to.have.property('level', 'info')
|
|
69
152
|
})
|
|
70
|
-
|
|
71
153
|
|
|
72
|
-
it('
|
|
154
|
+
it('merges top-level JSON parameter', async() => {
|
|
155
|
+
await awsSecrets.loadSecretParameters({
|
|
156
|
+
secretParameters: [{ name: 'aws', json: true, merge: true }],
|
|
157
|
+
config
|
|
158
|
+
})
|
|
73
159
|
expect(config.aws).to.have.property('account', '456')
|
|
74
|
-
expect(config.aws).to.have.property('accessKeys').length(0)
|
|
160
|
+
expect(config.aws).to.have.property('accessKeys').with.length(0)
|
|
75
161
|
})
|
|
76
162
|
|
|
77
|
-
it('
|
|
163
|
+
it('loads wildcard path parameters as array', async() => {
|
|
164
|
+
await awsSecrets.loadSecretParameters({
|
|
165
|
+
secretParameters: [{ name: 'db/*', path: 'db', array: true, json: true }],
|
|
166
|
+
config
|
|
167
|
+
})
|
|
78
168
|
expect(config.db).to.have.length(2)
|
|
79
169
|
expect(config.db[0]).to.have.property('url', 'https://db1.admiralcloud.com')
|
|
80
170
|
expect(config.db[1]).to.have.property('url', 'https://db2.admiralcloud.com')
|
|
81
171
|
})
|
|
172
|
+
|
|
173
|
+
it('handles batches of more than 10 parameters', async() => {
|
|
174
|
+
// Create 12 parameters to force batching
|
|
175
|
+
const manyParams = Array.from({ length: 12 }, (_, i) => ({
|
|
176
|
+
name: `configVar6/prop${i + 1}`,
|
|
177
|
+
path: `batchTest.prop${i + 1}`
|
|
178
|
+
}))
|
|
179
|
+
// Should not throw even though most params won't exist
|
|
180
|
+
await awsSecrets.loadSecretParameters({ secretParameters: manyParams, config })
|
|
181
|
+
expect(config).to.exist
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('throws on invalid parameters when throwError is set', async() => {
|
|
185
|
+
ssmMock.reset()
|
|
186
|
+
// Mock a batch where all params are invalid
|
|
187
|
+
ssmMock.on(GetParametersCommand).resolves({
|
|
188
|
+
Parameters: [],
|
|
189
|
+
InvalidParameters: ['/test/nonExistent']
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
await awsSecrets.loadSecretParameters({
|
|
194
|
+
secretParameters: [{ name: 'nonExistent' }],
|
|
195
|
+
config,
|
|
196
|
+
throwError: true
|
|
197
|
+
})
|
|
198
|
+
expect.fail('Should have thrown')
|
|
199
|
+
}
|
|
200
|
+
catch(e) {
|
|
201
|
+
expect(e.message).to.include('Invalid parameters')
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
ssmMock.reset()
|
|
205
|
+
buildSsmMocks()
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('falls back to GetParameterCommand when batch fails', async() => {
|
|
210
|
+
ssmMock.reset()
|
|
211
|
+
// Make batch command fail
|
|
212
|
+
ssmMock.on(GetParametersCommand).rejects(new Error('BatchFailed'))
|
|
213
|
+
// Individual fallback succeeds
|
|
214
|
+
ssmMock.on(GetParameterCommand).callsFake((input) => {
|
|
215
|
+
const found = fixture.parameterStore.find(p => p.name === input.Name)
|
|
216
|
+
if (!found) throw new Error(`ParameterNotFound: ${input.Name}`)
|
|
217
|
+
return { Parameter: { Name: found.name, Value: found.value, Type: 'SecureString' } }
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
await awsSecrets.loadSecretParameters({
|
|
221
|
+
secretParameters: [{ name: 'configVar1', json: true }],
|
|
222
|
+
config
|
|
223
|
+
})
|
|
224
|
+
const expected = JSON.parse(fixture.parameterStore.find(p => p.name === '/test/configVar1').value)
|
|
225
|
+
expect(config.configVar1).to.deep.equal(expected)
|
|
226
|
+
|
|
227
|
+
ssmMock.reset()
|
|
228
|
+
buildSsmMocks()
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('skips parameters with ignoreInTestMode in test environment', async() => {
|
|
232
|
+
await awsSecrets.loadSecretParameters({
|
|
233
|
+
secretParameters: [
|
|
234
|
+
{ name: 'configVar1', json: true },
|
|
235
|
+
{ name: 'configVar6/prop1', path: 'configVar6.prop1', ignoreInTestMode: true }
|
|
236
|
+
],
|
|
237
|
+
config
|
|
238
|
+
})
|
|
239
|
+
expect(config.configVar6).to.be.undefined
|
|
240
|
+
expect(config.configVar1).to.have.property('c2')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('outputs debug logs when debug is true', async() => {
|
|
244
|
+
const hook = captureStream(process.stderr)
|
|
245
|
+
await awsSecrets.loadSecretParameters({
|
|
246
|
+
secretParameters: [{ name: 'configVar1', json: true }],
|
|
247
|
+
config,
|
|
248
|
+
debug: true
|
|
249
|
+
})
|
|
250
|
+
hook.unhook()
|
|
251
|
+
expect(hook.captured()).to.include('/test/configVar1')
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('handles multiple wildcards in parameter name via replaceAll', async() => {
|
|
255
|
+
// Should not throw and should strip all * correctly
|
|
256
|
+
await awsSecrets.loadSecretParameters({
|
|
257
|
+
secretParameters: [{ name: 'db/**', path: 'db', array: true, json: true }],
|
|
258
|
+
config
|
|
259
|
+
})
|
|
260
|
+
expect(config).to.exist
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('throws when path is missing on wildcard parameter', async() => {
|
|
264
|
+
try {
|
|
265
|
+
await awsSecrets.loadSecretParameters({
|
|
266
|
+
secretParameters: [{ name: 'db/*' }], // no path!
|
|
267
|
+
config
|
|
268
|
+
})
|
|
269
|
+
expect.fail('Should have thrown')
|
|
270
|
+
}
|
|
271
|
+
catch(e) {
|
|
272
|
+
expect(e.message).to.equal('pathMustBeSet')
|
|
273
|
+
}
|
|
274
|
+
})
|
|
82
275
|
})
|
|
83
276
|
|
|
277
|
+
// ─── Prototype Pollution ────────────────────────────────────────────────────
|
|
84
278
|
|
|
85
|
-
describe('
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
279
|
+
describe('Prototype Pollution Protection', () => {
|
|
280
|
+
let testConfig
|
|
281
|
+
|
|
282
|
+
before(() => {
|
|
283
|
+
ssmMock.reset()
|
|
284
|
+
buildSsmMocks()
|
|
89
285
|
})
|
|
90
286
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
expect(config.configVar1).to.have.property('c3', expected.value.c3)
|
|
287
|
+
beforeEach(() => {
|
|
288
|
+
// Include environment so the SSM mock can match fixture parameters
|
|
289
|
+
testConfig = { environment: 'test' }
|
|
290
|
+
})
|
|
96
291
|
|
|
292
|
+
it('rejects __proto__ in secretParameters path', async() => {
|
|
293
|
+
try {
|
|
294
|
+
await awsSecrets.loadSecretParameters({
|
|
295
|
+
secretParameters: [{ name: 'configVar1', path: '__proto__.polluted', json: false }],
|
|
296
|
+
config: testConfig
|
|
297
|
+
})
|
|
298
|
+
expect.fail('Should have thrown')
|
|
299
|
+
}
|
|
300
|
+
catch(e) {
|
|
301
|
+
expect(e.message).to.include('unsafe key segment')
|
|
302
|
+
}
|
|
303
|
+
expect(({}).polluted).to.be.undefined
|
|
97
304
|
})
|
|
98
305
|
|
|
99
|
-
it('
|
|
100
|
-
|
|
101
|
-
|
|
306
|
+
it('rejects constructor in secretParameters path', async() => {
|
|
307
|
+
try {
|
|
308
|
+
await awsSecrets.loadSecretParameters({
|
|
309
|
+
secretParameters: [{ name: 'configVar1', path: 'constructor.polluted' }],
|
|
310
|
+
config: testConfig
|
|
311
|
+
})
|
|
312
|
+
expect.fail('Should have thrown')
|
|
313
|
+
}
|
|
314
|
+
catch(e) {
|
|
315
|
+
expect(e.message).to.include('unsafe key segment')
|
|
316
|
+
}
|
|
102
317
|
})
|
|
103
318
|
|
|
104
|
-
it('
|
|
319
|
+
it('rejects prototype in secretParameters path', async() => {
|
|
320
|
+
try {
|
|
321
|
+
await awsSecrets.loadSecretParameters({
|
|
322
|
+
secretParameters: [{ name: 'configVar1', path: 'prototype.polluted' }],
|
|
323
|
+
config: testConfig
|
|
324
|
+
})
|
|
325
|
+
expect.fail('Should have thrown')
|
|
326
|
+
}
|
|
327
|
+
catch(e) {
|
|
328
|
+
expect(e.message).to.include('unsafe key segment')
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('rejects __proto__ in nested path', async() => {
|
|
333
|
+
try {
|
|
334
|
+
await awsSecrets.loadSecretParameters({
|
|
335
|
+
secretParameters: [{ name: 'configVar1', path: 'safe.__proto__.polluted' }],
|
|
336
|
+
config: testConfig
|
|
337
|
+
})
|
|
338
|
+
expect.fail('Should have thrown')
|
|
339
|
+
}
|
|
340
|
+
catch(e) {
|
|
341
|
+
expect(e.message).to.include('unsafe key segment')
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('allows paths that contain "proto" as substring', async() => {
|
|
346
|
+
await awsSecrets.loadSecretParameters({
|
|
347
|
+
secretParameters: [{ name: 'configVar4/api/url', path: 'protocol.settings' }],
|
|
348
|
+
config: testConfig
|
|
349
|
+
})
|
|
350
|
+
// protocol is a safe key and should be set
|
|
351
|
+
expect(testConfig.protocol).to.exist
|
|
352
|
+
expect(testConfig.protocol.settings).to.equal('https://api.admiralcloud.com')
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
// ─── loadSecrets (Secrets Manager) ─────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
describe('loadSecrets', () => {
|
|
359
|
+
let config
|
|
360
|
+
|
|
361
|
+
before(() => {
|
|
362
|
+
secretsMock.reset()
|
|
363
|
+
buildSecretsMocks()
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
beforeEach(() => {
|
|
367
|
+
config = {
|
|
368
|
+
environment: 'test',
|
|
369
|
+
configVar1: { c1: true },
|
|
370
|
+
configVar2: { servers: [{ server: 'main', port: 3000 }] },
|
|
371
|
+
configVar5: { path: {} },
|
|
372
|
+
configVar7: { level: 'info' },
|
|
373
|
+
aws: { account: '123', accessKeys: [] }
|
|
374
|
+
}
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('loads and merges a secret into config', async() => {
|
|
378
|
+
await awsSecrets.loadSecrets({
|
|
379
|
+
secrets: [{ name: 'configVar1', key: 'configVar1' }],
|
|
380
|
+
config
|
|
381
|
+
})
|
|
382
|
+
expect(config.configVar1).to.have.property('c1', true)
|
|
383
|
+
expect(config.configVar1).to.have.property('c2', 'secretValue2')
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('updates server config via servers flag', async() => {
|
|
387
|
+
await awsSecrets.loadSecrets({
|
|
388
|
+
secrets: [{ name: 'configVar2', key: 'configVar2', servers: true, serverName: 'main' }],
|
|
389
|
+
config
|
|
390
|
+
})
|
|
391
|
+
expect(config.configVar2.servers[0]).to.have.property('port', 9999)
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('parses JSON: prefixed values', async() => {
|
|
395
|
+
await awsSecrets.loadSecrets({
|
|
396
|
+
secrets: [{ name: 'configVar4', key: 'configVar4' }],
|
|
397
|
+
config
|
|
398
|
+
})
|
|
105
399
|
expect(config.configVar4.api).to.have.property('url', 'https://api.admiralcloud.com')
|
|
106
400
|
})
|
|
107
401
|
|
|
108
|
-
it('
|
|
109
|
-
|
|
402
|
+
it('converts string "true"/"false" to booleans', async() => {
|
|
403
|
+
await awsSecrets.loadSecretParameters({
|
|
404
|
+
secretParameters: [{ name: 'configVar5/path/cookie', path: 'configVar5.path.cookie' }],
|
|
405
|
+
config
|
|
406
|
+
})
|
|
407
|
+
// Value from SSM is the string 'true', not a boolean
|
|
408
|
+
expect(config.configVar5.path.cookie).to.equal('true')
|
|
110
409
|
})
|
|
111
410
|
|
|
112
|
-
it('
|
|
411
|
+
it('creates non-existing config keys', async() => {
|
|
412
|
+
await awsSecrets.loadSecrets({
|
|
413
|
+
secrets: [{ name: 'configVar6', key: 'configVar6' }],
|
|
414
|
+
config
|
|
415
|
+
})
|
|
113
416
|
expect(config.configVar6).to.have.property('prop1', 123)
|
|
114
417
|
expect(config.configVar6).to.have.property('prop2', 'abc')
|
|
115
418
|
})
|
|
116
419
|
|
|
117
|
-
it('
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
420
|
+
it('expands multisecrets and loads each as array entry', async() => {
|
|
421
|
+
await awsSecrets.loadSecrets({
|
|
422
|
+
secrets: [],
|
|
423
|
+
multisecrets: [{ name: 'awsAccessKeys', key: 'aws.accessKeys' }],
|
|
424
|
+
config
|
|
425
|
+
})
|
|
123
426
|
expect(config.aws.accessKeys).to.have.length(2)
|
|
124
427
|
expect(config.aws.accessKeys[0]).to.have.property('accessKeyId', 'awsKey1')
|
|
125
428
|
expect(config.aws.accessKeys[1]).to.have.property('accessKeyId', 'awsKey2')
|
|
126
429
|
})
|
|
127
|
-
})
|
|
128
430
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
431
|
+
it('throws on invalid JSON: value', async() => {
|
|
432
|
+
// Mock a secret that returns a broken JSON: value
|
|
433
|
+
secretsMock.reset()
|
|
434
|
+
secretsMock.on(GetSecretValueCommand).resolves({
|
|
435
|
+
SecretString: JSON.stringify({ data: 'JSON:{invalid' })
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
await awsSecrets.loadSecrets({
|
|
440
|
+
secrets: [{ name: 'badJson', key: 'errorVar' }],
|
|
441
|
+
config
|
|
442
|
+
})
|
|
443
|
+
expect.fail('Should have thrown')
|
|
136
444
|
}
|
|
137
445
|
catch(e) {
|
|
138
|
-
expect(e).to.
|
|
139
|
-
|
|
446
|
+
expect(e.message).to.equal('invalidJSON')
|
|
447
|
+
}
|
|
448
|
+
finally {
|
|
449
|
+
secretsMock.reset()
|
|
450
|
+
buildSecretsMocks()
|
|
140
451
|
}
|
|
141
452
|
})
|
|
142
|
-
})
|
|
143
|
-
|
|
144
453
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
454
|
+
it('skips secrets with ignoreInTestMode in test environment', async() => {
|
|
455
|
+
await awsSecrets.loadSecrets({
|
|
456
|
+
secrets: [
|
|
457
|
+
{ name: 'configVar1', key: 'configVar1' },
|
|
458
|
+
{ name: 'configVar6', key: 'configVar6', ignoreInTestMode: true }
|
|
459
|
+
],
|
|
460
|
+
config
|
|
461
|
+
})
|
|
462
|
+
expect(config.configVar6).to.be.undefined
|
|
463
|
+
expect(config.configVar1).to.have.property('c2')
|
|
149
464
|
})
|
|
150
|
-
|
|
465
|
+
|
|
466
|
+
it('outputs debug logs when debug is true', async() => {
|
|
467
|
+
const hook = captureStream(process.stderr)
|
|
468
|
+
await awsSecrets.loadSecrets({
|
|
469
|
+
secrets: [{ name: 'configVar1', key: 'configVar1' }],
|
|
470
|
+
config,
|
|
471
|
+
debug: true
|
|
472
|
+
})
|
|
151
473
|
hook.unhook()
|
|
474
|
+
expect(hook.captured()).to.include('ac-awsSecrets')
|
|
152
475
|
})
|
|
153
476
|
|
|
154
|
-
it('
|
|
155
|
-
|
|
156
|
-
|
|
477
|
+
it('rejects __proto__ in secret key', async() => {
|
|
478
|
+
try {
|
|
479
|
+
await awsSecrets.loadSecrets({
|
|
480
|
+
secrets: [{ name: 'configVar1', key: '__proto__.polluted' }],
|
|
481
|
+
config
|
|
482
|
+
})
|
|
483
|
+
expect(({}).polluted).to.be.undefined
|
|
484
|
+
}
|
|
485
|
+
catch(e) {
|
|
486
|
+
expect(e.message).to.include('unsafe key segment')
|
|
487
|
+
}
|
|
157
488
|
})
|
|
158
|
-
})
|
|
489
|
+
})
|