ac-awssecrets 2.5.6 → 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 +56 -0
- package/Makefile +2 -2
- package/SECURITY.md +22 -0
- package/eslint.config.js +42 -26
- package/index.js +183 -230
- package/package.json +13 -6
- package/test/config.js +58 -156
- package/test/test.js +417 -86
package/index.js
CHANGED
|
@@ -1,44 +1,76 @@
|
|
|
1
1
|
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager')
|
|
2
2
|
const { SSMClient, GetParameterCommand, GetParametersCommand, GetParametersByPathCommand } = require("@aws-sdk/client-ssm")
|
|
3
3
|
|
|
4
|
-
const testConfig = require('./test/config')
|
|
5
4
|
const functionName = 'ac-awsSecrets'.padEnd(15)
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Replaces configuration variables with secrets
|
|
9
|
-
|
|
10
|
-
KEY is the variable name
|
|
11
|
-
NAME is the name of the secret
|
|
12
|
-
|
|
13
|
-
OPT
|
|
14
|
-
serverName
|
|
15
|
-
|
|
16
|
-
MULTISECRETS
|
|
17
|
-
Multisecrets -> the secret contains a list of secrets that should be fetched
|
|
18
8
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
9
|
+
* KEY is the variable name
|
|
10
|
+
* NAME is the name of the secret
|
|
11
|
+
*
|
|
12
|
+
* MULTISECRETS
|
|
13
|
+
* Multisecrets -> the secret contains a list of secrets that should be fetched
|
|
21
14
|
*/
|
|
22
15
|
|
|
23
16
|
const awsSecrets = () => {
|
|
24
17
|
|
|
25
|
-
|
|
18
|
+
// Helper function to check for unsafe key segments
|
|
19
|
+
const isUnsafeKeySegment = (segment) => (
|
|
20
|
+
segment === '__proto__' ||
|
|
21
|
+
segment === 'constructor' ||
|
|
22
|
+
segment === 'prototype'
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const getKey = (obj, key) => {
|
|
26
|
+
if (!obj || typeof key !== 'string') {
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return key.split('.').reduce((acc, cur) => {
|
|
31
|
+
if (isUnsafeKeySegment(cur)) {
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
if (acc === undefined || acc === null) {
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
return acc[cur]
|
|
38
|
+
}, obj)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const setKey = (obj, key, value) => {
|
|
42
|
+
if (!obj || typeof key !== 'string') {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
26
45
|
|
|
27
|
-
const setKey = (obj, key, value ) => {
|
|
28
46
|
const [head, ...rest] = key.split('.')
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
47
|
+
|
|
48
|
+
if (isUnsafeKeySegment(head)) {
|
|
49
|
+
throw new Error('Refusing to set unsafe key segment: ' + head)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!rest.length) {
|
|
53
|
+
obj[head] = value
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
if (obj[head] === undefined || obj[head] === null || typeof obj[head] !== 'object') {
|
|
57
|
+
obj[head] = {}
|
|
58
|
+
}
|
|
59
|
+
setKey(obj[head], rest.join('.'), value)
|
|
60
|
+
}
|
|
32
61
|
}
|
|
33
62
|
|
|
34
63
|
const deepMerge = (target, source) => {
|
|
35
64
|
if (Array.isArray(target) && Array.isArray(source)) {
|
|
36
65
|
return [...new Set([...target, ...source])]
|
|
37
66
|
}
|
|
38
|
-
|
|
67
|
+
|
|
39
68
|
if (typeof source === 'object' && source !== null && typeof target === 'object' && target !== null) {
|
|
40
69
|
const result = { ...target }
|
|
41
70
|
for (const key in source) {
|
|
71
|
+
if (isUnsafeKeySegment(key)) {
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
42
74
|
if (key in result) {
|
|
43
75
|
result[key] = deepMerge(result[key], source[key])
|
|
44
76
|
}
|
|
@@ -48,23 +80,31 @@ const awsSecrets = () => {
|
|
|
48
80
|
}
|
|
49
81
|
return result
|
|
50
82
|
}
|
|
51
|
-
|
|
83
|
+
|
|
52
84
|
return source
|
|
53
85
|
}
|
|
54
86
|
|
|
55
87
|
const setValue = (config, { path, value, array = false, property, merge = false }) => {
|
|
56
|
-
// path can be from AWS
|
|
88
|
+
// path can be from AWS parameter store (/a/b/c) or a real JSON path (a.b.c)
|
|
57
89
|
const keys = path.includes('/') ? path.split('/').filter(Boolean) : path.split('.')
|
|
58
90
|
const lastKey = keys.pop()
|
|
91
|
+
|
|
92
|
+
if (isUnsafeKeySegment(lastKey)) {
|
|
93
|
+
throw new Error('Refusing to set unsafe key segment: ' + lastKey)
|
|
94
|
+
}
|
|
95
|
+
|
|
59
96
|
let pointer = config
|
|
60
|
-
|
|
97
|
+
|
|
61
98
|
for (const key of keys) {
|
|
99
|
+
if (isUnsafeKeySegment(key)) {
|
|
100
|
+
throw new Error('Refusing to traverse unsafe key segment: ' + key)
|
|
101
|
+
}
|
|
62
102
|
if (!pointer[key]) {
|
|
63
103
|
pointer[key] = {}
|
|
64
104
|
}
|
|
65
105
|
pointer = pointer[key]
|
|
66
106
|
}
|
|
67
|
-
|
|
107
|
+
|
|
68
108
|
if (array) {
|
|
69
109
|
if (!Array.isArray(pointer[lastKey])) {
|
|
70
110
|
pointer[lastKey] = []
|
|
@@ -72,331 +112,246 @@ const awsSecrets = () => {
|
|
|
72
112
|
if (property) {
|
|
73
113
|
const [propKey, propValue] = Object.entries(property)[0]
|
|
74
114
|
const index = pointer[lastKey].findIndex(item => item[propKey] === propValue)
|
|
75
|
-
|
|
115
|
+
|
|
76
116
|
if (index !== -1) {
|
|
77
117
|
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
78
118
|
throw new Error("Value must be an object when replacing an entry in the array.")
|
|
79
119
|
}
|
|
80
120
|
// Merge existing properties with new ones
|
|
81
121
|
pointer[lastKey][index] = { ...pointer[lastKey][index], ...value }
|
|
82
|
-
}
|
|
122
|
+
}
|
|
83
123
|
else {
|
|
84
124
|
pointer[lastKey].push(value)
|
|
85
125
|
}
|
|
86
|
-
}
|
|
126
|
+
}
|
|
87
127
|
else {
|
|
88
128
|
pointer[lastKey].push(value)
|
|
89
129
|
}
|
|
90
|
-
}
|
|
130
|
+
}
|
|
91
131
|
else {
|
|
92
132
|
if (merge && typeof pointer[lastKey] === 'object' && !Array.isArray(pointer[lastKey]) && typeof value === 'object' && !Array.isArray(value)) {
|
|
93
|
-
pointer[lastKey] = deepMerge(pointer[lastKey], value)
|
|
133
|
+
pointer[lastKey] = deepMerge(pointer[lastKey], value)
|
|
94
134
|
}
|
|
95
135
|
else {
|
|
96
136
|
pointer[lastKey] = value
|
|
97
137
|
}
|
|
98
138
|
}
|
|
99
139
|
}
|
|
100
|
-
|
|
101
140
|
|
|
102
|
-
|
|
141
|
+
|
|
142
|
+
const loadSecretParameters = async({ secretParameters = [], config = {}, debug = false, throwError = false, region = 'eu-central-1' } = {}) => {
|
|
103
143
|
const environment = config?.environment || process.env.NODE_ENV || 'development'
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
const ssmClient = new SSMClient(awsConfig)
|
|
109
|
-
|
|
144
|
+
|
|
145
|
+
const ssmClient = new SSMClient({ region })
|
|
146
|
+
|
|
110
147
|
// Process parameters in batches of 10 (AWS limit for GetParametersCommand)
|
|
111
148
|
const processBatchedParameters = async(paramList) => {
|
|
112
|
-
// Skip if no parameters
|
|
113
149
|
if (paramList.length === 0) return
|
|
114
|
-
|
|
115
|
-
// Split parameters into batches of 10
|
|
150
|
+
|
|
116
151
|
const batchSize = 10
|
|
117
152
|
const batches = []
|
|
118
|
-
|
|
153
|
+
|
|
119
154
|
for (let i = 0; i < paramList.length; i += batchSize) {
|
|
120
155
|
batches.push(paramList.slice(i, i + batchSize))
|
|
121
156
|
}
|
|
122
|
-
|
|
123
|
-
// Process each batch
|
|
157
|
+
|
|
124
158
|
for (const batch of batches) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
159
|
+
try {
|
|
160
|
+
const parameterNames = batch.map(param => `/${environment}/${param.name}`)
|
|
161
|
+
|
|
162
|
+
const command = new GetParametersCommand({
|
|
163
|
+
Names: parameterNames,
|
|
164
|
+
WithDecryption: true
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const response = await ssmClient.send(command)
|
|
168
|
+
const parameters = response?.Parameters || []
|
|
169
|
+
|
|
170
|
+
await Promise.all(parameters.map(async parameter => {
|
|
171
|
+
const parameterName = parameter.Name
|
|
172
|
+
const paramConfig = batch.find(p => `/${environment}/${p.name}` === parameterName)
|
|
173
|
+
|
|
174
|
+
if (!paramConfig) return
|
|
175
|
+
|
|
176
|
+
let value = parameter.Value
|
|
177
|
+
|
|
178
|
+
if (paramConfig.json && value) {
|
|
133
179
|
try {
|
|
134
180
|
value = JSON.parse(value)
|
|
135
|
-
}
|
|
181
|
+
}
|
|
136
182
|
catch (e) {
|
|
137
183
|
console.error('%s | %s | %s', functionName, parameterName, e?.message)
|
|
138
184
|
if (throwError) throw e
|
|
185
|
+
return
|
|
139
186
|
}
|
|
140
187
|
}
|
|
141
|
-
|
|
188
|
+
|
|
142
189
|
if (debug) {
|
|
143
190
|
console.warn('P %s | T %s | V %j', parameterName, typeof value, value)
|
|
144
191
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
192
|
+
|
|
193
|
+
setValue(config, {
|
|
194
|
+
path: (paramConfig.path || paramConfig.name),
|
|
195
|
+
value,
|
|
196
|
+
array: paramConfig.array || false,
|
|
197
|
+
property: paramConfig.property,
|
|
198
|
+
merge: paramConfig.merge || false
|
|
151
199
|
})
|
|
152
200
|
}))
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const parameterNames = batch.map(param => `/${environment}/${param.name}`)
|
|
159
|
-
|
|
160
|
-
// Fetch all parameters in this batch with a single API call
|
|
161
|
-
const command = new GetParametersCommand({
|
|
162
|
-
Names: parameterNames,
|
|
163
|
-
WithDecryption: true
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
const response = await ssmClient.send(command)
|
|
167
|
-
const parameters = response?.Parameters || []
|
|
168
|
-
|
|
169
|
-
// Process each parameter
|
|
170
|
-
await Promise.all(parameters.map(async parameter => {
|
|
171
|
-
// Find corresponding parameter config
|
|
172
|
-
const paramName = parameter.Name
|
|
173
|
-
const paramConfig = batch.find(p => `/${environment}/${p.name}` === paramName)
|
|
174
|
-
|
|
175
|
-
if (!paramConfig) return // Skip if no matching config found
|
|
176
|
-
|
|
177
|
-
let value = parameter.Value
|
|
178
|
-
|
|
179
|
-
if (paramConfig.json && value) {
|
|
180
|
-
try {
|
|
181
|
-
value = JSON.parse(value)
|
|
182
|
-
}
|
|
183
|
-
catch (e) {
|
|
184
|
-
console.error('%s | %s | %s', functionName, paramName, e?.message)
|
|
185
|
-
if (throwError) throw e
|
|
186
|
-
return // Skip this parameter if JSON parsing fails
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (debug) {
|
|
191
|
-
console.warn('P %s | T %s | V %j', paramName, typeof value, value)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
setValue(config, {
|
|
195
|
-
path: (paramConfig.path || paramConfig.name),
|
|
196
|
-
value,
|
|
197
|
-
array: paramConfig.array || false,
|
|
198
|
-
property: paramConfig.property,
|
|
199
|
-
merge: paramConfig.merge || false
|
|
200
|
-
})
|
|
201
|
-
}))
|
|
202
|
-
|
|
203
|
-
// Handle invalid parameters
|
|
204
|
-
if (response?.InvalidParameters?.length > 0) {
|
|
205
|
-
console.error('%s | Invalid parameters: %j', functionName, response.InvalidParameters)
|
|
206
|
-
if (throwError) {
|
|
207
|
-
throw new Error(`Invalid parameters: ${response.InvalidParameters.join(', ')}`)
|
|
208
|
-
}
|
|
201
|
+
|
|
202
|
+
if (response?.InvalidParameters?.length > 0) {
|
|
203
|
+
console.error('%s | Invalid parameters: %j', functionName, response.InvalidParameters)
|
|
204
|
+
if (throwError) {
|
|
205
|
+
throw new Error(`Invalid parameters: ${response.InvalidParameters.join(', ')}`)
|
|
209
206
|
}
|
|
210
|
-
}
|
|
211
|
-
catch (e) {
|
|
212
|
-
console.error('%s | Batch parameter fetch error: %s', functionName, e?.message)
|
|
213
|
-
if (throwError) throw e
|
|
214
|
-
|
|
215
|
-
// Fallback: process parameters individually if batch fails
|
|
216
|
-
await Promise.all(batch.map(param => getSecretParameter(param)))
|
|
217
207
|
}
|
|
218
208
|
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
// Security errors must always propagate, regardless of throwError flag
|
|
211
|
+
if (e?.message?.includes('unsafe key segment')) throw e
|
|
212
|
+
|
|
213
|
+
console.error('%s | Batch parameter fetch error: %s', functionName, e?.message)
|
|
214
|
+
if (throwError) throw e
|
|
215
|
+
|
|
216
|
+
// Fallback: process parameters individually if batch fails
|
|
217
|
+
await Promise.all(batch.map(param => getSecretParameter(param)))
|
|
218
|
+
}
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
|
-
|
|
222
|
-
//
|
|
221
|
+
|
|
222
|
+
// Fallback for individual parameter fetching
|
|
223
223
|
const getSecretParameter = async(param) => {
|
|
224
224
|
const parameterName = `/${environment}/${param.name}`
|
|
225
225
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
Name: parameterName,
|
|
235
|
-
WithDecryption: true,
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
// Send the command to retrieve the parameter
|
|
239
|
-
const response = await ssmClient.send(command)
|
|
240
|
-
value = response?.Parameter?.Value
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Extract and return the parameter value
|
|
226
|
+
const command = new GetParameterCommand({
|
|
227
|
+
Name: parameterName,
|
|
228
|
+
WithDecryption: true,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const response = await ssmClient.send(command)
|
|
232
|
+
let value = response?.Parameter?.Value
|
|
233
|
+
|
|
244
234
|
if (param.json) {
|
|
245
235
|
value = JSON.parse(value)
|
|
246
236
|
}
|
|
247
|
-
|
|
237
|
+
|
|
248
238
|
if (debug) {
|
|
249
239
|
console.warn('P %s | T %s | V %j', parameterName, typeof value, value)
|
|
250
240
|
}
|
|
251
241
|
setValue(config, { path: (param.path || param.name), value, array: param.array, property: param.property, merge: param.merge })
|
|
252
|
-
}
|
|
242
|
+
}
|
|
253
243
|
catch (e) {
|
|
254
244
|
console.error('%s | %s | %s', functionName, parameterName, e?.message)
|
|
255
245
|
if (throwError) throw e
|
|
256
246
|
}
|
|
257
247
|
}
|
|
258
|
-
|
|
259
|
-
//
|
|
248
|
+
|
|
249
|
+
// Fetch all parameters under a path (wildcard support)
|
|
260
250
|
const getSecretParametersByPath = async({ path, name, json = false, array, property, merge }) => {
|
|
261
251
|
if (!path) throw new Error('pathMustBeSet')
|
|
262
252
|
const parameterName = `/${environment}/${name}`
|
|
263
253
|
try {
|
|
264
|
-
let valueArray
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
valueArray = testConfig.parameterStore.filter(item => {
|
|
268
|
-
return item.name.startsWith(parameterName.replace('*', ''))
|
|
269
|
-
})
|
|
270
|
-
valueArray = valueArray.map(item => {
|
|
271
|
-
return {
|
|
272
|
-
Name: item?.name,
|
|
273
|
-
Type: 'SecureString',
|
|
274
|
-
Value: item?.value,
|
|
275
|
-
Version: 1,
|
|
276
|
-
LastModifiedDate: new Date(),
|
|
277
|
-
ARN: `arn:aws:ssm:region:account-id:parameter/${item?.name}`,
|
|
278
|
-
DataType: 'text'
|
|
279
|
-
}
|
|
280
|
-
})
|
|
281
|
-
}
|
|
282
|
-
else {
|
|
283
|
-
// fetch all paramters with the path
|
|
254
|
+
let valueArray = []
|
|
255
|
+
let nextToken = undefined
|
|
256
|
+
do {
|
|
284
257
|
const command = new GetParametersByPathCommand({
|
|
285
|
-
Path: parameterName.
|
|
258
|
+
Path: parameterName.replaceAll('*', ''),
|
|
286
259
|
Recursive: true,
|
|
287
260
|
WithDecryption: true,
|
|
261
|
+
NextToken: nextToken,
|
|
288
262
|
})
|
|
289
263
|
const response = await ssmClient.send(command)
|
|
290
|
-
valueArray
|
|
264
|
+
valueArray.push(...response.Parameters)
|
|
265
|
+
nextToken = response.NextToken
|
|
291
266
|
}
|
|
292
|
-
|
|
267
|
+
while (nextToken)
|
|
268
|
+
|
|
293
269
|
for (const item of valueArray) {
|
|
294
270
|
let value = item?.Value
|
|
295
|
-
|
|
271
|
+
|
|
296
272
|
if (json) {
|
|
297
273
|
value = JSON.parse(value)
|
|
298
274
|
}
|
|
299
|
-
|
|
275
|
+
|
|
300
276
|
if (debug) {
|
|
301
277
|
console.warn('P %s | T %s | V %j', item?.Name, typeof value, value)
|
|
302
278
|
}
|
|
303
279
|
setValue(config, { path, value, array, property, merge })
|
|
304
280
|
}
|
|
305
|
-
}
|
|
281
|
+
}
|
|
306
282
|
catch (e) {
|
|
307
283
|
console.error('%s | %s | %s', functionName, parameterName, e?.message)
|
|
308
284
|
if (throwError) throw e
|
|
309
285
|
}
|
|
310
286
|
}
|
|
311
|
-
|
|
287
|
+
|
|
312
288
|
// Filter out parameters with ignoreInTestMode = true in test environment
|
|
313
289
|
let filteredParams = secretParameters
|
|
314
290
|
if (environment === 'test') {
|
|
315
291
|
filteredParams = secretParameters.filter(param => !param.ignoreInTestMode)
|
|
316
292
|
}
|
|
317
|
-
|
|
318
|
-
// Add debug if needed
|
|
293
|
+
|
|
319
294
|
if (debug) {
|
|
320
295
|
filteredParams.forEach(param => param.debug = true)
|
|
321
296
|
}
|
|
322
|
-
|
|
297
|
+
|
|
323
298
|
// Split parameters into regular and wildcard ones
|
|
324
299
|
const wildcardParams = filteredParams.filter(param => param.name.endsWith('*'))
|
|
325
300
|
const regularParams = filteredParams.filter(param => !param.name.endsWith('*'))
|
|
326
|
-
|
|
327
|
-
// Process parameters in parallel
|
|
301
|
+
|
|
328
302
|
await Promise.all([
|
|
329
|
-
// Process regular parameters in batches
|
|
330
303
|
processBatchedParameters(regularParams),
|
|
331
|
-
|
|
332
|
-
// Process wildcard parameters individually (using original method)
|
|
333
304
|
...wildcardParams.map(param => getSecretParametersByPath(param))
|
|
334
305
|
])
|
|
335
306
|
}
|
|
336
307
|
|
|
337
308
|
|
|
338
|
-
const loadSecrets = async({ secrets = [], multisecrets = [], config = {},
|
|
309
|
+
const loadSecrets = async({ secrets = [], multisecrets = [], config = {}, debug = false, region = 'eu-central-1' } = {}) => {
|
|
339
310
|
const environment = config?.environment || 'development'
|
|
340
311
|
|
|
341
|
-
const
|
|
342
|
-
region
|
|
343
|
-
}
|
|
344
|
-
const client = new SecretsManagerClient(awsConfig)
|
|
312
|
+
const client = new SecretsManagerClient({ region })
|
|
345
313
|
|
|
346
|
-
|
|
347
314
|
const getSecret = async({ secret }) => {
|
|
348
315
|
const secretName = (environment === 'test' ? 'test.' : '') + secret?.name + (secret?.suffix ? '.' + secret?.suffix : '')
|
|
349
316
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const command = new GetSecretValueCommand({
|
|
358
|
-
SecretId: secretName
|
|
359
|
-
})
|
|
360
|
-
try {
|
|
361
|
-
const response = await client.send(command)
|
|
362
|
-
if (response?.SecretString) {
|
|
363
|
-
secret.value = JSON.parse(response?.SecretString)
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
catch(e) {
|
|
367
|
-
console.error('%s | %s | %s', functionName, secretName, e?.message)
|
|
317
|
+
const command = new GetSecretValueCommand({
|
|
318
|
+
SecretId: secretName
|
|
319
|
+
})
|
|
320
|
+
try {
|
|
321
|
+
const response = await client.send(command)
|
|
322
|
+
if (response?.SecretString) {
|
|
323
|
+
secret.value = JSON.parse(response?.SecretString)
|
|
368
324
|
}
|
|
369
325
|
}
|
|
326
|
+
catch(e) {
|
|
327
|
+
console.error('%s | %s | %s', functionName, secretName, e?.message)
|
|
328
|
+
}
|
|
370
329
|
return secret
|
|
371
330
|
}
|
|
372
331
|
|
|
373
332
|
const fetchSecrets = async({ secrets }) => {
|
|
374
|
-
//
|
|
333
|
+
// Filter out secrets with ignoreInTestMode = true in test environment
|
|
375
334
|
if (environment === 'test') {
|
|
376
335
|
secrets = secrets.filter(secret => !secret.ignoreInTestMode)
|
|
377
336
|
}
|
|
378
337
|
return Promise.all(secrets.map(secret => getSecret({ secret })))
|
|
379
338
|
}
|
|
380
339
|
|
|
381
|
-
//
|
|
340
|
+
// Fetch multisecrets first and expand them into the secrets list
|
|
382
341
|
if (multisecrets.length > 0) {
|
|
383
|
-
|
|
384
|
-
// we have to fetch them first from a secret and add them to the secrets to fetch
|
|
385
|
-
let secretsToAdd = await fetchSecrets({ secrets: multisecrets })
|
|
386
|
-
// iterate each multisecret and add the values as new secrets
|
|
342
|
+
const secretsToAdd = await fetchSecrets({ secrets: multisecrets })
|
|
387
343
|
secretsToAdd.forEach(secadd => {
|
|
388
|
-
|
|
344
|
+
const items = JSON.parse(secadd?.value?.values) || []
|
|
389
345
|
if (typeof items !== 'object' || items.length < 1) {
|
|
390
346
|
console.error('%s | %s | MultiSecret has no valid property values', functionName, secadd.name)
|
|
391
347
|
throw new Error('MultiSecret has no valid property values')
|
|
392
348
|
}
|
|
393
349
|
items.forEach(item => {
|
|
394
|
-
|
|
350
|
+
secrets.push({
|
|
395
351
|
key: secadd.key,
|
|
396
352
|
name: item,
|
|
397
|
-
type: 'arrayObject'
|
|
398
|
-
}
|
|
399
|
-
secrets.push(p)
|
|
353
|
+
type: 'arrayObject'
|
|
354
|
+
})
|
|
400
355
|
})
|
|
401
356
|
})
|
|
402
357
|
}
|
|
@@ -405,11 +360,14 @@ const awsSecrets = () => {
|
|
|
405
360
|
await fetchSecrets({ secrets })
|
|
406
361
|
for (const secret of secrets) {
|
|
407
362
|
let existingValue = getKey(config, secret.key) || {}
|
|
408
|
-
|
|
363
|
+
const value = secret?.value
|
|
409
364
|
if (value) {
|
|
410
|
-
//
|
|
365
|
+
// Convert string booleans and JSON values
|
|
411
366
|
if (typeof value === 'object') {
|
|
412
|
-
Object.keys(value)
|
|
367
|
+
for (const key of Object.keys(value)) {
|
|
368
|
+
if (isUnsafeKeySegment(key)) {
|
|
369
|
+
continue
|
|
370
|
+
}
|
|
413
371
|
let val = value[key]
|
|
414
372
|
if (val === 'true') val = true
|
|
415
373
|
else if (val === 'false') val = false
|
|
@@ -417,32 +375,27 @@ const awsSecrets = () => {
|
|
|
417
375
|
try {
|
|
418
376
|
val = JSON.parse(val.substring(5))
|
|
419
377
|
}
|
|
420
|
-
catch
|
|
378
|
+
catch {
|
|
421
379
|
console.error('%s | %s | JSON could not be parsed %j', functionName, secret.name, val)
|
|
422
380
|
throw new Error('invalidJSON')
|
|
423
381
|
}
|
|
424
382
|
}
|
|
425
383
|
value[key] = val
|
|
426
|
-
}
|
|
384
|
+
}
|
|
427
385
|
}
|
|
428
386
|
|
|
429
387
|
if (secret.servers) {
|
|
430
388
|
if (typeof secret.servers === 'boolean') {
|
|
431
|
-
|
|
432
|
-
|
|
389
|
+
const servers = existingValue?.servers || []
|
|
390
|
+
const updatedServers = servers.map(server => {
|
|
433
391
|
if (server.server === secret.serverName) {
|
|
434
392
|
server = { ...server, ...value }
|
|
435
393
|
}
|
|
436
394
|
return server
|
|
437
395
|
})
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
/* TODO: Probably not used anywhere, so legacy is ok
|
|
442
|
-
let match = {}
|
|
443
|
-
_.set(match, _.get(secret.servers, 'identifier'), _.get(secret.servers, 'value'))
|
|
444
|
-
existingValue = _.find(_.get(config, key, []), match)
|
|
445
|
-
*/
|
|
396
|
+
if (!isUnsafeKeySegment('servers')) {
|
|
397
|
+
config[secret.key].servers = updatedServers
|
|
398
|
+
}
|
|
446
399
|
}
|
|
447
400
|
}
|
|
448
401
|
else if (secret?.type === 'arrayObject') {
|
|
@@ -462,7 +415,7 @@ const awsSecrets = () => {
|
|
|
462
415
|
console.warn('%s | %s | %j', functionName, secret?.name, existingValue)
|
|
463
416
|
}
|
|
464
417
|
}
|
|
465
|
-
}
|
|
418
|
+
}
|
|
466
419
|
}
|
|
467
420
|
|
|
468
421
|
return {
|
package/package.json
CHANGED
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
"author": "Mark Poepping (https://www.admiralcloud.com)",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": "admiralcloud/ac-awssecrets",
|
|
6
|
-
"version": "
|
|
6
|
+
"version": "3.0.1",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@aws-sdk/client-secrets-manager": "^3.
|
|
9
|
-
"@aws-sdk/client-ssm": "^3.
|
|
8
|
+
"@aws-sdk/client-secrets-manager": "^3.997.0",
|
|
9
|
+
"@aws-sdk/client-ssm": "^3.997.0"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"ac-semantic-release": "^0.4.10",
|
|
13
|
-
"
|
|
13
|
+
"aws-sdk-client-mock": "^4.1.0",
|
|
14
|
+
"c8": "^11.0.0",
|
|
14
15
|
"chai": "^4.x",
|
|
15
|
-
"eslint": "
|
|
16
|
+
"eslint": "10.x",
|
|
17
|
+
"@eslint/js": "^10.0.1",
|
|
18
|
+
"globals": "^17.3.0",
|
|
16
19
|
"mocha": "^11.7.5"
|
|
17
20
|
},
|
|
18
21
|
"scripts": {
|
|
@@ -20,7 +23,11 @@
|
|
|
20
23
|
"coverage": "./node_modules/c8/bin/c8.js yarn test"
|
|
21
24
|
},
|
|
22
25
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
26
|
+
"node": ">=20.0.0"
|
|
27
|
+
},
|
|
28
|
+
"resolutions": {
|
|
29
|
+
"mocha/diff": "^8.0.3",
|
|
30
|
+
"minimatch": "^10.2.1"
|
|
24
31
|
},
|
|
25
32
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
26
33
|
}
|