Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(custom-resources): IAM policy incorrect for SDKv3 names #27313

Merged
merged 21 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion nx.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"outputs": [
"{projectRoot}/**/*.integ.*.js.snapshot/*",
"{projectRoot}/tsconfig.json",
"{projectRoot}/**/lib/aws-custom-resource/sdk-api-metadata.json",
"{projectRoot}/**/lib/aws-custom-resource/sdk-v2-to-v3.json",
"{projectRoot}/**/lib/aws-custom-resource/sdk-v3-metadata.json",
"{projectRoot}/**/build-info.json",
"{projectRoot}/**/*.js",
"{projectRoot}/**/*.js.map",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@types/node": "18.11.19",
"@types/prettier": "2.6.0",
"@yarnpkg/lockfile": "^1.1.0",
"aws-sdk-js-codemod": "^0.18.3",
"cdk-generate-synthetic-examples": "^0.1.291",
"conventional-changelog-cli": "^2.2.2",
"fs-extra": "^9.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-console */
// eslint-disable-next-line import/no-extraneous-dependencies
import { coerceApiParameters, getV3Client, getV3Command, loadV3ClientPackage } from '@aws-cdk/sdk-v2-to-v3-adapter';
import { ApiCall } from '@aws-cdk/sdk-v2-to-v3-adapter';

interface AwsApiInput {
readonly service: string;
Expand All @@ -15,15 +15,15 @@ interface AwsApiInput {
export async function handler(event: AwsApiInput) {
console.log('Event: %j', { ...event, ResponseURL: '...' });

const awsSdk = loadV3ClientPackage(event.service);
console.log(`AWS SDK V3: ${awsSdk.packageName}@${awsSdk.packageVersion}`);

const client = getV3Client(awsSdk, { apiVersion: event.apiVersion });
const Command = getV3Command(awsSdk, event.action);
const commandInput = coerceApiParameters(event.service, event.action, event.parameters);
const apiCall = new ApiCall(event.service, event.action);
console.log(`AWS SDK V3: ${apiCall.v3PackageName}`);

try {
const response = await client.send(new Command(commandInput));
const response = await apiCall.invoke({
parameters: event.parameters ?? {},
apiVersion: event.apiVersion,
});

delete response.$metadata;
console.log('Response: %j', response);
} catch (error: any) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { join } from 'path';
/* eslint-disable-next-line import/no-extraneous-dependencies,import/no-unresolved */
import * as AWSLambda from 'aws-lambda';
import { AwsSdkCall } from './construct-types';
import { decodeCall, decodeSpecialValues, filterKeys, flatten, respond, startsWithOneOf } from './shared';
import { decodeCall, decodeSpecialValues, filterKeys, respond, startsWithOneOf } from './shared';
// eslint-disable-next-line import/no-extraneous-dependencies
import { flatten } from '@aws-cdk/sdk-v2-to-v3-adapter';

let latestSdkInstalled = false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
/* eslint-disable no-console */
import { execSync } from 'child_process';
// eslint-disable-next-line import/no-extraneous-dependencies
import { coerceApiParameters, findV3ClientConstructor, getV3ClientPackageName } from '@aws-cdk/sdk-v2-to-v3-adapter';
import { ApiCall } from '@aws-cdk/sdk-v2-to-v3-adapter';
// import the AWSLambda package explicitly,
// which is globally available in the Lambda runtime,
// as otherwise linking this repository with link-all.sh
// fails in the CDK app executed with ts-node
/* eslint-disable-next-line import/no-extraneous-dependencies,import/no-unresolved */
import type * as AWSLambda from 'aws-lambda';
import type { AwsSdkCall } from './construct-types';
import { decodeCall, decodeSpecialValues, filterKeys, flatten, respond, startsWithOneOf } from './shared';
import { decodeCall, decodeSpecialValues, filterKeys, respond, startsWithOneOf } from './shared';

let installedSdk: { [service: string]: boolean } = {};

Expand Down Expand Up @@ -89,14 +89,9 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
}
const call: AwsSdkCall | undefined = event.ResourceProperties[event.RequestType];
if (call) {
// when provide v2 service name, transform it v3 package name.
const packageName = call.service.startsWith('@aws-sdk/') ? call.service : getV3ClientPackageName(call.service);
const serviceShortName = packageName.split('/client-')[1]; // '@aws-sdk/client-s3' -> 's3'
const apiCall = new ApiCall(call.service, call.action);

let awsSdk: AwsSdk | Promise<AwsSdk> = loadAwsSdk(
packageName,
event.ResourceProperties.InstallLatestAwsSdk,
);
let awsSdk: AwsSdk | Promise<AwsSdk> = loadAwsSdk(apiCall.v3PackageName, event.ResourceProperties.InstallLatestAwsSdk);

console.log(JSON.stringify({ ...event, ResponseURL: '...' }));

Expand All @@ -109,41 +104,29 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
RoleSessionName: `${timestamp}-${physicalResourceId}`.substring(0, 64),
};

const { fromTemporaryCredentials } = await import('@aws-sdk/credential-providers' as string);
const { fromTemporaryCredentials } = await import('@aws-sdk/credential-providers');
credentials = fromTemporaryCredentials({
params,
clientConfig: call.region !== undefined ? { region: call.region } : undefined,
});
}

awsSdk = await awsSdk;
const ServiceClient = findV3ClientConstructor(awsSdk);

const client = new ServiceClient({
apiVersion: call.apiVersion,
credentials: credentials,
region: call.region,
});
const commandName = call.action.endsWith('Command') ? call.action : `${call.action}Command`;
const shortCommandName = commandName.replace(/Command$/, ''); // 'PutObjectCommand' -> 'PutObject'
const Command = Object.entries(awsSdk).find(
([name]) => name.toLowerCase() === commandName.toLowerCase(),
)?.[1] as { new (input: any): any };

let flatData: { [key: string]: string } = {};

const flatData: { [key: string]: string } = {};
try {
// Command must pass input value https://github.com/aws/aws-sdk-js-v3/issues/424
const response = await client.send(
new Command(call.parameters
? coerceApiParameters(serviceShortName, shortCommandName, decodeSpecialValues(call.parameters, physicalResourceId))
: {},
),
);
flatData = {
apiVersion: client.config.apiVersion, // For test purposes: check if apiVersion was correctly passed.
region: await client.config.region().catch(() => undefined), // For test purposes: check if region was correctly passed.
...flatten(response),
};
const response = await await apiCall.invoke({
sdkPackage: awsSdk,
apiVersion: call.apiVersion,
credentials: credentials,
region: call.region,
parameters: decodeSpecialValues(call.parameters, physicalResourceId),
flattenResponse: true,
});

flatData.apiVersion = apiCall.client.config.apiVersion; // For test purposes: check if apiVersion was correctly passed.
flatData.region = await apiCall.client.config.region().catch(() => undefined); // For test purposes: check if region was correctly passed.
Object.assign(flatData, response);

let outputPaths: string[] | undefined;
if (call.outputPath) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,6 @@ import * as AWSLambda from 'aws-lambda';
*/
export const PHYSICAL_RESOURCE_ID_REFERENCE = 'PHYSICAL:RESOURCEID:';

/**
* Text decoder used for Uint8Array response parsing
*/
const decoder = new TextDecoder();

/**
* Parse both buffers and ArrayBuffers which can be returned by sdkv3
*/
function parseField(value: any): any {
if (Buffer.isBuffer(value)) {
return value.toString('utf8');
} else if (ArrayBuffer.isView(value)) {
return decoder.decode(value.buffer);
}

return value;
}
/**
* Flattens a nested object
*
* @param object the object to be flattened
* @returns a flat object with path as keys
*/
export function flatten(object: object): { [key: string]: any } {
function _flatten(child: any, path: string[] = []): any {
return [].concat(...Object.keys(child)
.map(key => {
const childKey = parseField(child[key]);
return typeof childKey === 'object' && childKey !== null
? _flatten(childKey, path.concat([key]))
: ({ [path.concat([key]).join('.')]: childKey });
}));
}
return Object.assign(
{},
..._flatten(object),
);
}

/**
* Decodes encoded special values (physicalResourceId)
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,98 +1,21 @@
/* eslint-disable no-console */
import { CustomResourceHandler } from './base';
import { AwsApiCallRequest, AwsApiCallResult } from './types';
import {
getV3ClientPackageName,
findV3ClientConstructor,
coerceApiParameters,
} from '@aws-cdk/sdk-v2-to-v3-adapter';
import { decodeParameters, coerceResponse } from './utils';

/**
* Flattens a nested object
*
* @param object the object to be flattened
* @returns a flat object with path as keys
*/
export function flatten(object: object): { [key: string]: any } {
return Object.assign(
{},
...function _flatten(child: any, path: string[] = []): any {
return [].concat(...Object.keys(child)
.map(key => {
let childKey = Buffer.isBuffer(child[key]) ? child[key].toString('utf8') : child[key];
// if the value is a json string then treat it as an object
// and keep recursing. This allows for easier assertions against complex json strings
if (typeof childKey === 'string') {
childKey = isJsonString(childKey);
}
return typeof childKey === 'object' && childKey !== null
? _flatten(childKey, path.concat([key]))
: ({ [path.concat([key]).join('.')]: childKey });
}));
}(object),
);
}

interface V3SdkPkg {
service: string;
packageName: string;
pkg: object;
}

function getServicePackage(service: string): V3SdkPkg {
const packageName = getV3ClientPackageName(service);
try {
/* eslint-disable-next-line @typescript-eslint/no-require-imports */
const pkg = require(packageName);

return {
service,
pkg,
packageName,
};
} catch (e) {
throw Error(`Service ${service} client package with name '${packageName}' does not exist.`);
}
}

function getServiceClient(sdkPkg: V3SdkPkg): any {
try {
const ServiceClient = findV3ClientConstructor(sdkPkg.pkg);
return new ServiceClient({});
} catch (e) {
console.error(e);
throw Error(`No client constructor found within package: ${sdkPkg.packageName}`);
}
}

function getSdkCommand(sdkPkg: V3SdkPkg, api: string): any {
const commandName = api.endsWith('Command') ? api : `${api}Command`;
const command = Object.entries(sdkPkg.pkg).find(
([name]) => name.toLowerCase() === commandName.toLowerCase(),
)?.[1] as { new (input: any): any };

if (!command) {
throw new Error(`Unable to find command named: ${commandName} for api: ${api} in service package`);
}
return command;
}
import { ApiCall, flatten } from '@aws-cdk/sdk-v2-to-v3-adapter';
import { decodeParameters, deepParseJson } from './utils';

export class AwsApiCallHandler extends CustomResourceHandler<AwsApiCallRequest, AwsApiCallResult | { [key: string]: string }> {
protected async processEvent(request: AwsApiCallRequest): Promise<AwsApiCallResult | { [key: string]: string } | undefined> {
const sdkPkg = getServicePackage(request.service);
const client = getServiceClient(sdkPkg);
const apiCall = new ApiCall(request.service, request.api);

const Command = getSdkCommand(sdkPkg, request.api);
const parameters = (request.parameters && decodeParameters(request.parameters)) ?? {};
const commandInput = coerceApiParameters(request.service, request.api, parameters);
const parameters = request.parameters ? decodeParameters(request.parameters) : {};
console.log(`SDK request to ${apiCall.service}.${apiCall.action} with parameters ${JSON.stringify(parameters)}`);
const response = await apiCall.invoke({ parameters }) as Record<string, unknown>;
const parsedResponse = deepParseJson(response);

console.log(`SDK request to ${sdkPkg.service}.${request.api} with parameters ${JSON.stringify(commandInput)}`);
const response = await client.send(new Command(commandInput));
await coerceResponse(response);
console.log(`SDK response received ${JSON.stringify(parsedResponse)}`);
delete parsedResponse.$metadata;

console.log(`SDK response received ${JSON.stringify(response)}`);
delete response.$metadata;
const respond = {
apiCallResponse: response,
};
Expand Down Expand Up @@ -120,12 +43,4 @@ function filterKeys(object: object, searchStrings: string[]): { [key: string]: s
}
return filteredObject;
}, {});
}

function isJsonString(value: string): any {
try {
return JSON.parse(value);
} catch {
return value;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,38 @@
async function coerceValue(v: any) {

if (v && typeof(v) === 'object' && typeof((v as any).transformToString) === 'function') {
// in sdk v3 some return types are now adapters that we need to explicitly
// convert to strings. see example: https://github.com/aws/aws-sdk-js-v3/blob/main/UPGRADING.md?plain=1#L573-L576
// note we don't use 'instanceof Unit8Array' because observations show this won't always return true, even though
// the `transformToString` function will be available. (for example S3::GetObject)
const text = await (v as any).transformToString();
return tryJsonParse(text);
/**
* Recurse into the given object, trying to parse any string as JSON
*/
export function deepParseJson<A extends string>(x: A): unknown;
export function deepParseJson<A extends object>(x: A): A;
export function deepParseJson(x: unknown): unknown {
if (typeof x === 'string') {
return tryJsonParse(x);
}
if (Array.isArray(x)) {
return x.map(deepParseJson);
}
if (x && typeof x === 'object') {
for (const [key, value] of Object.entries(x)) {
(x as any)[key] = deepParseJson(value);
}

return x;
}
return tryJsonParse(v);

return x;
}

function tryJsonParse(v: any) {
function tryJsonParse(v: string): unknown {
if (typeof(v) !== 'string') {
return v;
}

try {
return JSON.parse(v);
} catch {
return v;
}
}

export async function coerceResponse(response: any) {

if (response == null) {
return;
}

for (const key of Object.keys(response)) {
response[key] = await coerceValue(response[key]);
if (typeof response[key] === 'object') {
await coerceResponse(response[key]);
}
}

}

export function decodeParameters(obj: Record<string, any>): any {
return Object.fromEntries(Object.entries(obj).map(([key, value]) => {
try {
Expand Down
Loading
Loading