ac-awssecrets 1.1.6 → 2.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/index.js CHANGED
@@ -1,182 +1,165 @@
1
- const _ = require('lodash')
2
- const async = require('async')
3
- const AWS = require('aws-sdk')
1
+ const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
2
+
3
+ const testConfig = require('./test/config')
4
4
 
5
5
  /**
6
- * @param aws OBJ object with aws configuration data
7
- */
6
+ * Replaces configuration variables with secrets
7
+
8
+ KEY is the variable name
9
+ NAME is the name of the secret
10
+
11
+ OPT
12
+ serverName
13
+
14
+ MULTISECRETS
15
+ Multisecrets -> the secret contains a list of secrets that should be fetched
16
+ *
17
+ * TESTMODES
18
+ * 3 -> use secrets from testConfig
19
+ */
8
20
 
9
21
  const awsSecrets = () => {
10
- const loadSecrets = (params, cb) => {
11
- const multiSecrets = _.get(params, 'multisecrets', [])
12
- const secrets = _.get(params, 'secrets', [])
13
- let config = _.get(params, 'config', {}) // the config object -> will be changed by reference
14
- const environment = _.get(config, 'environment', 'development')
15
-
16
- const region = _.get(params, 'aws.region', 'eu-central-1')
17
- const endpoint = _.find(awsEndpoints, { region })
18
- const client = new AWS.SecretsManager({
19
- accessKeyId: _.get(params, 'aws.accessKeyId'),
20
- secretAccessKey: _.get(params, 'aws.secretAccessKey'),
21
- region: region,
22
- endpoint: _.get(endpoint, 'endpoint')
23
- })
24
-
25
- let result = []
26
- async.series({
27
- fetchPlacholders: (done) => {
28
- if (!_.size(multiSecrets)) return done()
29
- // some keys can have multiple entries (e.g. cloudfrontCOnfigs can have 1 - n entries)
30
- // we have to fetch them first from a secret and add them to the secrets to fetch
31
-
32
- async.each(multiSecrets, (secret, itDone) => {
33
- let secretName = (config.environment === 'test' ? 'test.' : '') + _.get(secret, 'name')
34
-
35
- client.getSecretValue({ SecretId: secretName }, function(err, data) {
36
- if (err) {
37
- if (_.get(secret, 'ignoreInTestMode')) return itDone()
38
- if (_.get(secret, 'ignoreIfMissing')) return itDone() // this is an optional key
39
-
40
- console.error('Fetching secret %s failed', secretName, err)
41
- return itDone({ message: err, additionalInfo: { key: secretName } })
42
- }
43
- if (!_.get(data, 'SecretString')) {
44
- console.warn('Secret %s NOT avaialble', secretName, _.get(data, 'SecretString'))
45
- return itDone()
46
- }
47
22
 
48
- let value
49
- try {
50
- value = JSON.parse(_.get(data, 'SecretString'))
51
- }
52
- catch (e) {
53
- value = _.get(data, 'SecretString.values')
54
- }
23
+ const getKey = (obj, key) => key.split('.').reduce((acc, cur) => acc[cur], obj)
55
24
 
56
- try {
57
- value = JSON.parse(_.get(value, 'values'))
58
- }
59
- catch (e) {
60
- return done({ message: 'placeHolderSecrets_valuesInvalid', additionalInfo: { key: secret.key } })
61
- }
25
+ const setKey = (obj, key, value ) => {
26
+ const [head, ...rest] = key.split('.')
27
+ !rest.length
28
+ ? obj[head] = value
29
+ : setKey(obj[head], rest.join('.'), value)
30
+ }
62
31
 
63
- // value should be an array of keys
64
- _.forEach(value, (item) => {
65
- secrets.push({
66
- key: secret.key,
67
- name: item,
68
- type: 'arrayObject'
69
- })
70
- })
71
- return itDone()
72
- })
73
- }, done)
74
- },
75
- fetchSecrets: (done) => {
76
- if (!_.size(secrets)) return done()
77
- async.each(secrets, (secret, itDone) => {
78
- if (environment === 'test' && _.get(secret, 'ignoreInTestMode')) return itDone()
79
- // key is the local configuration path
80
- let key = _.get(secret, 'key')
81
- // secret name is the name used to fetch the secret
82
- let secretName = (config.environment === 'test' ? 'test.' : '') + _.get(secret, 'name') + (_.get(secret, 'suffix') ? '.' + _.get(secret, 'suffix') : '')
83
-
84
- client.getSecretValue({ SecretId: secretName }, function(err, data) {
85
- if (err) {
86
- if (_.get(secret, 'ignoreIfMissing')) return itDone() // this is an optional key
87
-
88
- console.error('Fetching secret %s failed', secretName, _.get(err, 'message', err))
89
- return itDone({ message: _.get(err, 'message', err), additionalInfo: { key: secretName } })
90
- }
91
- if (!_.get(data, 'SecretString')) {
92
- console.warn('Secret %s NOT avaialble', secretName, _.get(data, 'SecretString'))
93
- return itDone()
94
- }
95
32
 
96
- let value
97
- try {
98
- value = JSON.parse(_.get(data, 'SecretString'))
99
-
100
- // if value is prefixed with JSON -> parse the value
101
- if (_.get(value, 'valueHasJSON')) {
102
- _.forEach(value, (val, key) => {
103
- if (_.startsWith(val, 'JSON:')) {
104
- try {
105
- _.set(value, key, JSON.parse(val.substr(5)))
106
- }
107
- catch (e) {
108
- throw e
109
- }
110
- }
111
- })
112
- // remove that entry
113
- _.unset(value, 'valueHasJSON')
114
- }
115
- }
116
- catch (e) {
117
- value = _.get(data, 'SecretString')
118
- }
33
+ const functionName = 'ac-awsSecrets'.padEnd(15)
34
+ const loadSecrets = async({ secrets = [], multisecrets = [], config = {}, testMode = 0, debug = false } = {}) => {
35
+ const environment = config?.environment || 'development'
119
36
 
120
- // make sure boolean values are converted (from string)
121
- value = _.mapValues(value, (val) => {
122
- if (val === 'true') return true
123
- else if (val === 'false') return false
124
- else return val
125
- })
126
- let existingValue = _.get(config, key, {})
37
+ const awsConfig = {
38
+ region: 'eu-central-1'
39
+ }
40
+ const client = new SecretsManagerClient(awsConfig)
127
41
 
128
- if (secret.servers) {
129
- if (_.isBoolean(secret.servers)) {
130
- // LEGACY SUPPORT FOR OLD NOTATION - DEPRECATED - DO NOT USE ANY LONGER
131
- existingValue = _.find(_.get(config, key + '.servers', []), { server: secret.serverName })
132
- }
133
- else {
134
- // NEW NOTATION AS OBJECT
135
- let match = {}
136
- _.set(match, _.get(secret.servers, 'identifier'), _.get(secret.servers, 'value'))
137
- existingValue = _.find(_.get(config, key, []), match)
138
- }
139
- }
140
- if (_.get(secret, 'type') === 'array') {
141
- let array = []
142
- _.forEach(value, (val) => {
143
- array.push(val)
144
- })
145
- value = _.concat(existingValue, array)
146
- }
42
+
43
+ const getSecret = async({ secret }) => {
44
+ const secretName = (environment === 'test' ? 'test.' : '') + secret?.name + (secret?.suffix ? '.' + secret?.suffix : '')
147
45
 
148
- if (_.get(secret, 'type') === 'arrayObject') {
149
- existingValue.push(value)
150
- }
151
- else {
152
- let setFresh
153
- if (_.isEmpty(existingValue)) setFresh = true
154
- _.merge(existingValue, value)
155
- // setFresh -> this property/path has never existed
156
- if (setFresh) _.set(config, key, existingValue)
157
- }
46
+ // TESTMODE
47
+ if (testMode === 3) {
48
+ // fetch from availableSecrets
49
+ let found = testConfig.availableSecrets.find(item => item.name === secret.name)
50
+ secret.value = found?.value
51
+ }
52
+ else {
53
+ const command = new GetSecretValueCommand({
54
+ SecretId: secretName
55
+ })
56
+ try {
57
+ const response = await client.send(command)
58
+ if (response?.SecretString) {
59
+ secret.value = JSON.parse(response?.SecretString)
60
+ }
61
+ }
62
+ catch(e) {
63
+ console.error('%s | %s | %s', functionName, secretName, e?.message)
64
+ }
65
+ }
66
+ return secret
67
+ }
158
68
 
159
- if (_.get(secret, 'log')) {
160
- console.log(_.repeat('.', 90))
161
- console.warn(key, existingValue)
162
- console.warn(_.get(config, key))
163
- console.warn(_.repeat('.', 90))
69
+ const fetchSecrets = async({ secrets }) => {
70
+ // filter out secrets with ignoreInTestMode = true
71
+ if (environment === 'test') {
72
+ secrets = secrets.filter(secret => !secret.ignoreInTestMode)
73
+ }
74
+ return Promise.all(secrets.map(secret => getSecret({ secret })))
75
+ }
76
+
77
+ // fetch placeholder
78
+ if (multisecrets.length > 0) {
79
+ // some keys can have multiple entries (e.g. cloudfrontCOnfigs can have 1 - n entries)
80
+ // we have to fetch them first from a secret and add them to the secrets to fetch
81
+ let secretsToAdd = await fetchSecrets({ secrets: multisecrets })
82
+ // iterate each multisecret and add the values as new secrets
83
+ secretsToAdd.forEach(secadd => {
84
+ let items = JSON.parse(secadd?.value?.values) || []
85
+ if (typeof items !== 'object' || items.length < 1) {
86
+ console.error('%s | %s | MultiSecret has no valid property values', functionName, secadd.name)
87
+ throw new Error('MultiSecret has no valid property values')
88
+ }
89
+ items.forEach(item => {
90
+ let p = {
91
+ key: secadd.key,
92
+ name: item,
93
+ type: 'arrayObject' // multisecrets contain multiple secrets that belong to the same config property (which is an array of objects)
94
+ }
95
+ secrets.push(p)
96
+ })
97
+ })
98
+ }
99
+
100
+ if (secrets.length > 0) {
101
+ await fetchSecrets({ secrets })
102
+ for (const secret of secrets) {
103
+ let existingValue = getKey(config, secret.key) || {}
104
+ let value = secret?.value
105
+
106
+ // convert values
107
+ if (typeof value === 'object') {
108
+ Object.keys(value).forEach((key) => {
109
+ let val = value[key]
110
+ if (val === 'true') val = true
111
+ else if (val === 'false') val = false
112
+ else if (typeof val === 'string' && val.startsWith('JSON:')) {
113
+ try {
114
+ val = JSON.parse(val.substring(5))
115
+ }
116
+ catch(e) {
117
+ console.error('%s | %s | JSON could not be parsed %j', functionName, secret.name, val)
118
+ throw new Error('invalidJSON')
119
+ }
164
120
  }
165
-
166
- result.push({ key, name: _.get(secret, 'name', '-') })
167
- return itDone()
121
+ value[key] = val
168
122
  })
169
- }, done)
123
+ }
124
+
125
+ if (secret.servers) {
126
+ if (typeof secret.servers === 'boolean') {
127
+ let servers = existingValue?.servers || []
128
+ config[secret.key].servers = servers.map(server => {
129
+ if (server.server === secret.serverName) {
130
+ server = { ...server, ...value }
131
+ }
132
+ return server
133
+ })
134
+ }
135
+ else {
136
+ // NEW NOTATION AS OBJECT
137
+ /* TODO: Probably not used anywhere, so legacy is ok
138
+ let match = {}
139
+ _.set(match, _.get(secret.servers, 'identifier'), _.get(secret.servers, 'value'))
140
+ existingValue = _.find(_.get(config, key, []), match)
141
+ */
142
+ }
143
+ }
144
+ else if (secret?.type === 'arrayObject') {
145
+ existingValue.push(value)
146
+ setKey(config, secret.key, existingValue)
147
+ }
148
+ else {
149
+ if (Object.keys(existingValue).length === 0) {
150
+ setKey(config, secret.key, {})
151
+ }
152
+ existingValue = { ...existingValue, ...value }
153
+ setKey(config, secret.key, existingValue)
154
+ }
155
+
156
+ if (secret?.log || debug) {
157
+ console.log('%s | %s | %j', functionName, secret?.name, existingValue)
158
+ }
170
159
  }
171
- }, (err) => {
172
- return cb(err, _.orderBy(result, 'key'))
173
- })
160
+ }
174
161
  }
175
162
 
176
- const awsEndpoints = [
177
- { region: 'eu-central-1', endpoint: 'https://secretsmanager.eu-central-1.amazonaws.com' }
178
- ]
179
-
180
163
  return {
181
164
  loadSecrets
182
165
  }
package/package.json CHANGED
@@ -3,19 +3,20 @@
3
3
  "author": "Mark Poepping (https://www.admiralcloud.com)",
4
4
  "license": "MIT",
5
5
  "repository": "admiralcloud/ac-awssecrets",
6
- "version": "1.1.6",
6
+ "version": "2.0.1",
7
7
  "dependencies": {
8
- "async": "^3.2.3",
9
- "aws-sdk": "^2.1117.0",
10
- "lodash": "^4.17.21"
8
+ "@aws-sdk/client-secrets-manager": "^3.332.0"
11
9
  },
12
10
  "devDependencies": {
13
- "ac-semantic-release": "^0.3.0",
11
+ "ac-semantic-release": "^0.4.1",
12
+ "chai": "^4.3.7",
14
13
  "eslint": "8.x",
15
- "mocha": "^9.2.2"
14
+ "mocha": "^10.2.0",
15
+ "nyc": "^15.1.0"
16
16
  },
17
17
  "scripts": {
18
- "test": "mocha --reporter spec"
18
+ "test": "mocha --reporter spec",
19
+ "coverage": "./node_modules/nyc/bin/nyc.js report --reporter=lcov --reporter=text"
19
20
  },
20
21
  "engines": {
21
22
  "node": ">=8.0.0"
package/test/config.js ADDED
@@ -0,0 +1,130 @@
1
+ const config = {
2
+ configVar1: {
3
+ c1: false
4
+ },
5
+ configVar2: {
6
+ servers: [
7
+ { server: 'cacheRead', host: 'localhost', port: 6379 },
8
+ ]
9
+ },
10
+ configVar4: {
11
+ api: {
12
+ port: 90
13
+ }
14
+ },
15
+ configVar5: {
16
+ path: {
17
+ cookie: false
18
+ }
19
+ },
20
+ aws: {
21
+ accessKeys: []
22
+ }
23
+ }
24
+
25
+ const secrets = [
26
+ { key: 'configVar1', name: 'simple' },
27
+ { key: 'configVar2', name: 'server', servers: true, serverName: 'cacheRead' },
28
+ { key: 'configVar4', name: 'json' },
29
+ { key: 'configVar5.path', name: 'path' },
30
+ { key: 'configVar6', name: 'notExistingLocally' },
31
+ ]
32
+
33
+ const availableSecrets = [{
34
+ key: 'configVar1',
35
+ name: 'simple',
36
+ value: {
37
+ c1: 'true',
38
+ c2: 123,
39
+ c3: 'abc'
40
+ },
41
+ log: true
42
+ }, {
43
+ key: 'configVar2',
44
+ name: 'server',
45
+ value: {
46
+ port: 6360,
47
+ host: 'myRedisHost'
48
+ }
49
+ },
50
+ {
51
+ key: 'configVar3',
52
+ name: 'noSecret'
53
+ },
54
+ {
55
+ key: 'configVar4',
56
+ name: 'json',
57
+ value: {
58
+ api: 'JSON:{"url":"https://api.admiralcloud.com"}',
59
+ valueHasJSON: true
60
+ }
61
+ },
62
+ {
63
+ key: 'errorVar1',
64
+ name: 'invalidJSON',
65
+ value: {
66
+ api: 'JSON:abc',
67
+ valueHasJSON: true
68
+ }
69
+ },
70
+ {
71
+ key: 'configVar5.path',
72
+ name: 'path',
73
+ value: {
74
+ cookie: true
75
+ }
76
+ },
77
+ {
78
+ key: 'configVar6',
79
+ name: 'notExistingLocally',
80
+ value: {
81
+ prop1: 123,
82
+ prop2: 'abc'
83
+ }
84
+ },
85
+ {
86
+ key: 'aws.accessKeys',
87
+ name: 'aws.accessKeyConfigs',
88
+ value: {
89
+ values: '["aws.key1", "aws.key2"]'
90
+ }
91
+ },
92
+ {
93
+ key: 'aws.failedKeys',
94
+ name: 'aws.failedKeysConfig',
95
+ value: {
96
+ values: 123
97
+ }
98
+ },
99
+ {
100
+ key: 'aws.key1',
101
+ name: 'aws.key1',
102
+ value: {
103
+ accessKeyId: 'awsKey1',
104
+ secretAccessKey: 'awsSecret1'
105
+ }
106
+ },{
107
+ key: 'aws.key2',
108
+ name: 'aws.key2',
109
+ value: {
110
+ accessKeyId: 'awsKey2',
111
+ secretAccessKey: 'awsSecret2'
112
+ }
113
+ }]
114
+
115
+ const multisecrets = [
116
+ { key: 'aws.accessKeys', name: 'aws.accessKeyConfigs' }
117
+ ]
118
+
119
+ const multisecretsFail = [
120
+ { key: 'aws.failedKeys', name: 'aws.failedKeysConfig' }
121
+ ]
122
+
123
+ module.exports = {
124
+ config,
125
+ availableSecrets,
126
+ multisecretsFail,
127
+ secrets,
128
+ multisecrets
129
+ }
130
+
package/test/test.js CHANGED
@@ -1,7 +1,101 @@
1
- //const awsSecrets = require('../index')
1
+ const { expect } = require('chai')
2
2
 
3
- describe('Tests', () => {
4
- it('Should work', done => {
5
- return done()
3
+
4
+ const awsSecrets = require('../index')
5
+
6
+ const testConfig = require('./config')
7
+
8
+ const multisecrets = testConfig?.multisecrets
9
+ const config = testConfig.config
10
+ let secrets = testConfig.secrets
11
+
12
+ // HELPER for console.log checks
13
+ const captureStream = (stream) => {
14
+ let oldWrite = stream.write
15
+ var buf = ''
16
+ stream.write = (chunk) => {
17
+ buf += chunk.toString()
18
+ oldWrite.apply(stream, arguments)
19
+ }
20
+
21
+ return {
22
+ unhook: () => {
23
+ stream.write = oldWrite
24
+ },
25
+ captured: () => {
26
+ return buf
27
+ }
28
+ }
29
+ }
30
+
31
+
32
+ describe('Reading secrets', () => {
33
+ it('Read secrets', async() => {
34
+ await awsSecrets.loadSecrets({ secrets, config, multisecrets, testMode: 3 })
35
+ //console.log(18, config)
36
+ })
37
+
38
+ it('Check configVar1', async() => {
39
+ const expected = secrets.find(item => item.key === 'configVar1')
40
+ expect(config.configVar1).to.have.property('c1', true)
41
+ expect(config.configVar1).to.have.property('c2', expected.value.c2)
42
+ expect(config.configVar1).to.have.property('c3', expected.value.c3)
43
+
44
+ })
45
+
46
+ it('Check server', async() => {
47
+ const expected = secrets.find(item => item.key === 'configVar2')
48
+ expect(config.configVar2.servers[0]).to.have.property('port', expected.value.port)
49
+ })
50
+
51
+ it('Check JSON', async() => {
52
+ expect(config.configVar4.api).to.have.property('url', 'https://api.admiralcloud.com')
53
+ })
54
+
55
+ it('Check path', async() => {
56
+ expect(config.configVar5.path).to.have.property('cookie', true)
57
+ })
58
+
59
+ it('Check non existing local config', async() => {
60
+ expect(config.configVar6).to.have.property('prop1', 123)
61
+ expect(config.configVar6).to.have.property('prop2', 'abc')
62
+ })
63
+
64
+ it('Check multisecrets', async() => {
65
+ //console.log(50, config.aws.accessKeys)
66
+ expect(config.aws.accessKeys).to.have.length(2)
67
+ expect(config.aws.accessKeys[0]).to.have.property('accessKeyId', 'awsKey1')
68
+ expect(config.aws.accessKeys[1]).to.have.property('accessKeyId', 'awsKey2')
69
+ })
70
+ })
71
+
72
+ describe('Check errors', () => {
73
+ it('Check invalid JSON', async() => {
74
+ let errorSecrets = [
75
+ { name: 'invalidJSON', key: 'errorVar1' }
76
+ ]
77
+ try {
78
+ await awsSecrets.loadSecrets({ secrets: errorSecrets, config, testMode: 3 })
79
+ }
80
+ catch(e) {
81
+ expect(e).to.be.an('error')
82
+ expect(e).to.have.property('message', 'invalidJSON')
83
+ }
6
84
  })
7
85
  })
86
+
87
+
88
+ describe('Misc', () => {
89
+ var hook
90
+ beforeEach(function(){
91
+ hook = captureStream(process.stdout)
92
+ })
93
+ afterEach(function(){
94
+ hook.unhook()
95
+ })
96
+
97
+ it('Read secrets with debug mode', async() => {
98
+ await awsSecrets.loadSecrets({ secrets, config, multisecrets, testMode: 3, debug: true })
99
+ expect(hook.captured()).to.include('ac-awsSecrets')
100
+ })
101
+ })