From 34ae7760dbc053136e95538f213cd8e87c8c5e30 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 12 Jan 2022 17:31:17 +0100 Subject: [PATCH] feat(apigatewayv2): HttpRouteIntegration supports AWS services integrations (#18154) Add support for integration subtype and credentials allowing to extend `HttpRouteIntegration` to create integrations for AWS services. See https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-aws-services.html Extracted part of #16287 to make it more reviewer friendly. BREAKING CHANGE: `HttpIntegrationType.LAMBDA_PROXY` has been renamed to `HttpIntegrationType.AWS_PROXY` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/http/lambda.ts | 4 +- .../aws-apigatewayv2/lib/http/integration.ts | 143 ++++++++++++++++-- .../aws-apigatewayv2/lib/parameter-mapping.ts | 14 +- .../aws-apigatewayv2/test/http/route.test.ts | 56 ++++++- 4 files changed, 203 insertions(+), 14 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts index 6bdce918195cf..2417fffe1610d 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts @@ -63,10 +63,10 @@ export class HttpLambdaIntegration extends HttpRouteIntegration { }); return { - type: HttpIntegrationType.LAMBDA_PROXY, + type: HttpIntegrationType.AWS_PROXY, uri: this.handler.functionArn, payloadFormatVersion: this.props.payloadFormatVersion ?? PayloadFormatVersion.VERSION_2_0, parameterMapping: this.props.parameterMapping, }; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts index 857ef57657736..58e9c9a60879a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts @@ -1,3 +1,4 @@ +import { IRole } from '@aws-cdk/aws-iam'; import { Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnIntegration } from '../apigatewayv2.generated'; @@ -23,15 +24,96 @@ export interface IHttpIntegration extends IIntegration { */ export enum HttpIntegrationType { /** - * Integration type is a Lambda proxy - * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + * Integration type is an HTTP proxy. + * + * For integrating the route or method request with an HTTP endpoint, with the + * client request passed through as-is. This is also referred to as HTTP proxy + * integration. For HTTP API private integrations, use an HTTP_PROXY integration. + * + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-http.html */ - LAMBDA_PROXY = 'AWS_PROXY', + HTTP_PROXY = 'HTTP_PROXY', + /** - * Integration type is an HTTP proxy + * Integration type is an AWS proxy. + * + * For integrating the route or method request with a Lambda function or other + * AWS service action. This integration is also referred to as a Lambda proxy + * integration. + * + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-aws-services.html * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html */ - HTTP_PROXY = 'HTTP_PROXY', + AWS_PROXY = 'AWS_PROXY', +} + +/** + * Supported integration subtypes + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-aws-services-reference.html + */ +export enum HttpIntegrationSubtype { + /** + * EventBridge PutEvents integration + */ + EVENTBRIDGE_PUT_EVENTS = 'EventBridge-PutEvents', + /** + * SQS SendMessage integration + */ + SQS_SEND_MESSAGE = 'SQS-SendMessage', + /** + * SQS ReceiveMessage integration, + */ + SQS_RECEIVE_MESSAGE = 'SQS-ReceiveMessage', + /** + * SQS DeleteMessage integration, + */ + SQS_DELETE_MESSAGE = 'SQS-DeleteMessage', + /** + * SQS PurgeQueue integration + */ + SQS_PURGE_QUEUE = 'SQS-PurgeQueue', + /** + * AppConfig GetConfiguration integration + */ + APPCONFIG_GET_CONFIGURATION = 'AppConfig-GetConfiguration', + /** + * Kinesis PutRecord integration + */ + KINESIS_PUT_RECORD = 'Kinesis-PutRecord', + /** + * Step Functions StartExecution integration + */ + STEPFUNCTIONS_START_EXECUTION = 'StepFunctions-StartExecution', + /** + * Step Functions StartSyncExecution integration + */ + STEPFUNCTIONS_START_SYNC_EXECUTION = 'StepFunctions-StartSyncExecution', + /** + * Step Functions StopExecution integration + */ + STEPFUNCTIONS_STOP_EXECUTION = 'StepFunctions-StopExecution', +} + +/** + * Credentials used for AWS Service integrations. + */ +export abstract class IntegrationCredentials { + /** + * Use the specified role for integration requests + */ + public static fromRole(role: IRole): IntegrationCredentials { + return { credentialsArn: role.roleArn }; + } + + /** Use the calling user's identity to call the integration */ + public static useCallerIdentity(): IntegrationCredentials { + return { credentialsArn: 'arn:aws:iam::*:user/*' }; + } + + /** + * The ARN of the credentials + */ + public abstract readonly credentialsArn: string; } /** @@ -88,12 +170,23 @@ export interface HttpIntegrationProps { */ readonly integrationType: HttpIntegrationType; + /** + * Integration subtype. + * + * Used for AWS Service integrations, specifies the target of the integration. + * + * @default - none, required if no `integrationUri` is defined. + */ + readonly integrationSubtype?: HttpIntegrationSubtype; + /** * Integration URI. - * This will be the function ARN in the case of `HttpIntegrationType.LAMBDA_PROXY`, + * This will be the function ARN in the case of `HttpIntegrationType.AWS_PROXY`, * or HTTP URL in the case of `HttpIntegrationType.HTTP_PROXY`. + * + * @default - none, required if no `integrationSubtype` is defined. */ - readonly integrationUri: string; + readonly integrationUri?: string; /** * The HTTP method to use when calling the underlying HTTP proxy @@ -118,7 +211,7 @@ export interface HttpIntegrationProps { /** * The version of the payload format * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html - * @default - defaults to latest in the case of HttpIntegrationType.LAMBDA_PROXY`, irrelevant otherwise. + * @default - defaults to latest in the case of HttpIntegrationType.AWS_PROXY`, irrelevant otherwise. */ readonly payloadFormatVersion?: PayloadFormatVersion; @@ -135,6 +228,13 @@ export interface HttpIntegrationProps { * @default undefined requests are sent to the backend unmodified */ readonly parameterMapping?: ParameterMapping; + + /** + * The credentials with which to invoke the integration. + * + * @default - no credentials, use resource-based permissions on supported AWS services + */ + readonly credentials?: IntegrationCredentials; } /** @@ -148,15 +248,22 @@ export class HttpIntegration extends Resource implements IHttpIntegration { constructor(scope: Construct, id: string, props: HttpIntegrationProps) { super(scope, id); + + if (!props.integrationSubtype && !props.integrationUri) { + throw new Error('Either `integrationSubtype` or `integrationUri` must be specified.'); + } + const integ = new CfnIntegration(this, 'Resource', { apiId: props.httpApi.apiId, integrationType: props.integrationType, + integrationSubtype: props.integrationSubtype, integrationUri: props.integrationUri, integrationMethod: props.method, connectionId: props.connectionId, connectionType: props.connectionType, payloadFormatVersion: props.payloadFormatVersion?.version, requestParameters: props.parameterMapping?.mappings, + credentialsArn: props.credentials?.credentialsArn, }); if (props.secureServerName) { @@ -214,6 +321,7 @@ export abstract class HttpRouteIntegration { this.integration = new HttpIntegration(options.scope, this.id, { httpApi: options.route.httpApi, integrationType: config.type, + integrationSubtype: config.subtype, integrationUri: config.uri, method: config.method, connectionId: config.connectionId, @@ -221,6 +329,7 @@ export abstract class HttpRouteIntegration { payloadFormatVersion: config.payloadFormatVersion, secureServerName: config.secureServerName, parameterMapping: config.parameterMapping, + credentials: config.credentials, }); } return { integrationId: this.integration.integrationId }; @@ -241,10 +350,19 @@ export interface HttpRouteIntegrationConfig { */ readonly type: HttpIntegrationType; + /** + * Integration subtype. + * + * @default - none, required if no `integrationUri` is defined. + */ + readonly subtype?: HttpIntegrationSubtype; + /** * Integration URI + * + * @default - none, required if no `integrationSubtype` is defined. */ - readonly uri: string; + readonly uri?: string; /** * The HTTP method that must be used to invoke the underlying proxy. @@ -287,4 +405,11 @@ export interface HttpRouteIntegrationConfig { * @default undefined requests are sent to the backend unmodified */ readonly parameterMapping?: ParameterMapping; + + /** + * The credentials with which to invoke the integration. + * + * @default - no credentials, use resource-based permissions on supported AWS services + */ + readonly credentials?: IntegrationCredentials; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/parameter-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/parameter-mapping.ts index deb967d572de2..5b92a50d9ca61 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/parameter-mapping.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/parameter-mapping.ts @@ -71,6 +71,18 @@ export class MappingValue implements IMappingValue { * Represents a Parameter Mapping. */ export class ParameterMapping { + + /** + * Creates a mapping from an object. + */ + public static fromObject(obj: { [key: string]: MappingValue }): ParameterMapping { + const mapping = new ParameterMapping(); + for (const [k, m] of Object.entries(obj)) { + mapping.custom(k, m.value); + } + return mapping; + } + /** * Represents all created parameter mappings. */ @@ -142,4 +154,4 @@ export class ParameterMapping { this.mappings[key] = value; return this; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts index 0f0d4d01fd1c5..7f64176446928 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts @@ -1,5 +1,5 @@ import { Template } from '@aws-cdk/assertions'; -import { AccountPrincipal, Role } from '@aws-cdk/aws-iam'; +import { AccountPrincipal, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import { Stack, App } from '@aws-cdk/core'; import { HttpApi, HttpAuthorizer, HttpAuthorizerType, HttpConnectionType, HttpIntegrationType, HttpMethod, HttpRoute, @@ -7,6 +7,8 @@ import { MappingValue, ParameterMapping, PayloadFormatVersion, + HttpIntegrationSubtype, + IntegrationCredentials, } from '../../lib'; describe('HttpRoute', () => { @@ -249,6 +251,56 @@ describe('HttpRoute', () => { Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::VpcLink', 0); }); + test('configures AWS service integration correctly', () => { + // GIVEN + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + const role = new Role(stack, 'Role', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), + }); + + class SqsSendMessageIntegration extends HttpRouteIntegration { + public bind(): HttpRouteIntegrationConfig { + return { + method: HttpMethod.ANY, + payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, + type: HttpIntegrationType.AWS_PROXY, + subtype: HttpIntegrationSubtype.SQS_SEND_MESSAGE, + credentials: IntegrationCredentials.fromRole(role), + parameterMapping: ParameterMapping.fromObject({ + QueueUrl: MappingValue.requestHeader('queueUrl'), + MessageBody: MappingValue.requestBody('message'), + }), + }; + } + } + + // WHEN + new HttpRoute(stack, 'HttpRoute', { + httpApi, + integration: new SqsSendMessageIntegration('SqsSendMessageIntegration'), + routeKey: HttpRouteKey.with('/sqs', HttpMethod.POST), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationSubtype: 'SQS-SendMessage', + IntegrationMethod: 'ANY', + PayloadFormatVersion: '1.0', + CredentialsArn: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', + ], + }, + RequestParameters: { + QueueUrl: '$request.header.queueUrl', + MessageBody: '$request.body.message', + }, + }); + }); + test('can create route with an authorizer attached', () => { const stack = new Stack(); const httpApi = new HttpApi(stack, 'HttpApi'); @@ -632,4 +684,4 @@ class SomeAuthorizerType implements IHttpRouteAuthorizer { authorizationType: this.authorizationType, }; } -} \ No newline at end of file +}