Skip to content

Commit 6c9e653

Browse files
committed
move banner code to separate file with tests, narrow down resource scope, limit getParameters arguments
1 parent 578a5c2 commit 6c9e653

File tree

5 files changed

+105
-20
lines changed

5 files changed

+105
-20
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend-function/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"dependencies": {
2121
"@aws-amplify/backend-output-storage": "0.2.6",
2222
"@aws-amplify/plugin-types": "^0.5.0",
23+
"@aws-sdk/client-ssm": "^3.398.0",
2324
"execa": "^7.1.1"
2425
},
2526
"devDependencies": {

packages/backend-function/src/factory.ts

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { getCallerDirectory } from './get_caller_directory.js';
1414
import { Duration, Stack } from 'aws-cdk-lib';
1515
import { Runtime } from 'aws-cdk-lib/aws-lambda';
1616
import * as iam from 'aws-cdk-lib/aws-iam';
17+
import fs from 'fs';
18+
import os from 'os';
1719

1820
/**
1921
* Entry point for defining a function in the Amplify ecosystem
@@ -222,38 +224,43 @@ class AmplifyFunction
222224
readonly resources: FunctionResources;
223225
constructor(scope: Construct, id: string, props: HydratedFunctionProps) {
224226
super(scope, id);
227+
const envVars = {
228+
...props.environment,
229+
SECRET_PATH_ENV_VARS: process.env.SECRET_PATH_ENV_VARS,
230+
};
231+
232+
const bannerCodeFile = new URL(
233+
'./resolve_secret_banner.js',
234+
import.meta.url
235+
);
236+
const bannerCode = fs
237+
.readFileSync(bannerCodeFile, 'utf-8')
238+
.replaceAll(os.EOL, '');
239+
225240
const functionLambda = new NodejsFunction(scope, `${id}-lambda`, {
226241
entry: props.entry,
227-
environment: props.environment as { [key: string]: string }, // for some reason TS can't figure out that this is the same as Record<string, string>
242+
environment: envVars as { [key: string]: string }, // for some reason TS can't figure out that this is the same as Record<string, string>
228243
timeout: Duration.seconds(props.timeoutSeconds),
229244
memorySize: props.memoryMB,
230245
runtime: nodeVersionMap[props.runtime],
231246
bundling: {
232-
// Added '\' to the end of each line for readability here
233-
// eslint-disable-next-line spellcheck/spell-checker
234-
banner: `import { SSM } from '@aws-sdk/client-ssm'; const client = new SSM();\
235-
const envArray = ${JSON.stringify(Object.keys(props.environment))};\
236-
const response = await client.getParameters({ Names: envArray.map(a => process.env[a]), WithDecryption: true });\
237-
for (const parameter of response.Parameters) {\
238-
for (const envName of envArray) {\
239-
if (parameter.Name === process.env[envName]) {\
240-
process.env[envName.replace('_PATH', '')] = parameter.Value;\
241-
}\
242-
}\
243-
}`,
247+
banner: bannerCode,
244248
format: OutputFormat.ESM,
245249
},
246250
});
247251

252+
const resourceArns = secretPaths.map(
253+
(path) =>
254+
`arn:aws:ssm:${Stack.of(scope).region}:${
255+
Stack.of(scope).account
256+
}:parameter${path}`
257+
);
258+
248259
functionLambda.grantPrincipal.addToPrincipalPolicy(
249260
new iam.PolicyStatement({
250261
effect: iam.Effect.ALLOW,
251262
actions: ['ssm:GetParameters'],
252-
resources: [
253-
`arn:aws:ssm:${Stack.of(scope).region}:${
254-
Stack.of(scope).account
255-
}:parameter/*`,
256-
],
263+
resources: resourceArns,
257264
})
258265
);
259266

@@ -279,22 +286,28 @@ const nodeVersionMap: Record<NodeVersion, Runtime> = {
279286

280287
const secretPathSuffix = '_PATH';
281288
const secretPlaceholderText = '<value will be resolved during runtime>';
289+
const secretPaths: string[] = [];
282290

283291
const translateEnvironmentProp = (
284292
functionEnvironmentProp: HydratedFunctionProps['environment'],
285293
backendSecretResolver: BackendSecretResolver
286294
): Record<string, string> => {
287295
const result: Record<string, string> = {};
296+
const secretPathEnvVars = [];
288297

289298
for (const [key, value] of Object.entries(functionEnvironmentProp)) {
290299
if (typeof value !== 'string') {
291-
result[key + secretPathSuffix] =
292-
backendSecretResolver.resolveToPath(value);
300+
const secretPath = backendSecretResolver.resolveToPath(value);
301+
result[key + secretPathSuffix] = secretPath;
293302
result[key] = secretPlaceholderText;
303+
secretPathEnvVars.push(key + secretPathSuffix);
304+
secretPaths.push(secretPath);
294305
} else {
295306
result[key] = value;
296307
}
297308
}
298309

310+
process.env.SECRET_PATH_ENV_VARS = secretPathEnvVars.join(',');
311+
299312
return result;
300313
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { SSM } from '@aws-sdk/client-ssm';
2+
import { after, describe, it, mock } from 'node:test';
3+
import { resolveSecretBanner } from './resolve_secret_banner.js';
4+
import assert from 'node:assert';
5+
6+
void describe('resolveSecretBanner', () => {
7+
const originalEnv = process.env;
8+
const client = new SSM();
9+
10+
// reset process.env after test suite to ensure there are no side effects
11+
after(() => {
12+
process.env = originalEnv;
13+
});
14+
15+
void it('noop if there are no secret path env vars', async () => {
16+
delete process.env.SECRET_PATH_ENV_VARS;
17+
const mockGetParameters = mock.method(client, 'getParameters', mock.fn());
18+
await resolveSecretBanner(client);
19+
assert.equal(mockGetParameters.mock.callCount(), 0);
20+
});
21+
22+
void it('resolves secret and sets secret value to secret env var', async () => {
23+
const envName = 'TEST_SECRET_PATH';
24+
const secretPath = '/test/path';
25+
const secretValue = 'secretValue';
26+
process.env[envName] = secretPath;
27+
process.env.SECRET_PATH_ENV_VARS = envName;
28+
const mockGetParameters = mock.method(client, 'getParameters', () =>
29+
Promise.resolve({
30+
Parameters: [
31+
{
32+
Name: secretPath,
33+
Value: secretValue,
34+
},
35+
],
36+
})
37+
);
38+
await resolveSecretBanner(client);
39+
assert.equal(mockGetParameters.mock.callCount(), 1);
40+
assert.equal(process.env[envName.replace('_PATH', '')], secretValue);
41+
});
42+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { SSM } from '@aws-sdk/client-ssm';
2+
3+
const ssmClient = new SSM();
4+
5+
/**
6+
* The body of this function will be used to resolve secrets for Lambda functions
7+
*/
8+
export const resolveSecretBanner = async (client: SSM = ssmClient) => {
9+
const envArray = process.env.SECRET_PATH_ENV_VARS?.split(',');
10+
if (envArray) {
11+
const response = await client.getParameters({
12+
Names: envArray.map((a) => process.env[a] ?? ''),
13+
WithDecryption: true,
14+
});
15+
16+
if (response.Parameters && response.Parameters?.length > 0) {
17+
for (const parameter of response.Parameters) {
18+
for (const envName of envArray) {
19+
if (parameter.Name === process.env[envName]) {
20+
process.env[envName.replace('_PATH', '')] = parameter.Value;
21+
}
22+
}
23+
}
24+
}
25+
}
26+
};
27+
28+
await resolveSecretBanner();

0 commit comments

Comments
 (0)