From 107a06c4a23555f0420b8ddaf4c114931fa60279 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 18 Jan 2023 11:05:31 +0100 Subject: [PATCH 1/8] chore: update cdkAspect to include SSM --- .../parameters/tests/helpers/cdkAspectGrantAccess.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts b/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts index 57d99a393b..414b95182b 100644 --- a/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts +++ b/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts @@ -3,6 +3,7 @@ import { IConstruct } from 'constructs'; import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; import { Table } from 'aws-cdk-lib/aws-dynamodb'; import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import { StringParameter } from 'aws-cdk-lib/aws-ssm'; /** * An aspect that grants access to resources to a Lambda function. @@ -19,9 +20,9 @@ import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; * @see {@link https://docs.aws.amazon.com/cdk/v2/guide/aspects.html|CDK Docs - Aspects} */ export class ResourceAccessGranter implements IAspect { - private readonly resources: Table[] | Secret[]; + private readonly resources: Table[] | Secret[] | StringParameter[]; - public constructor(resources: Table[] | Secret[]) { + public constructor(resources: Table[] | Secret[] | StringParameter[]) { this.resources = resources; } @@ -30,11 +31,14 @@ export class ResourceAccessGranter implements IAspect { if (node instanceof NodejsFunction) { // Grant access to the resources - this.resources.forEach((resource: Table | Secret) => { + this.resources.forEach((resource: Table | Secret | StringParameter) => { if (resource instanceof Table) { resource.grantReadData(node); - } else if (resource instanceof Secret) { + } else if ( + resource instanceof Secret || + resource instanceof StringParameter + ) { resource.grantRead(node); } From e1318c897367a7967b8ca17a64e685971c6ad0fb Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 18 Jan 2023 20:50:48 +0100 Subject: [PATCH 2/8] tests: SSMProvider class usage --- packages/parameters/src/ssm/SSMProvider.ts | 21 +- .../tests/e2e/dynamoDBProvider.class.test.ts | 8 +- .../ssmProvider.class.test.functionCode.ts | 135 ++++++++++ .../tests/e2e/ssmProvider.class.test.ts | 250 ++++++++++++++++++ .../tests/helpers/cdkAspectGrantAccess.ts | 32 ++- .../tests/helpers/parametersUtils.ts | 81 +++++- .../tests/helpers/ssmSecureStringCdk.ts | 55 ++++ 7 files changed, 556 insertions(+), 26 deletions(-) create mode 100644 packages/parameters/tests/e2e/ssmProvider.class.test.functionCode.ts create mode 100644 packages/parameters/tests/e2e/ssmProvider.class.test.ts create mode 100644 packages/parameters/tests/helpers/ssmSecureStringCdk.ts diff --git a/packages/parameters/src/ssm/SSMProvider.ts b/packages/parameters/src/ssm/SSMProvider.ts index 39cc0ee569..ae5b357075 100644 --- a/packages/parameters/src/ssm/SSMProvider.ts +++ b/packages/parameters/src/ssm/SSMProvider.ts @@ -144,13 +144,12 @@ class SSMProvider extends BaseProvider { options?: SSMGetOptionsInterface ): Promise { const sdkOptions: GetParameterCommandInput = { + ...(options?.sdkOptions || {}), Name: name, }; if (options) { - if (options.hasOwnProperty('decrypt')) sdkOptions.WithDecryption = options.decrypt; - if (options.hasOwnProperty('sdkOptions')) { - Object.assign(sdkOptions, options.sdkOptions); - } + if (options.hasOwnProperty('decrypt')) + sdkOptions.WithDecryption = options.decrypt; } const result = await this.client.send(new GetParameterCommand(sdkOptions)); @@ -162,19 +161,19 @@ class SSMProvider extends BaseProvider { options?: SSMGetMultipleOptionsInterface ): Promise> { const sdkOptions: GetParametersByPathCommandInput = { + ...(options?.sdkOptions || {}), Path: path, }; const paginationOptions: PaginationConfiguration = { client: this.client }; if (options) { - if (options.hasOwnProperty('decrypt')) sdkOptions.WithDecryption = options.decrypt; - if (options.hasOwnProperty('recursive')) sdkOptions.Recursive = options.recursive; - if (options.hasOwnProperty('sdkOptions')) { - Object.assign(sdkOptions, options.sdkOptions); - if (sdkOptions.MaxResults) { - paginationOptions.pageSize = sdkOptions.MaxResults; - } + if (options.hasOwnProperty('decrypt')) + sdkOptions.WithDecryption = options.decrypt; + if (options.hasOwnProperty('recursive')) + sdkOptions.Recursive = options.recursive; + if (sdkOptions.MaxResults) { + paginationOptions.pageSize = sdkOptions.MaxResults; } } diff --git a/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts b/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts index ce8f7f50d4..dc8d782e3a 100644 --- a/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts +++ b/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts @@ -4,7 +4,6 @@ * @group e2e/parameters/dynamodb/class */ import path from 'path'; -import { Tracing } from 'aws-cdk-lib/aws-lambda'; import { AttributeType } from 'aws-cdk-lib/aws-dynamodb'; import { App, Stack, Aspects } from 'aws-cdk-lib'; import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb'; @@ -138,10 +137,9 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: ${runtime}`, () = // Create a stack with a Lambda function stack = createStackWithLambdaFunction({ app: integTestApp, - stackName: stackName, - functionName: functionName, + stackName, + functionName, functionEntry: path.join(__dirname, lambdaFunctionCodeFile), - tracing: Tracing.ACTIVE, environment: { UUID: uuid, @@ -154,7 +152,7 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: ${runtime}`, () = SORT_ATTR: sortAttr, VALUE_ATTR: valueAttr, }, - runtime: runtime, + runtime, }); // Create the DynamoDB tables diff --git a/packages/parameters/tests/e2e/ssmProvider.class.test.functionCode.ts b/packages/parameters/tests/e2e/ssmProvider.class.test.functionCode.ts new file mode 100644 index 0000000000..e387cfc615 --- /dev/null +++ b/packages/parameters/tests/e2e/ssmProvider.class.test.functionCode.ts @@ -0,0 +1,135 @@ +import { Context } from 'aws-lambda'; +import { SSMProvider } from '../../src/ssm'; +import { TinyLogger } from '../helpers/tinyLogger'; +// # TODO: Uncomment code below once #1222 is fixed +/* +import { middleware } from '../helpers/sdkMiddlewareRequestCounter'; +import { SSMClient } from '@aws-sdk/client-ssm'; +*/ + +const paramA = process.env.PARAM_A ?? 'my-param'; +const paramB = process.env.PARAM_B ?? 'my-param'; +const paramEncryptedA = process.env.PARAM_ENCRYPTED_A ?? 'my-encrypted-param'; +const paramEncryptedB = process.env.PARAM_ENCRYPTED_B ?? 'my-encrypted-param'; + +// We use a custom logger to log pure JSON objects to stdout +const logger = new TinyLogger(); + +// Provider test 1 +const providerGet = new SSMProvider(); + +export const handler = async (_event: unknown, _context: Context): Promise => { + // Test 1 - get a single parameter with default options + try { + const parameterValue = await providerGet.get(paramA); + logger.log({ + test: 'get', + value: parameterValue + }); + } catch (err) { + logger.log({ + test: 'get', + error: err.message + }); + } + + // Test 2 - get a single parameter with decrypt + try { + const parameterValue = await providerGet.get(paramEncryptedA, { decrypt: true }); + logger.log({ + test: 'get-decrypt', + value: parameterValue + }); + } catch (err) { + logger.log({ + test: 'get-decrypt', + error: err.message + }); + } + + // Test 3 - get multiple parameters with default options + try { + // Get path (/param/get) + const parameterPath = paramA.substring(0, paramA.lastIndexOf('/')); + const parameterValues = await providerGet.getMultiple(parameterPath); + logger.log({ + test: 'get-multiple', + value: parameterValues + }); + } catch (err) { + logger.log({ + test: 'get-multiple', + error: err.message + }); + } + + // Test 4 - get multiple parameters with recursive (aka. get all parameters under a path recursively) + try { + // Get parameters root (i.e. from /param/get/a & /param/get/b to /param) + const parameterRoot = paramA.substring( + 0, + paramA.substring(1, paramA.length).indexOf('/') + 1 + ); + const parameterValues = await providerGet.getMultiple(parameterRoot, { recursive: true }); + logger.log({ + test: 'get-multiple-recursive', + value: parameterValues + }); + } catch (err) { + logger.log({ + test: 'get-multiple-recursive', + error: err.message + }); + } + + // Test 5 - get multiple parameters with decrypt + try { + // Get parameters path (i.e. from /param/get/a & /param/get/b to /param/get) + const parameterPath = paramEncryptedA.substring(0, paramEncryptedA.lastIndexOf('/')); + const parameterValues = await providerGet.getMultiple(parameterPath, { decrypt: true }); + logger.log({ + test: 'get-multiple-decrypt', + value: parameterValues + }); + } catch (err) { + logger.log({ + test: 'get-multiple-decrypt', + error: err.message + }); + } + + // Test 6 - get multiple parameters by name with default options + try { + const parameterValues = await providerGet.getParametersByName({ + [paramA]: {}, + [paramB]: {}, + }); + logger.log({ + test: 'get-multiple-by-name', + value: parameterValues + }); + } catch (err) { + logger.log({ + test: 'get-multiple-by-name', + error: err.message + }); + } + + // Test 7 - get multiple parameters by name with mixed decrypt + try { + const parameterValues = await providerGet.getParametersByName({ + [paramA]: {}, + [paramEncryptedA]: { decrypt: true }, + [paramEncryptedB]: { decrypt: true }, + }); + logger.log({ + test: 'get-multiple-by-name-mixed-decrypt', + value: parameterValues + }); + } catch (err) { + logger.log({ + test: 'get-multiple-by-name-mixed-decrypt', + error: err.message + }); + } +}; \ No newline at end of file diff --git a/packages/parameters/tests/e2e/ssmProvider.class.test.ts b/packages/parameters/tests/e2e/ssmProvider.class.test.ts new file mode 100644 index 0000000000..7fc54d6fb1 --- /dev/null +++ b/packages/parameters/tests/e2e/ssmProvider.class.test.ts @@ -0,0 +1,250 @@ +/** + * Test SSMProvider class + * + * @group e2e/parameters/ssm/class + */ +import path from 'path'; +import { App, Stack, Aspects } from 'aws-cdk-lib'; +import { StringParameter } from 'aws-cdk-lib/aws-ssm'; +import { v4 } from 'uuid'; +import { + generateUniqueName, + isValidRuntimeKey, + createStackWithLambdaFunction, + invokeFunction, +} from '../../../commons/tests/utils/e2eUtils'; +import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; +import { deployStack, destroyStack } from '../../../commons/tests/utils/cdk-cli'; +import { ResourceAccessGranter } from '../helpers/cdkAspectGrantAccess'; +import { + RESOURCE_NAME_PREFIX, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + TEST_CASE_TIMEOUT +} from './constants'; +import { + createSecureStringProvider, + createSSMSecureString +} from '../helpers/parametersUtils'; + +const runtime: string = process.env.RUNTIME || 'nodejs18x'; + +if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); +} + +const uuid = v4(); +const stackName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'ssmProvider'); +const functionName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'ssmProvider'); +const lambdaFunctionCodeFile = 'ssmProvider.class.test.functionCode.ts'; + +const invocationCount = 1; + +// Parameter names to be used by Parameters in the Lambda function +const paramA = generateUniqueName(`/${RESOURCE_NAME_PREFIX}`, uuid, runtime, 'param/a'); +const paramB = generateUniqueName(`/${RESOURCE_NAME_PREFIX}`, uuid, runtime, 'param/b'); +const paramEncryptedA = generateUniqueName(`/${RESOURCE_NAME_PREFIX}`, uuid, runtime, 'param-encrypted/a'); +const paramEncryptedB = generateUniqueName(`/${RESOURCE_NAME_PREFIX}`, uuid, runtime, 'param-encrypted/b'); + +// Parameters values +const paramAValue = 'foo'; +const paramBValue = 'bar'; +const paramEncryptedAValue = 'foo-encrypted'; +const paramEncryptedBValue = 'bar-encrypted'; + +const integTestApp = new App(); +let stack: Stack; + +describe(`parameters E2E tests (dynamoDBProvider) for runtime: nodejs18x`, () => { + + let invocationLogs: InvocationLogs[]; + + beforeAll(async () => { + // Create a stack with a Lambda function + stack = createStackWithLambdaFunction({ + app: integTestApp, + stackName, + functionName, + functionEntry: path.join(__dirname, lambdaFunctionCodeFile), + environment: { + UUID: uuid, + + // Values(s) to be used by Parameters in the Lambda function + PARAM_A: paramA, + PARAM_B: paramB, + PARAM_ENCRYPTED_A: paramEncryptedA, + PARAM_ENCRYPTED_B: paramEncryptedB, + }, + runtime, + }); + + // Create Custom Resource provider: + // will be used to create some SSM parameters not supported by CDK + const provider = createSecureStringProvider({ + stack, + parametersPrefix: `${RESOURCE_NAME_PREFIX}-${runtime}-${uuid.substring(0,5)}` + }); + + // Create SSM parameters + const parameterGetA = new StringParameter(stack, 'Param-a', { + parameterName: paramA, + stringValue: paramAValue, + }); + const parameterGetB = new StringParameter(stack, 'Param-b', { + parameterName: paramB, + stringValue: paramBValue, + }); + + const parameterEncryptedA = createSSMSecureString({ + stack, + provider, + id: 'Param-encrypted-a', + name: paramEncryptedA, + value: paramEncryptedAValue, + }); + + const parameterEncryptedB = createSSMSecureString({ + stack, + provider, + id: 'Param-encrypted-b', + name: paramEncryptedB, + value: paramEncryptedBValue, + }); + + // Give the Lambda function access to the SSM parameters + Aspects.of(stack).add(new ResourceAccessGranter([ + parameterGetA, + parameterGetB, + parameterEncryptedA, + parameterEncryptedB, + ])); + + // Deploy the stack + await deployStack(integTestApp, stack); + + // and invoke the Lambda function + invocationLogs = await invokeFunction(functionName, invocationCount, 'SEQUENTIAL'); + + }, SETUP_TIMEOUT); + + describe('SSMProvider usage', () => { + + it('should retrieve a single parameter', async () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[0]); + + expect(testLog).toStrictEqual({ + test: 'get', + value: paramAValue + }); + + }, TEST_CASE_TIMEOUT); + + it('should retrieve a single parameter with decryption', async () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[1]); + + expect(testLog).toStrictEqual({ + test: 'get-decrypt', + value: paramEncryptedAValue + }); + + }, TEST_CASE_TIMEOUT); + + it('should retrieve multiple parameters', async () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[2]); + const expectedParameterNameA = paramA.substring(paramA.lastIndexOf('/') + 1); + const expectedParameterNameB = paramB.substring(paramB.lastIndexOf('/') + 1); + + expect(testLog).toStrictEqual({ + test: 'get-multiple', + value: { + [expectedParameterNameA]: paramAValue, + [expectedParameterNameB]: paramBValue, + } + }); + + }, TEST_CASE_TIMEOUT); + + it('should retrieve multiple parameters recursively', async () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[3]); + const expectedParameterNameA = paramA.substring(paramA.lastIndexOf('/') + 1); + const expectedParameterNameB = paramB.substring(paramB.lastIndexOf('/') + 1); + + expect(testLog).toStrictEqual({ + test: 'get-multiple-recursive', + value: { + [expectedParameterNameA]: paramAValue, + [expectedParameterNameB]: paramBValue, + } + }); + + }, TEST_CASE_TIMEOUT); + + it('should retrieve multiple parameters with decryption', async () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[4]); + const expectedParameterNameA = paramEncryptedA.substring( + paramEncryptedA.lastIndexOf('/') + 1 + ); + const expectedParameterNameB = paramEncryptedB.substring( + paramEncryptedB.lastIndexOf('/') + 1 + ); + + expect(testLog).toStrictEqual({ + test: 'get-multiple-decrypt', + value: { + [expectedParameterNameA]: paramEncryptedAValue, + [expectedParameterNameB]: paramEncryptedBValue, + } + }); + + }, TEST_CASE_TIMEOUT); + + it('should retrieve multiple parameters by name', async () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[5]); + + expect(testLog).toStrictEqual({ + test: 'get-multiple-by-name', + value: { + [paramA]: paramAValue, + [paramB]: paramBValue, + } + }); + + }, TEST_CASE_TIMEOUT); + + it('should retrieve multiple parameters by name with mixed decryption', async () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[6]); + + expect(testLog).toStrictEqual({ + test: 'get-multiple-by-name-mixed-decrypt', + value: { + [paramEncryptedA]: paramEncryptedAValue, + [paramEncryptedB]: paramEncryptedBValue, + [paramA]: paramAValue, + } + }); + + }, TEST_CASE_TIMEOUT); + + }); + + afterAll(async () => { + if (!process.env.DISABLE_TEARDOWN) { + await destroyStack(integTestApp, stack); + } + }, TEARDOWN_TIMEOUT); + +}); \ No newline at end of file diff --git a/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts b/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts index 414b95182b..cde9f8312f 100644 --- a/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts +++ b/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts @@ -2,8 +2,12 @@ import { IAspect } from 'aws-cdk-lib'; import { IConstruct } from 'constructs'; import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; import { Table } from 'aws-cdk-lib/aws-dynamodb'; +import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; -import { StringParameter } from 'aws-cdk-lib/aws-ssm'; +import { StringParameter, IStringParameter } from 'aws-cdk-lib/aws-ssm'; + +const isStringParameterGeneric = (parameter: IConstruct): parameter is StringParameter | IStringParameter => + parameter.hasOwnProperty('parameterArn'); /** * An aspect that grants access to resources to a Lambda function. @@ -16,13 +20,13 @@ import { StringParameter } from 'aws-cdk-lib/aws-ssm'; * This aspect allows us to grant access to specific resources to all Lambda functions in a stack * after the stack tree has been generated and before the stack is deployed. This aspect is * used to grant access to different resource types (DynamoDB tables, SSM parameters, etc.). - * - * @see {@link https://docs.aws.amazon.com/cdk/v2/guide/aspects.html|CDK Docs - Aspects} + * + * @see {@link https://docs.aws.amazon.com/cdk/v2/guide/aspects.html CDK Docs - Aspects} */ export class ResourceAccessGranter implements IAspect { - private readonly resources: Table[] | Secret[] | StringParameter[]; + private readonly resources: Table[] | Secret[] | StringParameter[] | IStringParameter[]; - public constructor(resources: Table[] | Secret[] | StringParameter[]) { + public constructor(resources: Table[] | Secret[] | StringParameter[] | IStringParameter[]) { this.resources = resources; } @@ -31,15 +35,27 @@ export class ResourceAccessGranter implements IAspect { if (node instanceof NodejsFunction) { // Grant access to the resources - this.resources.forEach((resource: Table | Secret | StringParameter) => { + this.resources.forEach((resource: Table | Secret | StringParameter | IStringParameter) => { if (resource instanceof Table) { resource.grantReadData(node); } else if ( - resource instanceof Secret || - resource instanceof StringParameter + resource instanceof Secret ) { resource.grantRead(node); + } else if (isStringParameterGeneric(resource)) { + resource.grantRead(node); + // Grant access also to the path of the parameter + node.addToRolePolicy( + new PolicyStatement({ + actions: [ + 'ssm:GetParametersByPath', + ], + resources: [ + resource.parameterArn.split(':').slice(0, -1).join(':'), + ], + }), + ); } }); diff --git a/packages/parameters/tests/helpers/parametersUtils.ts b/packages/parameters/tests/helpers/parametersUtils.ts index 3768a1fe53..088f3b6ee1 100644 --- a/packages/parameters/tests/helpers/parametersUtils.ts +++ b/packages/parameters/tests/helpers/parametersUtils.ts @@ -1,4 +1,11 @@ -import { Stack, RemovalPolicy } from 'aws-cdk-lib'; +import { Stack, RemovalPolicy, CustomResource, Duration } from 'aws-cdk-lib'; +import { Provider } from 'aws-cdk-lib/custom-resources'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { Runtime } from 'aws-cdk-lib/aws-lambda'; +import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { Key } from 'aws-cdk-lib/aws-kms'; +import { StringParameter, IStringParameter } from 'aws-cdk-lib/aws-ssm'; import { Table, TableProps, BillingMode } from 'aws-cdk-lib/aws-dynamodb'; export type CreateDynamoDBTableOptions = { @@ -17,6 +24,76 @@ const createDynamoDBTable = (options: CreateDynamoDBTableOptions): Table => { return new Table(stack, id, props); }; +export type CreateSecureStringProviderOptions = { + stack: Stack + parametersPrefix: string +}; + +const createSecureStringProvider = (options: CreateSecureStringProviderOptions): Provider => { + const { stack, parametersPrefix } = options; + + const ssmSecureStringHandlerFn = new NodejsFunction( + stack, + 'ssm-securestring-handler', + { + entry: 'tests/helpers/ssmSecureStringCdk.ts', + handler: 'handler', + bundling: { + minify: true, + sourceMap: true, + target: 'es2020', + externalModules: [], + }, + runtime: Runtime.NODEJS_18_X, + timeout: Duration.seconds(15), + }); + ssmSecureStringHandlerFn.addToRolePolicy( + new PolicyStatement({ + actions: [ + 'ssm:PutParameter', + 'ssm:DeleteParameter', + ], + resources: [ + `arn:aws:ssm:${stack.region}:${stack.account}:parameter/${parametersPrefix}*`, + ], + }), + ); + + return new Provider(stack, 'ssm-secure-string-provider', { + onEventHandler: ssmSecureStringHandlerFn, + logRetention: RetentionDays.ONE_DAY, + }); +}; + +export type CreateSSMSecureStringOptions = { + stack: Stack + provider: Provider + id: string + name: string + value: string +}; + +const createSSMSecureString = (options: CreateSSMSecureStringOptions): IStringParameter => { + const { stack, provider, id, name, value } = options; + + new CustomResource(stack, `custom-${id}`, { + serviceToken: provider.serviceToken, + properties: { + Name: name, + Value: value, + }, + }); + + const param = StringParameter.fromSecureStringParameterAttributes(stack, id, { + parameterName: name, + }); + param.node.addDependency(provider); + + return param; +}; + export { - createDynamoDBTable + createDynamoDBTable, + createSSMSecureString, + createSecureStringProvider, }; \ No newline at end of file diff --git a/packages/parameters/tests/helpers/ssmSecureStringCdk.ts b/packages/parameters/tests/helpers/ssmSecureStringCdk.ts new file mode 100644 index 0000000000..c2a497c774 --- /dev/null +++ b/packages/parameters/tests/helpers/ssmSecureStringCdk.ts @@ -0,0 +1,55 @@ +import { + Context, + CloudFormationCustomResourceEvent +} from 'aws-lambda'; +import { + SSMClient, + PutParameterCommand, + DeleteParameterCommand +} from '@aws-sdk/client-ssm'; + +const client = new SSMClient({}); + +/** + * Create a new SSM SecureString parameter, overwriting any existing parameter with the same name if it exists. + */ +const createResource = async (event: CloudFormationCustomResourceEvent): Promise => { + const { ResourceProperties } = event; + const { Name, Value } = ResourceProperties; + + await client.send(new PutParameterCommand({ + Name, + Value, + Type: 'SecureString', + Overwrite: true, + })); +}; + +/** + * Delete an existing SSM parameter. + */ +const deleteResource = async (event: CloudFormationCustomResourceEvent): Promise => { + const { ResourceProperties } = event; + const { Name } = ResourceProperties; + + await client.send(new DeleteParameterCommand({ + Name, + })); +}; + +/** + * Custom resource handler for creating and deleting SSM SecureString parameters. This is used by + * CDK to create and delete the SSM SecureString parameters that are used to test the SSMProvider. + * + * We need a custom resource because CDK does not support creating SSM SecureString parameters. + */ +export const handler = async (event: CloudFormationCustomResourceEvent, _context: Context): Promise => { + if (event.RequestType === 'Create' || event.RequestType === 'Update') { + await createResource(event); + } else if (event.RequestType === 'Delete') { + await deleteResource(event); + } else { + console.error('Unknown request type', event); + throw new Error('Unknown request type'); + } +}; \ No newline at end of file From 7f5f93446fafff24d3786ffa2d2ca6492d2d257e Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 19 Jan 2023 16:22:07 +0100 Subject: [PATCH 3/8] chore: removed unused import --- packages/parameters/tests/helpers/parametersUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/parameters/tests/helpers/parametersUtils.ts b/packages/parameters/tests/helpers/parametersUtils.ts index 088f3b6ee1..e503fddd2b 100644 --- a/packages/parameters/tests/helpers/parametersUtils.ts +++ b/packages/parameters/tests/helpers/parametersUtils.ts @@ -4,7 +4,6 @@ import { RetentionDays } from 'aws-cdk-lib/aws-logs'; import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; import { Runtime } from 'aws-cdk-lib/aws-lambda'; import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; -import { Key } from 'aws-cdk-lib/aws-kms'; import { StringParameter, IStringParameter } from 'aws-cdk-lib/aws-ssm'; import { Table, TableProps, BillingMode } from 'aws-cdk-lib/aws-dynamodb'; From 4440039141e11a4136e9b269ae942f8bad158c1d Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 8 Feb 2023 14:45:56 +0100 Subject: [PATCH 4/8] tests: completed tests --- packages/parameters/src/ssm/index.ts | 3 +- .../ssmProvider.class.test.functionCode.ts | 196 +++++++++++------- .../tests/e2e/ssmProvider.class.test.ts | 85 ++++++++ 3 files changed, 208 insertions(+), 76 deletions(-) diff --git a/packages/parameters/src/ssm/index.ts b/packages/parameters/src/ssm/index.ts index 329d3d3f3b..e275be6d78 100644 --- a/packages/parameters/src/ssm/index.ts +++ b/packages/parameters/src/ssm/index.ts @@ -1,5 +1,4 @@ export * from './SSMProvider'; export * from './getParameter'; export * from './getParameters'; -export * from './getParametersByName'; -export * from '../types/SSMProvider'; \ No newline at end of file +export * from './getParametersByName'; \ No newline at end of file diff --git a/packages/parameters/tests/e2e/ssmProvider.class.test.functionCode.ts b/packages/parameters/tests/e2e/ssmProvider.class.test.functionCode.ts index e387cfc615..4f0280f0a8 100644 --- a/packages/parameters/tests/e2e/ssmProvider.class.test.functionCode.ts +++ b/packages/parameters/tests/e2e/ssmProvider.class.test.functionCode.ts @@ -1,134 +1,182 @@ import { Context } from 'aws-lambda'; -import { SSMProvider } from '../../src/ssm'; +import { + SSMProvider, +} from '../../src/ssm'; +import { + SSMGetOptionsInterface, + SSMGetMultipleOptionsInterface, + SSMGetParametersByNameOptionsInterface +} from '../../src/types'; import { TinyLogger } from '../helpers/tinyLogger'; -// # TODO: Uncomment code below once #1222 is fixed -/* import { middleware } from '../helpers/sdkMiddlewareRequestCounter'; import { SSMClient } from '@aws-sdk/client-ssm'; -*/ + +// We use a custom logger to log pure JSON objects to stdout +const logger = new TinyLogger(); + +const defaultProvider = new SSMProvider(); +// Provider test 8, 9 +const customClient = new SSMClient({}); +customClient.middlewareStack.use(middleware); +const providerWithMiddleware = new SSMProvider({ + awsSdkV3Client: customClient +}); const paramA = process.env.PARAM_A ?? 'my-param'; const paramB = process.env.PARAM_B ?? 'my-param'; const paramEncryptedA = process.env.PARAM_ENCRYPTED_A ?? 'my-encrypted-param'; const paramEncryptedB = process.env.PARAM_ENCRYPTED_B ?? 'my-encrypted-param'; -// We use a custom logger to log pure JSON objects to stdout -const logger = new TinyLogger(); +// Use provider specified, or default to main one & return it with cache cleared +const resolveProvider = (provider?: SSMProvider): SSMProvider => { + const resolvedProvider = provider ? provider : defaultProvider; + resolvedProvider.clearCache(); -// Provider test 1 -const providerGet = new SSMProvider(); + return resolvedProvider; +}; -export const handler = async (_event: unknown, _context: Context): Promise => { - // Test 1 - get a single parameter with default options - try { - const parameterValue = await providerGet.get(paramA); - logger.log({ - test: 'get', - value: parameterValue - }); - } catch (err) { - logger.log({ - test: 'get', - error: err.message - }); - } - - // Test 2 - get a single parameter with decrypt +// Helper function to call get() and log the result +const _call_get = async ( + paramName: string, + testName: string, + options?: SSMGetOptionsInterface, + provider?: SSMProvider +): Promise => { try { - const parameterValue = await providerGet.get(paramEncryptedA, { decrypt: true }); + const currentProvider = resolveProvider(provider); + + const parameterValue = await currentProvider.get(paramName, options); logger.log({ - test: 'get-decrypt', + test: testName, value: parameterValue }); } catch (err) { logger.log({ - test: 'get-decrypt', - error: err.message - }); - } - - // Test 3 - get multiple parameters with default options - try { - // Get path (/param/get) - const parameterPath = paramA.substring(0, paramA.lastIndexOf('/')); - const parameterValues = await providerGet.getMultiple(parameterPath); - logger.log({ - test: 'get-multiple', - value: parameterValues - }); - } catch (err) { - logger.log({ - test: 'get-multiple', + test: testName, error: err.message }); } - - // Test 4 - get multiple parameters with recursive (aka. get all parameters under a path recursively) +}; + +// Helper function to call getMultiple() and log the result +const _call_get_multiple = async ( + paramPath: string, + testName: string, + options?: SSMGetMultipleOptionsInterface, + provider?: SSMProvider +): Promise => { try { - // Get parameters root (i.e. from /param/get/a & /param/get/b to /param) - const parameterRoot = paramA.substring( - 0, - paramA.substring(1, paramA.length).indexOf('/') + 1 + const currentProvider = resolveProvider(provider); + + const parameterValues = await currentProvider.getMultiple( + paramPath, + options ); - const parameterValues = await providerGet.getMultiple(parameterRoot, { recursive: true }); logger.log({ - test: 'get-multiple-recursive', + test: testName, value: parameterValues }); } catch (err) { logger.log({ - test: 'get-multiple-recursive', + test: testName, error: err.message }); } +}; - // Test 5 - get multiple parameters with decrypt +// Helper function to call getParametersByName() and log the result +const _call_get_parameters_by_name = async ( + params: Record, + testName: string, + options?: SSMGetParametersByNameOptionsInterface, + provider?: SSMProvider +): Promise => { try { - // Get parameters path (i.e. from /param/get/a & /param/get/b to /param/get) - const parameterPath = paramEncryptedA.substring(0, paramEncryptedA.lastIndexOf('/')); - const parameterValues = await providerGet.getMultiple(parameterPath, { decrypt: true }); + const currentProvider = resolveProvider(provider); + + const parameterValues = await currentProvider.getParametersByName(params, options); logger.log({ - test: 'get-multiple-decrypt', + test: testName, value: parameterValues }); } catch (err) { logger.log({ - test: 'get-multiple-decrypt', + test: testName, error: err.message }); } +}; + +export const handler = async (_event: unknown, _context: Context): Promise => { + // Test 1 - get a single parameter by name with default options + await _call_get(paramA, 'get'); + + // Test 2 - get a single parameter by name with decrypt + await _call_get(paramEncryptedA, 'get-decrypt', { decrypt: true }); + + // Test 3 - get multiple parameters by path with default options + // Get path (/param/get) + const parameterPath = paramA.substring(0, paramA.lastIndexOf('/')); + await _call_get_multiple(parameterPath, 'get-multiple'); + + // Test 4 - get multiple parameters by path recursively (aka. get all parameters under a path recursively) + // Get parameters root (i.e. from /param/get/a & /param/get/b to /param) + const parameterRoot = paramA.substring( + 0, + paramA.substring(1, paramA.length).indexOf('/') + 1 + ); + await _call_get_multiple(parameterRoot, 'get-multiple-recursive', { recursive: true }); + + // Test 5 - get multiple parameters by path with decrypt + // Get parameters path (i.e. from /param/get/a & /param/get/b to /param/get) + const parameterPathDecrypt = paramEncryptedA.substring(0, paramEncryptedA.lastIndexOf('/')); + await _call_get_multiple(parameterPathDecrypt, 'get-multiple-decrypt', { decrypt: true }); // Test 6 - get multiple parameters by name with default options + await _call_get_parameters_by_name({ + [paramA]: {}, + [paramB]: {}, + }, 'get-multiple-by-name'); + + // Test 7 - get multiple parameters by name, some of them encrypted and some not + await _call_get_parameters_by_name({ + [paramA]: {}, + [paramEncryptedA]: { decrypt: true }, + [paramEncryptedB]: { decrypt: true }, + }, 'get-multiple-by-name-mixed-decrypt'); + + // Test 8 + // get parameter twice with middleware, which counts the number of requests, we check later if we only called SSM API once try { - const parameterValues = await providerGet.getParametersByName({ - [paramA]: {}, - [paramB]: {}, - }); + providerWithMiddleware.clearCache(); + middleware.counter = 0; + await providerWithMiddleware.get(paramA); + await providerWithMiddleware.get(paramA); logger.log({ - test: 'get-multiple-by-name', - value: parameterValues + test: 'get-cached', + value: middleware.counter // should be 1 }); } catch (err) { logger.log({ - test: 'get-multiple-by-name', + test: 'get-cached', error: err.message }); } - // Test 7 - get multiple parameters by name with mixed decrypt + // Test 9 + // get parameter twice, but force fetch 2nd time, we count number of SDK requests and check that we made two API calls try { - const parameterValues = await providerGet.getParametersByName({ - [paramA]: {}, - [paramEncryptedA]: { decrypt: true }, - [paramEncryptedB]: { decrypt: true }, - }); + providerWithMiddleware.clearCache(); + middleware.counter = 0; + await providerWithMiddleware.get(paramA); + await providerWithMiddleware.get(paramA, { forceFetch: true }); logger.log({ - test: 'get-multiple-by-name-mixed-decrypt', - value: parameterValues + test: 'get-forced', + value: middleware.counter // should be 2 }); } catch (err) { logger.log({ - test: 'get-multiple-by-name-mixed-decrypt', + test: 'get-forced', error: err.message }); } diff --git a/packages/parameters/tests/e2e/ssmProvider.class.test.ts b/packages/parameters/tests/e2e/ssmProvider.class.test.ts index 7fc54d6fb1..4dc00b212d 100644 --- a/packages/parameters/tests/e2e/ssmProvider.class.test.ts +++ b/packages/parameters/tests/e2e/ssmProvider.class.test.ts @@ -55,6 +55,55 @@ const paramEncryptedBValue = 'bar-encrypted'; const integTestApp = new App(); let stack: Stack; +/** + * This test suite deploys a CDK stack with a Lambda function and a number of SSM parameters. + * The function code uses the Parameters utility to retrieve the SSM parameters. + * It then logs the values to CloudWatch Logs as JSON objects. + * + * Once the stack is deployed, the Lambda function is invoked and the CloudWatch Logs are retrieved. + * The logs are then parsed and the values are checked against the expected values for each test case. + * + * The parameters created are: + * - Name: param/a - Value: foo + * - Name: param/b - Value: bar + * - Name: param-encrypted/a - Value: foo-encrypted + * - Name: param-encrypted/b - Value: bar-encrypted + * + * These parameters allow to retrieve one or more parameters both by name and by path, as well as + * mixing encrypted and unencrypted parameters. + * + * The tests are: + * + * Test 1 + * get a single parameter by name with default options + * + * Test 2 + * get a single parameter by name with decrypt + * + * Test 3 + * get multiple parameters by path with default options + * + * Test 4 + * get multiple parameters by path recursively (aka. get all parameters under a path recursively) + * i.e. given /param, retrieve /param/get/a and /param/get/b (note path depth) + * + * Test 5 + * get multiple parameters by path with decrypt + * + * Test 6 + * get multiple parameters by name with default options + * + * Test 7 + * get multiple parameters by name, some of them encrypted and some not + * + * Test 8 + * get parameter twice with middleware, which counts the number of requests, + * we check later if we only called SSM API once + * + * Test 9 + * get parameter twice, but force fetch 2nd time, we count number of SDK requests and + * check that we made two API calls + */ describe(`parameters E2E tests (dynamoDBProvider) for runtime: nodejs18x`, () => { let invocationLogs: InvocationLogs[]; @@ -129,6 +178,7 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: nodejs18x`, () => describe('SSMProvider usage', () => { + // Test 1 - get a single parameter by name with default options it('should retrieve a single parameter', async () => { const logs = invocationLogs[0].getFunctionLogs(); @@ -141,6 +191,7 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: nodejs18x`, () => }, TEST_CASE_TIMEOUT); + // Test 2 - get a single parameter by name with decrypt it('should retrieve a single parameter with decryption', async () => { const logs = invocationLogs[0].getFunctionLogs(); @@ -153,6 +204,7 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: nodejs18x`, () => }, TEST_CASE_TIMEOUT); + // Test 3 - get multiple parameters by path with default options it('should retrieve multiple parameters', async () => { const logs = invocationLogs[0].getFunctionLogs(); @@ -170,6 +222,9 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: nodejs18x`, () => }, TEST_CASE_TIMEOUT); + // Test 4 - get multiple parameters by path recursively + // (aka. get all parameters under a path recursively) i.e. + // given /param, retrieve /param/get/a and /param/get/b (note path depth) it('should retrieve multiple parameters recursively', async () => { const logs = invocationLogs[0].getFunctionLogs(); @@ -208,6 +263,7 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: nodejs18x`, () => }, TEST_CASE_TIMEOUT); + // Test 6 - get multiple parameters by name with default options it('should retrieve multiple parameters by name', async () => { const logs = invocationLogs[0].getFunctionLogs(); @@ -223,6 +279,7 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: nodejs18x`, () => }, TEST_CASE_TIMEOUT); + // Test 7 - get multiple parameters by name, some of them encrypted and some not it('should retrieve multiple parameters by name with mixed decryption', async () => { const logs = invocationLogs[0].getFunctionLogs(); @@ -239,6 +296,34 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: nodejs18x`, () => }, TEST_CASE_TIMEOUT); + // Test 8 - get parameter twice with middleware, which counts the number + // of requests, we check later if we only called SSM API once + it('should retrieve single parameter cached', async () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[7]); + + expect(testLog).toStrictEqual({ + test: 'get-cached', + value: 1 + }); + + }, TEST_CASE_TIMEOUT); + + // Test 9 - get parameter twice, but force fetch 2nd time, + // we count number of SDK requests and check that we made two API calls + it('should retrieve single parameter twice without caching', async () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[8]); + + expect(testLog).toStrictEqual({ + test: 'get-forced', + value: 2 + }); + + }); + }); afterAll(async () => { From e15dc7be3c7d0af420325244cc4f5219a4029da8 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 14 Feb 2023 09:41:26 +0100 Subject: [PATCH 5/8] tests: fixed typos --- packages/parameters/tests/e2e/ssmProvider.class.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/parameters/tests/e2e/ssmProvider.class.test.ts b/packages/parameters/tests/e2e/ssmProvider.class.test.ts index 4dc00b212d..51701649e5 100644 --- a/packages/parameters/tests/e2e/ssmProvider.class.test.ts +++ b/packages/parameters/tests/e2e/ssmProvider.class.test.ts @@ -104,7 +104,7 @@ let stack: Stack; * get parameter twice, but force fetch 2nd time, we count number of SDK requests and * check that we made two API calls */ -describe(`parameters E2E tests (dynamoDBProvider) for runtime: nodejs18x`, () => { +describe(`parameters E2E tests (ssmProvider) for runtime: ${runtime}`, () => { let invocationLogs: InvocationLogs[]; @@ -322,7 +322,7 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: nodejs18x`, () => value: 2 }); - }); + }, TEST_CASE_TIMEOUT); }); From 97a59badbe765c2a4da6c040bcb0cd3c92154935 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 16 Feb 2023 13:56:21 +0100 Subject: [PATCH 6/8] chore: added missing effect in cdkAspect --- packages/parameters/tests/helpers/cdkAspectGrantAccess.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts b/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts index cde9f8312f..f75848acd5 100644 --- a/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts +++ b/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts @@ -2,7 +2,7 @@ import { IAspect } from 'aws-cdk-lib'; import { IConstruct } from 'constructs'; import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; import { Table } from 'aws-cdk-lib/aws-dynamodb'; -import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { PolicyStatement, Effect } from 'aws-cdk-lib/aws-iam'; import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; import { StringParameter, IStringParameter } from 'aws-cdk-lib/aws-ssm'; @@ -48,6 +48,7 @@ export class ResourceAccessGranter implements IAspect { // Grant access also to the path of the parameter node.addToRolePolicy( new PolicyStatement({ + effect: Effect.ALLOW, actions: [ 'ssm:GetParametersByPath', ], From bd25df0a209b110150de59b1d8617a7e66c8bbb8 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 16 Feb 2023 13:57:41 +0100 Subject: [PATCH 7/8] chore: reduced scope of ssmSecureString custom resource --- packages/parameters/tests/helpers/ssmSecureStringCdk.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/parameters/tests/helpers/ssmSecureStringCdk.ts b/packages/parameters/tests/helpers/ssmSecureStringCdk.ts index c2a497c774..2076c25c47 100644 --- a/packages/parameters/tests/helpers/ssmSecureStringCdk.ts +++ b/packages/parameters/tests/helpers/ssmSecureStringCdk.ts @@ -11,7 +11,7 @@ import { const client = new SSMClient({}); /** - * Create a new SSM SecureString parameter, overwriting any existing parameter with the same name if it exists. + * Create a new SSM SecureString parameter. */ const createResource = async (event: CloudFormationCustomResourceEvent): Promise => { const { ResourceProperties } = event; @@ -21,7 +21,6 @@ const createResource = async (event: CloudFormationCustomResourceEvent): Promise Name, Value, Type: 'SecureString', - Overwrite: true, })); }; @@ -44,12 +43,12 @@ const deleteResource = async (event: CloudFormationCustomResourceEvent): Promise * We need a custom resource because CDK does not support creating SSM SecureString parameters. */ export const handler = async (event: CloudFormationCustomResourceEvent, _context: Context): Promise => { - if (event.RequestType === 'Create' || event.RequestType === 'Update') { + if (event.RequestType === 'Create') { await createResource(event); } else if (event.RequestType === 'Delete') { await deleteResource(event); } else { - console.error('Unknown request type', event); - throw new Error('Unknown request type'); + console.error('Unknown or unsupported request type', event); + throw new Error('Unknown or unsupported request type'); } }; \ No newline at end of file From da22d85729c2a055c39194baf24dec04fddf3041 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 16 Feb 2023 17:12:13 +0100 Subject: [PATCH 8/8] refactor: options setting logic --- packages/parameters/src/ssm/SSMProvider.ts | 33 +++++++++------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/packages/parameters/src/ssm/SSMProvider.ts b/packages/parameters/src/ssm/SSMProvider.ts index ae5b357075..dd45070162 100644 --- a/packages/parameters/src/ssm/SSMProvider.ts +++ b/packages/parameters/src/ssm/SSMProvider.ts @@ -147,10 +147,8 @@ class SSMProvider extends BaseProvider { ...(options?.sdkOptions || {}), Name: name, }; - if (options) { - if (options.hasOwnProperty('decrypt')) - sdkOptions.WithDecryption = options.decrypt; - } + sdkOptions.WithDecryption = options?.decrypt !== undefined ? + options.decrypt : sdkOptions.WithDecryption; const result = await this.client.send(new GetParameterCommand(sdkOptions)); return result.Parameter?.Value; @@ -167,15 +165,12 @@ class SSMProvider extends BaseProvider { const paginationOptions: PaginationConfiguration = { client: this.client }; - if (options) { - if (options.hasOwnProperty('decrypt')) - sdkOptions.WithDecryption = options.decrypt; - if (options.hasOwnProperty('recursive')) - sdkOptions.Recursive = options.recursive; - if (sdkOptions.MaxResults) { - paginationOptions.pageSize = sdkOptions.MaxResults; - } - } + sdkOptions.WithDecryption = options?.decrypt !== undefined ? + options.decrypt : sdkOptions.WithDecryption; + sdkOptions.Recursive = options?.recursive !== undefined ? + options.recursive : sdkOptions.Recursive; + paginationOptions.pageSize = sdkOptions.MaxResults !== undefined ? + sdkOptions.MaxResults : undefined; const parameters: Record = {}; for await (const page of paginateGetParametersByPath(paginationOptions, sdkOptions)) { @@ -388,13 +383,11 @@ class SSMProvider extends BaseProvider { const overrides = parameterOptions; overrides.transform = overrides.transform || configs.transform; - if (!overrides.hasOwnProperty('decrypt')) { - overrides.decrypt = configs.decrypt; - } - if (!overrides.hasOwnProperty('maxAge')) { - overrides.maxAge = configs.maxAge; - } - + overrides.decrypt = overrides.decrypt !== undefined ? + overrides.decrypt : configs.decrypt; + overrides.maxAge = overrides.maxAge !== undefined ? + overrides.maxAge : configs.maxAge; + if (overrides.decrypt) { parametersToDecrypt[parameterName] = overrides; } else {