ac-awssecrets 1.1.5 → 2.0.0

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,172 @@
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
+ const { fromIni } = require("@aws-sdk/credential-providers")
3
+
4
+ const testConfig = require('./test/config')
4
5
 
5
6
  /**
6
- * @param aws OBJ object with aws configuration data
7
- */
7
+ * Replaces configuration variables with secrets
8
8
 
9
- 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
- }
9
+ KEY is the variable name
10
+ NAME is the name of the secret
47
11
 
48
- let value
49
- try {
50
- value = JSON.parse(_.get(data, 'SecretString'))
51
- }
52
- catch (e) {
53
- value = _.get(data, 'SecretString.values')
54
- }
12
+ OPT
13
+ serverName
55
14
 
56
- try {
57
- value = JSON.parse(_.get(value, 'values'))
58
- }
59
- catch (e) {
60
- return done({ message: 'placeHolderSecrets_valuesInvalid', additionalInfo: { key: secret.key } })
61
- }
15
+ MULTISECRETS
16
+ Multisecrets -> the secret contains a list of secrets that should be fetched
17
+ *
18
+ * TESTMODES
19
+ * 3 -> use secrets from testConfig
20
+ */
62
21
 
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
- }
22
+ const awsSecrets = () => {
95
23
 
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
- }
24
+ const getKey = (obj, key) => key.split('.').reduce((acc, cur) => acc[cur], obj)
119
25
 
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, {})
26
+ const setKey = (obj, key, value ) => {
27
+ const [head, ...rest] = key.split('.')
28
+ !rest.length
29
+ ? obj[head] = value
30
+ : setKey(obj[head], rest.join('.'), value)
31
+ }
127
32
 
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
- }
147
33
 
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
- }
34
+ const functionName = 'ac-awsSecrets'.padEnd(15)
35
+ const loadSecrets = async({ secrets = [], multisecrets = [], config = {}, profile = process.env['profile'], testMode = 0, debug = false } = {}) => {
36
+ const environment = config?.environment || 'development'
37
+
38
+ const awsConfig = {
39
+ region: 'eu-central-1'
40
+ }
41
+ // credentials are determined from Lambda role.
42
+ // But you can also use a set profile
43
+ if (profile) {
44
+ console.error('%s | Using AWS profile | %s', functionName, profile)
45
+ awsConfig.credentials = fromIni({ profile })
46
+ }
47
+ const client = new SecretsManagerClient(awsConfig)
48
+
49
+
50
+ const getSecret = async({ secret }) => {
51
+ const secretName = (environment === 'test' ? 'test.' : '') + secret?.name + (secret?.suffix ? '.' + secret?.suffix : '')
52
+
53
+ // TESTMODE
54
+ if (testMode === 3) {
55
+ // fetch from availableSecrets
56
+ let found = testConfig.availableSecrets.find(item => item.name === secret.name)
57
+ secret.value = found?.value
58
+ }
59
+ else {
60
+ const command = new GetSecretValueCommand({
61
+ SecretId: secretName
62
+ })
63
+ try {
64
+ const response = await client.send(command)
65
+ if (response?.SecretString) {
66
+ secret.value = JSON.parse(response?.SecretString)
67
+ }
68
+ }
69
+ catch(e) {
70
+ console.error('%s | %s | %s', functionName, secretName, e?.message)
71
+ }
72
+ }
73
+ return secret
74
+ }
158
75
 
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))
76
+ const fetchSecrets = async({ secrets }) => {
77
+ // filter out secrets with ignoreInTestMode = true
78
+ if (environment === 'test') {
79
+ secrets = secrets.filter(secret => !secret.ignoreInTestMode)
80
+ }
81
+ return Promise.all(secrets.map(secret => getSecret({ secret })))
82
+ }
83
+
84
+ // fetch placeholder
85
+ if (multisecrets.length > 0) {
86
+ // some keys can have multiple entries (e.g. cloudfrontCOnfigs can have 1 - n entries)
87
+ // we have to fetch them first from a secret and add them to the secrets to fetch
88
+ let secretsToAdd = await fetchSecrets({ secrets: multisecrets })
89
+ // iterate each multisecret and add the values as new secrets
90
+ secretsToAdd.forEach(secadd => {
91
+ let items = JSON.parse(secadd?.value?.values) || []
92
+ if (typeof items !== 'object' || items.length < 1) {
93
+ console.error('%s | %s | MultiSecret has no valid property values', functionName, secadd.name)
94
+ throw new Error('MultiSecret has no valid property values')
95
+ }
96
+ items.forEach(item => {
97
+ let p = {
98
+ key: secadd.key,
99
+ name: item,
100
+ type: 'arrayObject' // multisecrets contain multiple secrets that belong to the same config property (which is an array of objects)
101
+ }
102
+ secrets.push(p)
103
+ })
104
+ })
105
+ }
106
+
107
+ if (secrets.length > 0) {
108
+ await fetchSecrets({ secrets })
109
+ for (const secret of secrets) {
110
+ let existingValue = getKey(config, secret.key) || {}
111
+ let value = secret?.value
112
+
113
+ // convert values
114
+ if (typeof value === 'object') {
115
+ Object.keys(value).forEach((key) => {
116
+ let val = value[key]
117
+ if (val === 'true') val = true
118
+ else if (val === 'false') val = false
119
+ else if (typeof val === 'string' && val.startsWith('JSON:')) {
120
+ try {
121
+ val = JSON.parse(val.substring(5))
122
+ }
123
+ catch(e) {
124
+ console.error('%s | %s | JSON could not be parsed %j', functionName, secret.name, val)
125
+ throw new Error('invalidJSON')
126
+ }
164
127
  }
165
-
166
- result.push({ key, name: _.get(secret, 'name', '-') })
167
- return itDone()
128
+ value[key] = val
168
129
  })
169
- }, done)
130
+ }
131
+
132
+ if (secret.servers) {
133
+ if (typeof secret.servers === 'boolean') {
134
+ let servers = existingValue?.servers || []
135
+ config[secret.key].servers = servers.map(server => {
136
+ if (server.server === secret.serverName) {
137
+ server = { ...server, ...value }
138
+ }
139
+ return server
140
+ })
141
+ }
142
+ else {
143
+ // NEW NOTATION AS OBJECT
144
+ /* TODO: Probably not used anywhere, so legacy is ok
145
+ let match = {}
146
+ _.set(match, _.get(secret.servers, 'identifier'), _.get(secret.servers, 'value'))
147
+ existingValue = _.find(_.get(config, key, []), match)
148
+ */
149
+ }
150
+ }
151
+ else if (secret?.type === 'arrayObject') {
152
+ existingValue.push(value)
153
+ setKey(config, secret.key, existingValue)
154
+ }
155
+ else {
156
+ if (Object.keys(existingValue).length === 0) {
157
+ setKey(config, secret.key, {})
158
+ }
159
+ existingValue = { ...existingValue, ...value }
160
+ setKey(config, secret.key, existingValue)
161
+ }
162
+
163
+ if (secret?.log || debug) {
164
+ console.log('%s | %s | %j', functionName, secret?.name, existingValue)
165
+ }
170
166
  }
171
- }, (err) => {
172
- return cb(err, _.orderBy(result, 'key'))
173
- })
167
+ }
174
168
  }
175
169
 
176
- const awsEndpoints = [
177
- { region: 'eu-central-1', endpoint: 'https://secretsmanager.eu-central-1.amazonaws.com' }
178
- ]
179
-
180
170
  return {
181
171
  loadSecrets
182
172
  }
package/package.json CHANGED
@@ -3,19 +3,21 @@
3
3
  "author": "Mark Poepping (https://www.admiralcloud.com)",
4
4
  "license": "MIT",
5
5
  "repository": "admiralcloud/ac-awssecrets",
6
- "version": "1.1.5",
6
+ "version": "2.0.0",
7
7
  "dependencies": {
8
- "async": "^3.2.3",
9
- "aws-sdk": "^2.1101.0",
10
- "lodash": "^4.17.21"
8
+ "@aws-sdk/client-secrets-manager": "^3.279.0",
9
+ "@aws-sdk/credential-providers": "^3.279.0"
11
10
  },
12
11
  "devDependencies": {
13
- "ac-semantic-release": "^0.2.7",
12
+ "ac-semantic-release": "^0.3.5",
13
+ "chai": "^4.3.7",
14
14
  "eslint": "8.x",
15
- "mocha": "^9.2.2"
15
+ "mocha": "^10.2.0",
16
+ "nyc": "^15.1.0"
16
17
  },
17
18
  "scripts": {
18
- "test": "mocha --reporter spec"
19
+ "test": "mocha --reporter spec",
20
+ "coverage": "./node_modules/nyc/bin/nyc.js report --reporter=lcov --reporter=text"
19
21
  },
20
22
  "engines": {
21
23
  "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
+ })