ac-awssecrets 2.1.1 → 2.2.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.
@@ -6,7 +6,7 @@ name: Node.js CI
6
6
 
7
7
  on:
8
8
  push:
9
- branches: [ develop ]
9
+ branches: [ develop, master ]
10
10
  pull_request:
11
11
  branches: [ develop ]
12
12
 
@@ -17,7 +17,7 @@ jobs:
17
17
 
18
18
  strategy:
19
19
  matrix:
20
- node-version: [16.x, 18.x]
20
+ node-version: [18.x, 20.x]
21
21
  # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
22
22
 
23
23
  steps:
package/.ncurc.js ADDED
@@ -0,0 +1,8 @@
1
+ // List packages for minor updates
2
+ const minorUpdatePackages = ['chai']
3
+
4
+ module.exports = {
5
+ target: packageName => {
6
+ return minorUpdatePackages.includes(packageName) ? 'minor' : 'latest'
7
+ }
8
+ }
package/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
1
+ <a name="2.2.0"></a>
2
+
3
+ # [2.2.0](https://github.com/admiralcloud/ac-awssecrets/compare/v2.1.1..v2.2.0) (2024-07-15 08:04:33)
4
+
5
+
6
+ ### Feature
7
+
8
+ * **App:** Support for AWS parameter store | MP | [a8427d5086ec1eaaf675c27f04e276ef2f630c17](https://github.com/admiralcloud/ac-awssecrets/commit/a8427d5086ec1eaaf675c27f04e276ef2f630c17)
9
+ In addition to AWS secrets you can now also use AWS parameter store
10
+ Related issues: [undefined/undefined#develop](undefined/browse/develop)
11
+ ### Tests
12
+
13
+ * **App:** Fixed tests | MP | [91a21ccede855ee63e2bc15becec9839f3f9702a](https://github.com/admiralcloud/ac-awssecrets/commit/91a21ccede855ee63e2bc15becec9839f3f9702a)
14
+ Fixed tests
15
+ Related issues: [undefined/undefined#develop](undefined/browse/develop)
16
+ ### Chores
17
+
18
+ * **App:** Updated packages | MP | [2ed4f507784e6d03c44a9c98a279560550fb00d4](https://github.com/admiralcloud/ac-awssecrets/commit/2ed4f507784e6d03c44a9c98a279560550fb00d4)
19
+ Updated packages
20
+ Related issues: [undefined/undefined#develop](undefined/browse/develop)
21
+ * **App:** Updated packages | MP | [0086c4ba8ac53b1527c33a68ba2e1e776b1f2111](https://github.com/admiralcloud/ac-awssecrets/commit/0086c4ba8ac53b1527c33a68ba2e1e776b1f2111)
22
+ Updated packages
23
+ Related issues: [undefined/undefined#develop](undefined/browse/develop)
24
+ * **App:** Updated packages | MP | [11c0754761401652d0f9ff7070e9265475bfe624](https://github.com/admiralcloud/ac-awssecrets/commit/11c0754761401652d0f9ff7070e9265475bfe624)
25
+ Updated packages
26
+ Related issues: [undefined/undefined#develop](undefined/browse/develop)
1
27
  <a name="2.1.1"></a>
2
28
 
3
29
  ## [2.1.1](https://github.com/admiralcloud/ac-awssecrets/compare/v2.1.0..v2.1.1) (2024-05-08 13:44:22)
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # AC AWS Secrets
2
2
  Reads secrets from AWS secrets manager and adds them to the configuration of the embedding app.
3
3
 
4
+ Instead of AWS secrets, you can now also use AWS parameter store (which is not as expensive as AWS secrets)
5
+
4
6
  ![example workflow](https://github.com/admiralcloud/ac-awssecrets/actions/workflows/node.js.yml/badge.svg)
5
7
 
6
8
 
@@ -9,7 +11,69 @@ Reads secrets from AWS secrets manager and adds them to the configuration of the
9
11
  + async/await - no callback!
10
12
  + uses AWS IAM roles or AWS IAM profiles instead of IAM credentials
11
13
 
12
- # Parameters
14
+ # AWS Parameter Store
15
+ Using parameter store is a less expensive and gives you more flexibility in handling password and other configurations that should not be hardcoded.
16
+
17
+ ## Usage of AWS parmeters
18
+ When you create your parameters in AW parameter store, use the following structure:
19
+ ```
20
+ /ENVIRONMENT/CONFIG_PATH[/...]
21
+ ```
22
+
23
+ The script will replace configuration properties based on the path. See example for more information:
24
+ ```
25
+ // your app's example configuration
26
+ const config = {
27
+ http: {
28
+ port: 8080
29
+ },
30
+ database: {
31
+ servers: [
32
+ { server: 'mainDB' }
33
+ ]
34
+ }
35
+ }
36
+
37
+ // AWS parameters (values must be stored as strigified JSON)
38
+ /development/http -> { port: 8090 }
39
+ /development/database -> { host: 'awsAurora', port: 3306 }:
40
+
41
+ // function payload
42
+ const payload = {
43
+ secretParameters: [
44
+ { name: 'http', json: true },
45
+ { name: 'database', json: true, array: true, property: { server: 'mainDB' }}
46
+ ],
47
+ config
48
+ }
49
+ await awsSecrets.loadSecretParameters(payload)
50
+
51
+ // result
52
+ const config = {
53
+ http: {
54
+ port: 8090
55
+ },
56
+ database: {
57
+ servers: [
58
+ { server: 'mainDB', host: 'awsAurora', port: 3306 }
59
+ ]
60
+ }
61
+ }
62
+
63
+ ```
64
+
65
+ ## Options
66
+ |Parameter|Type|Required|Description|
67
+ |---|---|---|---|
68
+ |name|string|yes|name of the parameter (without environment) (and property in config)
69
+ |path|string|-|If your config property does not match name, you can specify the path
70
+ |json|boolean|-|If true, the parameter value will be parsed as JSON
71
+ |array|boolean|-|If true, the the value will be pushed to the array at name or path
72
+ |property|object|-|If set, instead of pushing the value to an array it will inserted at the object which matches the property
73
+
74
+
75
+ # AWS Secrets
76
+ ## Parameters
13
77
  |Parameter|Type|Required|Description|
14
78
  |---|---|---|---|
15
79
  |key|string|yes|the local variable name|
@@ -17,12 +81,12 @@ Reads secrets from AWS secrets manager and adds them to the configuration of the
17
81
  |servers|bool|-|See below
18
82
  |valueHasJSON|bool|-|If true, some properties have JSON content (prefixed with JSON:)
19
83
 
20
- # Usage
84
+ ## Usage
21
85
  AWS secret is a JSON object. Those properties will be merged with local config properties based on the secret's name.
22
86
 
23
- ## Secret
87
+ ### Secret
24
88
 
25
- ### Store secret in AWS
89
+ #### Store secret in AWS
26
90
  ```
27
91
  Example secret
28
92
  // name: mySecret1
@@ -33,7 +97,7 @@ Example secret
33
97
  }
34
98
  ```
35
99
 
36
- ### Configure a local variable, that should be enhanced with the secret
100
+ #### Configure a local variable, that should be enhanced with the secret
37
101
  ```
38
102
  const config = {
39
103
  key1: {},
@@ -43,7 +107,7 @@ const config = {
43
107
  }
44
108
  ```
45
109
 
46
- ### Fetch secrets
110
+ #### Fetch secrets
47
111
  ```
48
112
  const secrets = [
49
113
  { key: 'key1', name: 'mySecret1' } // key is the config var, name is the AWS secret name
@@ -64,10 +128,10 @@ const config = {
64
128
 
65
129
  ```
66
130
 
67
- ## Multisecrets
131
+ ### Multisecrets
68
132
  Use multisecrets if you want to add a number of additional secrets to be fetched. Usually it is used to fetch multiple objects for an array of objects:
69
133
 
70
- ### Store multisecret in AWS
134
+ #### Store multisecret in AWS
71
135
  ```
72
136
  Example secret
73
137
  // name: mySecret2
@@ -76,7 +140,7 @@ Example secret
76
140
  }
77
141
  ```
78
142
 
79
- ### Store secrets in AWS
143
+ #### Store secrets in AWS
80
144
  ```
81
145
  // name: aws.key1
82
146
  {
@@ -91,7 +155,7 @@ Example secret
91
155
  }
92
156
  ```
93
157
 
94
- ### Configure a local variable, that should be enhanced with the secret
158
+ #### Configure a local variable, that should be enhanced with the secret
95
159
  ```
96
160
  const config = {
97
161
  mySecret2: [],
@@ -101,7 +165,7 @@ const config = {
101
165
  }
102
166
  ```
103
167
 
104
- ### Fetch secrets
168
+ #### Fetch secrets
105
169
  ```
106
170
  const multisecrets = [
107
171
  { key: 'mySecret2', name: 'mySecret2' } // key is the config var, name is the AWS secret name
@@ -217,9 +281,8 @@ awsSecrets.loadSecrets(secretParams, (err, result) => {
217
281
 
218
282
  ```
219
283
 
220
- ## Links
284
+ # Links
221
285
  - [Website](https://www.admiralcloud.com/)
222
- - [Facebook](https://www.facebook.com/MediaAssetManagement/)
223
286
 
224
- ## License
287
+ # License
225
288
  [MIT License](https://opensource.org/licenses/MIT) Copyright © 2009-present, AdmiralCloud AG, Mark Poepping
@@ -0,0 +1,30 @@
1
+ const globals = require('globals');
2
+
3
+ module.exports = {
4
+ ignores: [
5
+ 'config/env/**'
6
+ ],
7
+ languageOptions: {
8
+ ecmaVersion: 2022,
9
+ sourceType: 'module',
10
+ globals: {
11
+ ...globals.commonjs,
12
+ ...globals.es6,
13
+ ...globals.node,
14
+ expect: 'readonly',
15
+ describe: 'readonly',
16
+ it: 'readonly'
17
+ }
18
+ },
19
+ rules: {
20
+ 'no-const-assign': 'error', // Ensure this rule is enabled
21
+ 'space-before-function-paren': 'off',
22
+ 'no-extra-semi': 'off',
23
+ 'object-curly-spacing': ['error', 'always'],
24
+ 'brace-style': ['error', 'stroustrup', { allowSingleLine: true }],
25
+ 'no-useless-escape': 'off',
26
+ 'standard/no-callback-literal': 'off',
27
+ 'new-cap': 'off',
28
+ 'no-console': ['warn', { allow: ['warn', 'error'] }]
29
+ }
30
+ };
package/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager')
2
+ const { SSMClient, GetParameterCommand } = require("@aws-sdk/client-ssm")
2
3
 
3
- const testConfig = require('./test/config');
4
+ const testConfig = require('./test/config')
5
+ const functionName = 'ac-awsSecrets'.padEnd(15)
4
6
 
5
7
  /**
6
8
  * Replaces configuration variables with secrets
@@ -29,13 +31,104 @@ const awsSecrets = () => {
29
31
  : setKey(obj[head], rest.join('.'), value)
30
32
  }
31
33
 
34
+ const setValue = (config, { path, value, array = false, property }) => {
35
+ // path can be from AWS parametes store (/a/b/c) or a real JSON path (a.b.c)
36
+ const keys = path.includes('/') ? path.split('/').filter(Boolean) : path.split('.')
37
+ const lastKey = keys.pop()
38
+ let pointer = config
39
+
40
+ for (const key of keys) {
41
+ if (!pointer[key]) {
42
+ pointer[key] = {}
43
+ }
44
+ pointer = pointer[key]
45
+ }
46
+
47
+ if (array) {
48
+ if (!Array.isArray(pointer[lastKey])) {
49
+ pointer[lastKey] = []
50
+ }
51
+ if (property) {
52
+ const [propKey, propValue] = Object.entries(property)[0]
53
+ const index = pointer[lastKey].findIndex(item => item[propKey] === propValue)
54
+
55
+ if (index !== -1) {
56
+ if (typeof value !== 'object' || Array.isArray(value)) {
57
+ throw new Error("Value must be an object when replacing an entry in the array.")
58
+ }
59
+ // Merge existing properties with new ones
60
+ pointer[lastKey][index] = { ...pointer[lastKey][index], ...value }
61
+ }
62
+ else {
63
+ pointer[lastKey].push(value)
64
+ }
65
+ }
66
+ else {
67
+ pointer[lastKey].push(value)
68
+ }
69
+ }
70
+ else {
71
+ pointer[lastKey] = value
72
+ }
73
+ }
74
+
75
+
76
+
77
+ const loadSecretParameters = async({ secretParameters = [], config = {}, testMode = 0, debug = false, region = 'eu-central-1' } = {}) => {
78
+ const environment = config?.environment || process.env.NODE_ENV || 'development'
79
+
80
+ const awsConfig = {
81
+ region
82
+ }
83
+ const ssmClient = new SSMClient(awsConfig)
84
+
85
+ const getSecretParameter = async({ name, json = false, array = false, path, property, debug }) => {
86
+ const parameterName = `/${environment}/${name}`
87
+ try {
88
+ let value
89
+ if (testMode === 3) {
90
+ // fetch from availableSecrets
91
+ let found = testConfig.parameterStore.find(item => item.name === parameterName)
92
+ value = found?.value
93
+ }
94
+ else {
95
+ const command = new GetParameterCommand({
96
+ Name: parameterName,
97
+ WithDecryption: true,
98
+ })
99
+
100
+ // Send the command to retrieve the parameter
101
+ const response = await ssmClient.send(command)
102
+ value = response?.Parameter?.Value
103
+ }
104
+
105
+ // Extract and return the parameter value
106
+ if (json) {
107
+ value = JSON.parse(value)
108
+ }
109
+
110
+ if (debug) {
111
+ console.warn('P %s | T %s | V %j', parameterName, typeof value, value)
112
+ }
113
+ setValue(config, { path: (path || name), value, array, property })
114
+
115
+ }
116
+ catch (e) {
117
+ console.error('%s | %s | %s', functionName, parameterName, e?.message)
118
+ }
119
+ }
120
+
121
+ for (const secretParameter of secretParameters) {
122
+ await getSecretParameter(secretParameter)
123
+ }
124
+ }
125
+
32
126
 
33
- const functionName = 'ac-awsSecrets'.padEnd(15)
34
- const loadSecrets = async({ secrets = [], multisecrets = [], config = {}, testMode = 0, debug = false } = {}) => {
127
+ const loadSecrets = async({ secrets = [], multisecrets = [], config = {}, testMode = 0, debug = false, region = 'eu-central-1' } = {}) => {
35
128
  const environment = config?.environment || 'development'
36
129
 
37
130
  const awsConfig = {
38
- region: 'eu-central-1'
131
+ region
39
132
  }
40
133
  const client = new SecretsManagerClient(awsConfig)
41
134
 
@@ -155,15 +248,16 @@ const awsSecrets = () => {
155
248
  }
156
249
 
157
250
  if (secret?.log || debug) {
158
- console.log('%s | %s | %j', functionName, secret?.name, existingValue)
251
+ console.warn('%s | %s | %j', functionName, secret?.name, existingValue)
159
252
  }
160
253
  }
161
254
  }
162
255
  }
163
256
 
164
257
  return {
258
+ loadSecretParameters,
165
259
  loadSecrets
166
260
  }
167
261
  }
168
262
 
169
- module.exports = awsSecrets()
263
+ module.exports = awsSecrets()
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": "2.1.1",
6
+ "version": "2.2.0",
7
7
  "dependencies": {
8
- "@aws-sdk/client-secrets-manager": "^3.569.0"
8
+ "@aws-sdk/client-secrets-manager": "^3.614.0",
9
+ "@aws-sdk/client-ssm": "^3.614.0"
9
10
  },
10
11
  "devDependencies": {
11
12
  "ac-semantic-release": "^0.4.2",
12
- "c8": "^9.1.0",
13
- "chai": "^4.4.1",
14
- "eslint": "8.x",
15
- "mocha": "^10.4.0"
13
+ "c8": "^10.1.2",
14
+ "chai": "^4.x",
15
+ "eslint": "9.x",
16
+ "mocha": "^10.6.0"
16
17
  },
17
18
  "scripts": {
18
- "test": "mocha --reporter spec",
19
+ "test": "NODE_ENV=test mocha --reporter spec",
19
20
  "coverage": "./node_modules/c8/bin/c8.js yarn test"
20
21
  },
21
22
  "engines": {
package/test/config.js CHANGED
@@ -25,6 +25,51 @@ const config = {
25
25
  }
26
26
  }
27
27
 
28
+
29
+ // AWS PARAMETER STORE
30
+ const secretParameters = [
31
+ { name: 'configVar1', json: true },
32
+ { name: 'configVar2', json: true, array: true, path: 'configVar2.servers', property: { server: 'cacheRead' } },
33
+ { name: 'configVar4/api', json: true },
34
+ { name: 'configVar5.path', json: true },
35
+ { name: 'configVar6', json: true },
36
+ ]
37
+
38
+ const parameterStore = [
39
+ { name: '/test/configVar1', value: JSON.stringify({ c1: true, c2: 123, c3: 'abc' }) },
40
+ {
41
+ name: '/test/configVar2',
42
+ value: JSON.stringify({
43
+ port: 6360,
44
+ host: 'myRedisHost'
45
+ })
46
+ },
47
+ {
48
+ name: '/test/configVar3',
49
+ },
50
+ {
51
+ name: '/test/configVar4/api',
52
+ value: JSON.stringify({ "url":"https://api.admiralcloud.com" })
53
+ },
54
+ {
55
+ name: '/test/errorVar1',
56
+ value: 'JSON:abc',
57
+ },
58
+ {
59
+ name: '/test/configVar5.path',
60
+ value: JSON.stringify({ cookie: true })
61
+ },
62
+ {
63
+ name: '/test/configVar6',
64
+ value: JSON.stringify({
65
+ prop1: 123,
66
+ prop2: 'abc'
67
+ })
68
+ },
69
+ ]
70
+
71
+
72
+ // AWS SECRETS
28
73
  const secrets = [
29
74
  { key: 'configVar1', name: 'simple' },
30
75
  { key: 'configVar2', name: 'server', servers: true, serverName: 'cacheRead' },
@@ -34,6 +79,8 @@ const secrets = [
34
79
  { key: 'configVar7', name: 'notExistingKey' },
35
80
  ]
36
81
 
82
+
83
+
37
84
  const availableSecrets = [{
38
85
  key: 'configVar1',
39
86
  name: 'simple',
@@ -126,6 +173,8 @@ const multisecretsFail = [
126
173
 
127
174
  module.exports = {
128
175
  config,
176
+ parameterStore,
177
+ secretParameters,
129
178
  availableSecrets,
130
179
  multisecretsFail,
131
180
  secrets,
package/test/test.js CHANGED
@@ -9,6 +9,9 @@ const multisecrets = testConfig?.multisecrets
9
9
  const config = testConfig.config
10
10
  let secrets = testConfig.secrets
11
11
 
12
+ const parameterStore = testConfig.parameterStore
13
+ let secretParameters = testConfig.secretParameters
14
+
12
15
  // HELPER for console.log checks
13
16
  const captureStream = (stream) => {
14
17
  let oldWrite = stream.write
@@ -28,6 +31,44 @@ const captureStream = (stream) => {
28
31
  }
29
32
  }
30
33
 
34
+ describe('Reading secretParameters', () => {
35
+ it('Read secretParameters', async() => {
36
+ await awsSecrets.loadSecretParameters({ secretParameters, config, testMode: 3 })
37
+ })
38
+
39
+ it('Check configVar1', async() => {
40
+ const expected = parameterStore.find(item => item.name === '/test/configVar1')
41
+ expected.value = JSON.parse(expected.value)
42
+ expect(config.configVar1).to.have.property('c1', true)
43
+ expect(config.configVar1).to.have.property('c2', expected.value.c2)
44
+ expect(config.configVar1).to.have.property('c3', expected.value.c3)
45
+
46
+ })
47
+
48
+ it('Check server', async() => {
49
+ const expected = testConfig.parameterStore.find(item => item.name === '/test/configVar2');
50
+ expected.value = JSON.parse(expected.value);
51
+ expect(config.configVar2.servers[0]).to.have.property('port', expected.value.port);
52
+ })
53
+
54
+ it('Check JSON', async() => {
55
+ expect(config.configVar4.api).to.have.property('url', 'https://api.admiralcloud.com')
56
+ })
57
+
58
+ it('Check path', async() => {
59
+ expect(config.configVar5.path).to.have.property('cookie', true)
60
+ })
61
+
62
+ it('Check non existing local config', async() => {
63
+ expect(config.configVar6).to.have.property('prop1', 123)
64
+ expect(config.configVar6).to.have.property('prop2', 'abc')
65
+ })
66
+
67
+ it('Check non existing key - should fallback to existing value without error', async() => {
68
+ expect(config.configVar7).to.have.property('level', 'info')
69
+ })
70
+ })
71
+
31
72
 
32
73
  describe('Reading secrets', () => {
33
74
  it('Read secrets', async() => {
@@ -92,7 +133,7 @@ describe('Check errors', () => {
92
133
  describe('Misc', () => {
93
134
  var hook
94
135
  beforeEach(function(){
95
- hook = captureStream(process.stdout)
136
+ hook = captureStream(process.stderr)
96
137
  })
97
138
  afterEach(function(){
98
139
  hook.unhook()
@@ -102,4 +143,4 @@ describe('Misc', () => {
102
143
  await awsSecrets.loadSecrets({ secrets, config, multisecrets, testMode: 3, debug: true })
103
144
  expect(hook.captured()).to.include('ac-awsSecrets')
104
145
  })
105
- })
146
+ })
package/.eslintrc.js DELETED
@@ -1,29 +0,0 @@
1
- const config = {
2
- root: true,
3
- 'env': {
4
- 'commonjs': true,
5
- 'es6': true,
6
- 'node': true
7
- },
8
- 'extends': 'eslint:recommended',
9
- "rules": {
10
- "space-before-function-paren": 0,
11
- "no-extra-semi": 0,
12
- "object-curly-spacing": ["error", "always"],
13
- "brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
14
- "no-useless-escape": 0,
15
- "standard/no-callback-literal": 0,
16
- "new-cap": 0
17
- },
18
- globals: {
19
- describe: true,
20
- it: true,
21
- beforeEach: true,
22
- afterEach: true
23
- },
24
- 'parserOptions': {
25
- 'ecmaVersion': 2022
26
- },
27
- }
28
-
29
- module.exports = config