From bb50c04f5b2e6a144295b453577a7ea1a15ac011 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 22 Feb 2023 18:01:32 +0100 Subject: [PATCH] fix(parameters): handle base64/binaries in transformer (#1326) --- packages/parameters/src/BaseProvider.ts | 16 +-- ...pConfigProvider.class.test.functionCode.ts | 30 +++-- .../tests/e2e/appConfigProvider.class.test.ts | 103 ++++++------------ .../tests/unit/BaseProvider.test.ts | 10 +- .../tests/unit/getAppConfig.test.ts | 8 +- 5 files changed, 63 insertions(+), 104 deletions(-) diff --git a/packages/parameters/src/BaseProvider.ts b/packages/parameters/src/BaseProvider.ts index 3e4b2a1474..b2fc4efc1c 100644 --- a/packages/parameters/src/BaseProvider.ts +++ b/packages/parameters/src/BaseProvider.ts @@ -131,6 +131,11 @@ abstract class BaseProvider implements BaseProviderInterface { const transformValue = (value: string | Uint8Array | undefined, transform: TransformOptions, throwOnTransformError: boolean, key: string): string | Record | undefined => { try { const normalizedTransform = transform.toLowerCase(); + + if (value instanceof Uint8Array) { + value = new TextDecoder('utf-8').decode(value); + } + if ( (normalizedTransform === TRANSFORM_METHOD_JSON || (normalizedTransform === 'auto' && key.toLowerCase().endsWith(`.${TRANSFORM_METHOD_JSON}`))) && @@ -139,15 +144,12 @@ const transformValue = (value: string | Uint8Array | undefined, transform: Trans return JSON.parse(value) as Record; } else if ( (normalizedTransform === TRANSFORM_METHOD_BINARY || - (normalizedTransform === 'auto' && key.toLowerCase().endsWith(`.${TRANSFORM_METHOD_BINARY}`))) + (normalizedTransform === 'auto' && key.toLowerCase().endsWith(`.${TRANSFORM_METHOD_BINARY}`))) && + typeof value === 'string' ) { - if (typeof value === 'string') { - return new TextDecoder('utf-8').decode(fromBase64(value)); - } else { - return new TextDecoder('utf-8').decode(value); - } + return new TextDecoder('utf-8').decode(fromBase64(value)); } else { - return value as string; + return value; } } catch (error) { if (throwOnTransformError) diff --git a/packages/parameters/tests/e2e/appConfigProvider.class.test.functionCode.ts b/packages/parameters/tests/e2e/appConfigProvider.class.test.functionCode.ts index 589fd475ed..11d8feb01f 100644 --- a/packages/parameters/tests/e2e/appConfigProvider.class.test.functionCode.ts +++ b/packages/parameters/tests/e2e/appConfigProvider.class.test.functionCode.ts @@ -16,8 +16,7 @@ const application = process.env.APPLICATION_NAME || 'my-app'; const environment = process.env.ENVIRONMENT_NAME || 'my-env'; const freeFormJsonName = process.env.FREEFORM_JSON_NAME || 'freeform-json'; const freeFormYamlName = process.env.FREEFORM_YAML_NAME || 'freeform-yaml'; -const freeFormPlainTextNameA = process.env.FREEFORM_PLAIN_TEXT_NAME_A || 'freeform-plain-text'; -const freeFormPlainTextNameB = process.env.FREEFORM_PLAIN_TEXT_NAME_B || 'freeform-plain-text'; +const freeFormBase64encodedPlainText = process.env.FREEFORM_BASE64_ENCODED_PLAIN_TEXT_NAME || 'freeform-plain-text'; const featureFlagName = process.env.FEATURE_FLAG_NAME || 'feature-flag'; const defaultProvider = new AppConfigProvider({ @@ -65,28 +64,25 @@ const _call_get = async ( }; export const handler = async (_event: unknown, _context: Context): Promise => { - // Test 1 - get a single parameter as-is (no transformation) - await _call_get(freeFormPlainTextNameA, 'get'); + // Test 1 - get a single parameter as-is (no transformation - should return an Uint8Array) + await _call_get(freeFormYamlName, 'get'); - // Test 2 - get a free-form JSON and apply binary transformation (should return a stringified JSON) - await _call_get(freeFormJsonName, 'get-freeform-json-binary', { transform: 'binary' }); + // Test 2 - get a free-form JSON and apply json transformation (should return an object) + await _call_get(freeFormJsonName, 'get-freeform-json-binary', { transform: 'json' }); - // Test 3 - get a free-form YAML and apply binary transformation (should return a string-encoded YAML) - await _call_get(freeFormYamlName, 'get-freeform-yaml-binary', { transform: 'binary' }); + // Test 3 - get a free-form base64-encoded plain text and apply binary transformation (should return a decoded string) + await _call_get(freeFormBase64encodedPlainText, 'get-freeform-base64-plaintext-binary', { transform: 'binary' }); - // Test 4 - get a free-form plain text and apply binary transformation (should return a string) - await _call_get(freeFormPlainTextNameB, 'get-freeform-plain-text-binary', { transform: 'binary' }); - - // Test 5 - get a feature flag and apply binary transformation (should return a stringified JSON) - await _call_get(featureFlagName, 'get-feature-flag-binary', { transform: 'binary' }); + // Test 5 - get a feature flag and apply json transformation (should return an object) + await _call_get(featureFlagName, 'get-feature-flag-binary', { transform: 'json' }); // Test 6 // get parameter twice with middleware, which counts the number of requests, we check later if we only called AppConfig API once try { providerWithMiddleware.clearCache(); middleware.counter = 0; - await providerWithMiddleware.get(freeFormPlainTextNameA); - await providerWithMiddleware.get(freeFormPlainTextNameA); + await providerWithMiddleware.get(freeFormBase64encodedPlainText); + await providerWithMiddleware.get(freeFormBase64encodedPlainText); logger.log({ test: 'get-cached', value: middleware.counter // should be 1 @@ -103,8 +99,8 @@ export const handler = async (_event: unknown, _context: Context): Promise try { providerWithMiddleware.clearCache(); middleware.counter = 0; - await providerWithMiddleware.get(freeFormPlainTextNameA); - await providerWithMiddleware.get(freeFormPlainTextNameA, { forceFetch: true }); + await providerWithMiddleware.get(freeFormBase64encodedPlainText); + await providerWithMiddleware.get(freeFormBase64encodedPlainText, { forceFetch: true }); logger.log({ test: 'get-forced', value: middleware.counter // should be 2 diff --git a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts index b32504444a..02b2de3291 100644 --- a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts +++ b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts @@ -5,6 +5,7 @@ */ import path from 'path'; import { App, Stack, Aspects } from 'aws-cdk-lib'; +import { toBase64 } from '@aws-sdk/util-base64-node'; import { v4 } from 'uuid'; import { generateUniqueName, @@ -44,8 +45,7 @@ const environmentName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, const deploymentStrategyName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'immediate'); const freeFormJsonName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'freeFormJson'); const freeFormYamlName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'freeFormYaml'); -const freeFormPlainTextNameA = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'freeFormPlainTextA'); -const freeFormPlainTextNameB = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'freeFormPlainTextB'); +const freeFormBase64PlainTextName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'freeFormBase64PlainText'); const featureFlagName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'featureFlag'); const freeFormJsonValue = { @@ -85,7 +85,7 @@ let stack: Stack; * The parameters created are: * - Free-form JSON * - Free-form YAML - * - 2x Free-form plain text + * - Free-form plain text base64-encoded string * - Feature flag * * These parameters allow to retrieve the values and test some transformations. @@ -93,25 +93,22 @@ let stack: Stack; * The tests are: * * Test 1 - * get a single parameter as-is (no transformation) + * get a single parameter as-is (no transformation - should return an Uint8Array) * * Test 2 - * get a free-form JSON and apply binary transformation (should return a stringified JSON) + * get a free-form JSON and apply json transformation (should return an object) * * Test 3 - * get a free-form YAML and apply binary transformation (should return a string-encoded YAML) + * get a free-form base64-encoded plain text and apply binary transformation (should return a decoded string) * * Test 4 - * get a free-form plain text and apply binary transformation (should return a string) + * get a feature flag and apply json transformation (should return an object) * * Test 5 - * get a feature flag and apply binary transformation (should return a stringified JSON) - * - * Test 6 * get parameter twice with middleware, which counts the number of requests, * we check later if we only called AppConfig API once * - * Test 7 + * Test 6 * get parameter twice, but force fetch 2nd time, we count number of SDK requests and * check that we made two API calls * @@ -140,8 +137,7 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = ENVIRONMENT_NAME: environmentName, FREEFORM_JSON_NAME: freeFormJsonName, FREEFORM_YAML_NAME: freeFormYamlName, - FREEFORM_PLAIN_TEXT_NAME_A: freeFormPlainTextNameA, - FREEFORM_PLAIN_TEXT_NAME_B: freeFormPlainTextNameB, + FREEFORM_BASE64_ENCODED_PLAIN_TEXT_NAME: freeFormBase64PlainTextName, FEATURE_FLAG_NAME: featureFlagName, }, runtime, @@ -187,33 +183,19 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = }); freeFormYaml.node.addDependency(freeFormJson); - const freeFormPlainTextA = createAppConfigConfigurationProfile({ + const freeFormBase64PlainText = createAppConfigConfigurationProfile({ stack, application, environment, deploymentStrategy, - name: freeFormPlainTextNameA, + name: freeFormBase64PlainTextName, type: 'AWS.Freeform', content: { - content: freeFormPlainTextValue, + content: toBase64(new TextEncoder().encode(freeFormPlainTextValue)), contentType: 'text/plain', } }); - freeFormPlainTextA.node.addDependency(freeFormYaml); - - const freeFormPlainTextB = createAppConfigConfigurationProfile({ - stack, - application, - environment, - deploymentStrategy, - name: freeFormPlainTextNameB, - type: 'AWS.Freeform', - content: { - content: freeFormPlainTextValue, - contentType: 'text/plain', - } - }); - freeFormPlainTextB.node.addDependency(freeFormPlainTextA); + freeFormBase64PlainText.node.addDependency(freeFormYaml); const featureFlag = createAppConfigConfigurationProfile({ stack, @@ -227,14 +209,13 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = contentType: 'application/json', } }); - featureFlag.node.addDependency(freeFormPlainTextB); + featureFlag.node.addDependency(freeFormBase64PlainText); // Grant access to the Lambda function to the AppConfig resources. Aspects.of(stack).add(new ResourceAccessGranter([ freeFormJson, freeFormYaml, - freeFormPlainTextA, - freeFormPlainTextB, + freeFormBase64PlainText, featureFlag, ])); @@ -248,8 +229,8 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = describe('AppConfigProvider usage', () => { - // Test 1 - get a single parameter as-is (no transformation) - it('should retrieve single parameter', () => { + // Test 1 - get a single parameter as-is (no transformation - should return an Uint8Array) + it('should retrieve single parameter as-is', () => { const logs = invocationLogs[0].getFunctionLogs(); const testLog = InvocationLogs.parseFunctionLog(logs[0]); @@ -258,75 +239,59 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = test: 'get', value: JSON.parse( JSON.stringify( - encoder.encode(freeFormPlainTextValue) + encoder.encode(freeFormYamlValue) ) ), }); }); - // Test 2 - get a free-form JSON and apply binary transformation - // (should return a stringified JSON) - it('should retrieve single free-form JSON parameter with binary transformation', () => { + // Test 2 - get a free-form JSON and apply json transformation (should return an object) + it('should retrieve a free-form JSON parameter with JSON transformation', () => { const logs = invocationLogs[0].getFunctionLogs(); const testLog = InvocationLogs.parseFunctionLog(logs[1]); expect(testLog).toStrictEqual({ test: 'get-freeform-json-binary', - value: JSON.stringify(freeFormJsonValue), + value: freeFormJsonValue, }); }); - // Test 3 - get a free-form YAML and apply binary transformation - // (should return a string-encoded YAML) - it('should retrieve single free-form YAML parameter with binary transformation', () => { + // Test 3 - get a free-form base64-encoded plain text and apply binary transformation + // (should return a decoded string) + it('should retrieve a base64-encoded plain text parameter with binary transformation', () => { const logs = invocationLogs[0].getFunctionLogs(); const testLog = InvocationLogs.parseFunctionLog(logs[2]); expect(testLog).toStrictEqual({ - test: 'get-freeform-yaml-binary', - value: freeFormYamlValue, - }); - - }); - - // Test 4 - get a free-form plain text and apply binary transformation - // (should return a string) - it('should retrieve single free-form plain text parameter with binary transformation', () => { - - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[3]); - - expect(testLog).toStrictEqual({ - test: 'get-freeform-plain-text-binary', + test: 'get-freeform-base64-plaintext-binary', value: freeFormPlainTextValue, }); }); - // Test 5 - get a feature flag and apply binary transformation - // (should return a stringified JSON) - it('should retrieve single feature flag parameter with binary transformation', () => { + // Test 4 - get a feature flag and apply json transformation (should return an object) + it('should retrieve a feature flag parameter with JSON transformation', () => { const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[4]); + const testLog = InvocationLogs.parseFunctionLog(logs[3]); expect(testLog).toStrictEqual({ test: 'get-feature-flag-binary', - value: JSON.stringify(featureFlagValue.values), + value: featureFlagValue.values, }); }); - - // Test 6 - get parameter twice with middleware, which counts the number + + // Test 5 - get parameter twice with middleware, which counts the number // of requests, we check later if we only called AppConfig API once it('should retrieve single parameter cached', () => { const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[5]); + const testLog = InvocationLogs.parseFunctionLog(logs[4]); expect(testLog).toStrictEqual({ test: 'get-cached', @@ -335,12 +300,12 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = }, TEST_CASE_TIMEOUT); - // Test 7 - get parameter twice, but force fetch 2nd time, + // Test 6 - 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[6]); + const testLog = InvocationLogs.parseFunctionLog(logs[5]); expect(testLog).toStrictEqual({ test: 'get-forced', diff --git a/packages/parameters/tests/unit/BaseProvider.test.ts b/packages/parameters/tests/unit/BaseProvider.test.ts index 3b8f029d64..186e0c1cdc 100644 --- a/packages/parameters/tests/unit/BaseProvider.test.ts +++ b/packages/parameters/tests/unit/BaseProvider.test.ts @@ -206,19 +206,15 @@ describe('Class: BaseProvider', () => { }); - test('when called with a binary transform, and the value is a valid binary, it returns the decoded value', async () => { + test('when called with a binary transform, and the value is a valid binary but NOT base64 encoded, it throws', async () => { // Prepare const mockData = encoder.encode('my-value'); const provider = new TestProvider(); jest.spyOn(provider, '_get').mockImplementation(() => new Promise((resolve, _reject) => resolve(mockData as unknown as string))); - // Act - const value = await provider.get('my-parameter', { transform: 'binary' }); - - // Assess - expect(typeof value).toBe('string'); - expect(value).toEqual('my-value'); + // Act & Assess + await expect(provider.get('my-parameter', { transform: 'binary' })).rejects.toThrowError(TransformParameterError); }); diff --git a/packages/parameters/tests/unit/getAppConfig.test.ts b/packages/parameters/tests/unit/getAppConfig.test.ts index af03a8ec32..e729d23dfd 100644 --- a/packages/parameters/tests/unit/getAppConfig.test.ts +++ b/packages/parameters/tests/unit/getAppConfig.test.ts @@ -16,11 +16,11 @@ import { import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; import type { GetAppConfigCombinedInterface } from '../../src/types/AppConfigProvider'; +import { toBase64 } from '@aws-sdk/util-base64-node'; describe('Function: getAppConfig', () => { const client = mockClient(AppConfigDataClient); const encoder = new TextEncoder(); - const decoder = new TextDecoder(); beforeEach(() => { jest.clearAllMocks(); @@ -103,8 +103,8 @@ describe('Function: getAppConfig', () => { 'AYADeNgfsRxdKiJ37A12OZ9vN2cAXwABABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREF1RzlLMTg1Tkx2Wjk4OGV2UXkyQ1'; const mockNextToken = 'ImRmyljpZnxt7FfxeEOE5H8xQF1SfOlWZFnHujbzJmIvNeSAAA8/qA9ivK0ElRMwpvx96damGxt125XtMkmYf6a0OWSqnBw=='; - const mockData = encoder.encode('myAppConfiguration'); - const decodedData = decoder.decode(mockData); + const expectedValue = 'my-value'; + const mockData = encoder.encode(toBase64(encoder.encode(expectedValue))); client .on(StartConfigurationSessionCommand) @@ -121,6 +121,6 @@ describe('Function: getAppConfig', () => { const result = await getAppConfig(name, options); // Assess - expect(result).toBe(decodedData); + expect(result).toBe(expectedValue); }); });