From 5f919135a9e2a2688d6ca277e6af846b614d8e92 Mon Sep 17 00:00:00 2001 From: surukonda <65268382+surukonda@users.noreply.github.com> Date: Wed, 7 Oct 2020 00:49:27 +0530 Subject: [PATCH] feat(aws-apigateway-iot): new aws-apigateway-iot pattern implementation (#83) * new pattern aws-apigateway-iot implementation * address cfn_nag issues * resolved pr review comments, code refactoring, handling fully qualified iot endpoints Co-authored-by: surukonda <> Co-authored-by: EC2 Default User Co-authored-by: surukonda --- CONTRIBUTING.md | 2 +- .../aws-apigateway-iot/.eslintignore | 5 + .../aws-apigateway-iot/.gitignore | 15 + .../aws-apigateway-iot/.npmignore | 21 + .../aws-apigateway-iot/README.md | 151 + .../aws-apigateway-iot/architecture.png | Bin 0 -> 26164 bytes .../aws-apigateway-iot/lib/index.ts | 282 + .../aws-apigateway-iot/package.json | 81 + .../test.apigateway-iot.test.js.snap | 5117 +++++++++++++++++ .../test/integ.defaultParams.expected.json | 1247 ++++ .../test/integ.defaultParams.ts | 31 + .../test/integ.overrideParams.expected.json | 1376 +++++ .../test/integ.overrideParams.ts | 76 + .../test/test.apigateway-iot.test.ts | 623 ++ .../aws-events-rule-lambda/README.md | 6 +- .../aws-events-rule-step-function/README.md | 6 +- .../aws-iot-kinesisfirehose-s3/README.md | 6 +- .../aws-iot-lambda-dynamodb/README.md | 6 +- .../aws-iot-lambda/README.md | 6 +- .../README.md | 6 +- .../aws-kinesisfirehose-s3/README.md | 6 +- .../aws-kinesisstreams-lambda/README.md | 6 +- .../aws-lambda-dynamodb/README.md | 6 +- .../core/lib/apigateway-defaults.ts | 10 +- .../core/lib/apigateway-helper.ts | 101 +- .../core/test/apigateway-helper.test.ts | 334 ++ .../aws-serverless-image-handler/README.md | 8 +- 27 files changed, 9479 insertions(+), 55 deletions(-) create mode 100755 source/patterns/@aws-solutions-constructs/aws-apigateway-iot/.eslintignore create mode 100755 source/patterns/@aws-solutions-constructs/aws-apigateway-iot/.gitignore create mode 100755 source/patterns/@aws-solutions-constructs/aws-apigateway-iot/.npmignore create mode 100755 source/patterns/@aws-solutions-constructs/aws-apigateway-iot/README.md create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-iot/architecture.png create mode 100755 source/patterns/@aws-solutions-constructs/aws-apigateway-iot/lib/index.ts create mode 100755 source/patterns/@aws-solutions-constructs/aws-apigateway-iot/package.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-iot/test/__snapshots__/test.apigateway-iot.test.js.snap create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-iot/test/integ.defaultParams.expected.json create mode 100755 source/patterns/@aws-solutions-constructs/aws-apigateway-iot/test/integ.defaultParams.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-apigateway-iot/test/integ.overrideParams.expected.json create mode 100755 source/patterns/@aws-solutions-constructs/aws-apigateway-iot/test/integ.overrideParams.ts create mode 100755 source/patterns/@aws-solutions-constructs/aws-apigateway-iot/test/test.apigateway-iot.test.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e60e4377e..d5310a00c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,7 +83,7 @@ Now it's time to work your magic. Here are some guidelines: changes along the way, but try to avoid conflating multiple features. Eventually all these are going to go into a single commit, so you can use that to frame your scope. * If your change introduces a new construct, take a look at the our - [aws-apigateway-lambd Construct](https://github.com/awslabs/aws-solutions-constructs/tree/master/source/patterns/%40aws-solutions-constructs/aws-apigateway-lambda) for an explanation of the L3 patterns we use. + [aws-apigateway-lambda Construct](https://github.com/awslabs/aws-solutions-constructs/tree/master/source/patterns/%40aws-solutions-constructs/aws-apigateway-lambda) for an explanation of the L3 patterns we use. Feel free to start your contribution by copy&pasting files from that project, and then edit and rename them as appropriate - it might be easier to get started that way. diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/.eslintignore b/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/.eslintignore new file mode 100755 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/.gitignore b/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/.gitignore new file mode 100755 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/.gitignore @@ -0,0 +1,15 @@ +lib/*.js +test/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/.npmignore b/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/.npmignore new file mode 100755 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/README.md new file mode 100755 index 000000000..d66397925 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/README.md @@ -0,0 +1,151 @@ +# aws-apigateway-iot module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **Reference Documentation**:| https://docs.aws.amazon.com/solutions/latest/constructs/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png) Python|`aws_solutions_constructs.aws_apigateway_iot`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png) Typescript|`@aws-solutions-constructs/aws-apigateway-iot`| +|![Java Logo](https://docs.aws.amazon.com/cdk/api/latest/img/java32.png) Java|`software.amazon.awsconstructs.services.apigatewayiot`| + +## Overview + +This AWS Solutions Construct implements an Amazon API Gateway REST API connected to AWS IoT pattern. + +This construct creates a scalable HTTPS proxy between API Gateway and AWS IoT. This comes in handy when wanting to allow legacy devices that do not support the MQTT or MQTT/Websocket protocol to interact with the AWS IoT platform. + +This implementation enables write-only messages to be published on given MQTT topics, and also supports shadow updates of HTTPS devices to allowed things in the device registry. It does not involve Lambda functions for proxying messages, and instead relies on direct API Gateway to AWS IoT integration which supports both JSON messages as well as binary messages. + +Here is a minimal deployable pattern definition in Typescript: + +``` javascript +const { ApiGatewayToIot } from '@aws-solutions-constructs/aws-apigateway-iot'; + +new ApiGatewayToIot(this, 'ApiGatewayToIotPattern', { + apiGatewayToIotProps: { + iotEndpoint: 'a1234567890123-ats' + } +}); + +``` + +## Initializer + +``` text +new ApiGatewayToIot(scope: Construct, id: string, props: ApiGatewayToIotProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`ApiGatewayToIotProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|iotEndpoint|`string`|The AWS IoT endpoint subdomain to integrate the API Gateway with (e.g a1234567890123-ats).| +|apiGatewayCreateApiKey?|`boolean`|If set to `true`, an API Key is created and associated to a UsagePlan. User should specify `x-api-key` header while accessing RestApi. Default value set to `false`| +|apiGatewayExecutionRole?|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iam.Role.html)|IAM Role used by the API Gateway to access AWS IoT. If not specified, a default role is created with wildcard ('*') access to all topics and things.| +|apiGatewayProps?|[`api.restApiProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.RestApiProps.html)|Optional user-provided props to override the default props for the API Gateway.| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|apiGateway|[`api.RestApi`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.RestApi.html)|Returns an instance of the API Gateway REST API created by the pattern.| +|apiGatewayRole|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iam.Role.html)|Returns an instance of the iam.Role created by the construct for API Gateway.| +|apiGatewayCloudWatchRole|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iam.Role.html)|Returns an instance of the iam.Role created by the construct for API Gateway for CloudWatch access.| +|apiGatewayLogGroup|[`logs.LogGroup`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-logs.LogGroup.html)|Returns an instance of the LogGroup created by the construct for API Gateway access logging to CloudWatch.| + +## Default settings + +Out of the box implementation of the Construct without any override will set the following defaults: + +### Amazon API Gateway + +* Deploy an edge-optimized API endpoint +* Creates API Resources with `POST` Method to publish messages to IoT Topics +* Creates API Resources with `POST` Method to publish messages to ThingShadow & NamedShadows +* Enable CloudWatch logging for API Gateway +* Configure IAM role for API Gateway with access to all topics and things +* Set the default authorizationType for all API methods to IAM +* Enable X-Ray Tracing +* Creates a UsagePlan and associates to `prod` stage + +Below is a description of the different resources and methods exposed by the API Gateway after deploying the Construct. See the [Examples](#examples) section for more information on how to easily test these endpoints using `curl`. + +|Method | Resource | Query parameter(s) | Return code(s) | Description| +|-------------- | --------------------- | ------------------ | ----------------- | -----------------| +| **POST** | `/message/` | **qos** | `200/403/500` | By calling this endpoint, you need to pass the topics on which you would like to publish (e.g `/message/device/foo`).| +| **POST** | `/shadow/` | **None** | `200/403/500` | This route allows to update the shadow document of a thing, given its `thingName` using Unnamed (classic) shadow type. The body shall comply with the standard shadow stucture comprising a `state` node and associated `desired` and `reported` nodes. See the [Updating device shadows](#updating-device-shadows) section for an example.| +| **POST** | `/shadow//` | **None** | `200/403/500` | This route allows to update the named shadow document of a thing, given its `thingName` and the `shadowName` using the Named shadow type. The body shall comply with the standard shadow stucture comprising a `state` node and associated `desired` and `reported` nodes. See the [Updating named shadows](#updating-named-shadows) section for an example.| + +## Architecture + +![Architecture Diagram](architecture.png) + +## Examples + + The following examples only work with `API_KEY` authentication types, since IAM authorization requires a SIGv4 token to be specified as well, make sure the **apiGatewayCreateApiKey** property of your Construct props is set to **true** while deploying the stack, otherwise the below examples won't work. + +### Publishing a message + +You can use `curl` to publish a message on different MQTT topics using the HTTPS API. The below example will post a message on the `device/foo` topic. + +```bash +curl -XPOST https://.execute-api..amazonaws.com/prod/message/device/foo -H "x-api-key: " -H "Content-Type: application/json" -d '{"Hello": "World"}' +``` + +> Replace the `stage-id`, `region` and `api-key` parameters with your deployment values. + +You can chain topic names in the URL and the API accepts up to 7 sub-topics that you can publish on. For instance, the below example publishes a message on the topic `device/foo/bar/abc/xyz`. + +```bash +curl -XPOST https://.execute-api..amazonaws.com/development/message/device/foo/bar/abc/xyz -H "x-api-key: " -H "Content-Type: application/json" -d '{"Hello": "World"}' +``` + +### Updating device shadows + +To update the shadow document associated with a given thing, you can issue a shadow state request using a thing name. See the following example on how to update a thing shadow. + +```bash +curl -XPOST https://.execute-api..amazonaws.com/prod/shadow/device1 -H "x-api-key: " -H "Content-Type: application/json" -d '{"state": "desired": { "Hello": "World" }}' +``` + +### Updating named shadows + +To update the shadow document associated with a given thing's named shadow, you can issue a shadow state request using a thing name and shadow name. See the following example on how to update a named shadow. + +```bash +curl -XPOST https://.execute-api..amazonaws.com/prod/shadow/device1/shadow1 -H "x-api-key: " -H "Content-Type: application/json" -d '{"state": "desired": { "Hello": "World" }}' +``` + +### Sending binary payloads + +It is possible to send a binary payload to the proxy API, down to the AWS IoT service. In the following example, we send the content of the `README.md` file associated with this module (treated as a binary data) to `device/foo` topic using the `application/octet-stream` content type. + +```bash +curl -XPOST https://.execute-api..amazonaws.com/prod/message/device/foo/bar/baz/qux -H "x-api-key: " -H "Content-Type: application/octet-stream" --data-binary @README.md +``` + +> Execute this command while in the directory of this project. You can then test sending other type of binary files from your file-system. + + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/architecture.png b/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..566d57d29ecdb147d51d7792c84d01157678d396 GIT binary patch literal 26164 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 { + // Initial Setup + const stack = new cdk.Stack(); + const props: ApiGatewayToIotProps = { + iotEndpoint: `a1234567890123-ats` + }; + new ApiGatewayToIot(stack, 'test-apigateway-iot-default-snapshot', props); + // Assertion + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Check for ApiGateway params +// -------------------------------------------------------------- +test('Test for default Params construct props', () => { + // Initial Setup + const stack = new cdk.Stack(); + const props: ApiGatewayToIotProps = { + iotEndpoint: `a1234567890123-ats` + }; + const construct = new ApiGatewayToIot(stack, 'test-apigateway-iot-default-params', props); + // Assertion + expect(construct.apiGateway).not.toBeNull(); + expect(construct.apiGatewayCloudWatchRole).not.toBeNull(); + expect(construct.apiGatewayLogGroup).not.toBeNull(); + expect(construct.apiGatewayRole).not.toBeNull(); +}); + +// -------------------------------------------------------------- +// Check for Default IAM Role +// -------------------------------------------------------------- +test('Test for default IAM Role', () => { + // Initial Setup + const stack = new cdk.Stack(); + const props: ApiGatewayToIotProps = { + iotEndpoint: `a1234567890123-ats` + }; + new ApiGatewayToIot(stack, 'test-apigateway-iot-default-iam-role', props); + // Check whether default IAM role is creted to access IoT core + expect(stack).toHaveResource("AWS::IAM::Role", { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: "sts:AssumeRole", + Effect: "Allow", + Principal: { + Service: "apigateway.amazonaws.com" + } + } + ], + Version: "2012-10-17" + }, + Path: "/", + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: "iot:UpdateThingShadow", + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:iot:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":thing/*" + ] + ] + } + }, + { + Action: "iot:Publish", + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:iot:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":topic/*" + ] + ] + } + } + ], + Version: "2012-10-17" + }, + PolicyName: "awsapigatewayiotpolicy" + } + ] + }); +}); + +// -------------------------------------------------------------- +// Check for Request Validator +// -------------------------------------------------------------- +test('Test for default Params request validator', () => { + // Initial Setup + const stack = new cdk.Stack(); + const props: ApiGatewayToIotProps = { + iotEndpoint: `a1234567890123-ats` + }; + new ApiGatewayToIot(stack, 'test-apigateway-iot-request-validator', props); + // Assertion + expect(stack).toHaveResourceLike("AWS::ApiGateway::RequestValidator", { + ValidateRequestBody: false, + ValidateRequestParameters: true, + }); +}); + +// -------------------------------------------------------------- +// Check for Integ Props and Method Props +// -------------------------------------------------------------- +test('Test for default Params Integ Props and Method Props', () => { + // Initial Setup + const stack = new cdk.Stack(); + const props: ApiGatewayToIotProps = { + iotEndpoint: `a1234567890123-ats` + }; + new ApiGatewayToIot(stack, 'test-apigateway-iot-integpros-methodprops', props); + + // Assertion for {topic-level-7} to ensure all Integration Request Params, Integration Responses, + // Method Request Params and Method Reponses are intact + expect(stack).toHaveResourceLike("AWS::ApiGateway::Method", { + HttpMethod: "POST", + AuthorizationType: "AWS_IAM", + Integration: { + IntegrationHttpMethod: "POST", + IntegrationResponses: [ + { + ResponseTemplates: { + "application/json": "$input.json('$')" + }, + SelectionPattern: "2\\d{2}", + StatusCode: "200" + }, + { + ResponseTemplates: { + "application/json": "$input.json('$')" + }, + SelectionPattern: "5\\d{2}", + StatusCode: "500" + }, + { + ResponseTemplates: { + "application/json": "$input.json('$')" + }, + StatusCode: "403" + } + ], + PassthroughBehavior: "WHEN_NO_MATCH", + RequestParameters: { + "integration.request.path.topic-level-1": "method.request.path.topic-level-1", + "integration.request.path.topic-level-2": "method.request.path.topic-level-2", + "integration.request.path.topic-level-3": "method.request.path.topic-level-3", + "integration.request.path.topic-level-4": "method.request.path.topic-level-4", + "integration.request.path.topic-level-5": "method.request.path.topic-level-5", + "integration.request.path.topic-level-6": "method.request.path.topic-level-6", + "integration.request.path.topic-level-7": "method.request.path.topic-level-7" + }, + RequestTemplates: { + "application/json": "$input.json('$')" + }, + Type: "AWS", + Uri: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":apigateway:", + { + Ref: "AWS::Region" + }, + `:${props.iotEndpoint}.iotdata:path/topics/{topic-level-1}/{topic-level-2}/{topic-level-3}/{topic-level-4}/{topic-level-5}/{topic-level-6}/{topic-level-7}` + ] + ] + } + }, + MethodResponses: [ + { + StatusCode: "200" + }, + { + StatusCode: "500" + }, + { + StatusCode: "403" + } + ], + RequestParameters: { + "method.request.path.topic-level-1": true, + "method.request.path.topic-level-2": true, + "method.request.path.topic-level-3": true, + "method.request.path.topic-level-4": true, + "method.request.path.topic-level-5": true, + "method.request.path.topic-level-6": true, + "method.request.path.topic-level-7": true + } + }); +}); + +// -------------------------------------------------------------- +// Check for valid IoT Endpoint +// -------------------------------------------------------------- +test('Test for valid iot enpoint', () => { + // Initial Setup + const stack = new cdk.Stack(); + const props: ApiGatewayToIotProps = { + iotEndpoint: ' ' + }; + + const app = () => { + new ApiGatewayToIot(stack, 'test-apigateway-iot-no-endpoint', props); + }; + // Assertion + expect(app).toThrowError(); +}); + +// -------------------------------------------------------------- +// Check for binaryMediaTypes +// -------------------------------------------------------------- +test('Test for Binary Media types', () => { + // Stack + const stack = new cdk.Stack(); + // Helper declaration + new ApiGatewayToIot(stack, 'test-apigateway-iot-binaryMediaTypes', { + iotEndpoint: 'a1234567890123-ats' + } + ); + // Assertion 1 + expect(stack).toHaveResourceLike("AWS::ApiGateway::RestApi", { + BinaryMediaTypes: [ + "application/octet-stream", + ], + }); +}); + +// -------------------------------------------------------------- +// Check for multiple constructs +// -------------------------------------------------------------- +test('Test for multiple constructs usage', () => { + // Initial Setup + const stack = new cdk.Stack(); + const props: ApiGatewayToIotProps = { + iotEndpoint: `a1234567890123-ats` + }; + new ApiGatewayToIot(stack, 'test-apigateway-iot-default-params', props); + new ApiGatewayToIot(stack, 'test-apigateway-iot-default-params-1', props); + + // Assertion + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Check for ApiGateway Overriden Props Snapshot match +// -------------------------------------------------------------- +test('Test for overriden props snapshot', () => { + // Initial Setup + const stack = new cdk.Stack(); + const apiGatewayProps = { + restApiName: 'RestApi-Regional', + description: 'Description for the Regional Rest Api', + endpointConfiguration: {types: [api.EndpointType.REGIONAL]}, + apiKeySourceType: api.ApiKeySourceType.HEADER, + defaultMethodOptions: { + authorizationType: api.AuthorizationType.NONE, + } + }; + + const policyJSON = { + Version: "2012-10-17", + Statement: [ + { + Action: [ + "iot:UpdateThingShadow" + ], + Resource: `arn:aws:iot:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:thing/*`, + Effect: "Allow" + }, + { + Action: [ + "iot:Publish" + ], + Resource: `arn:aws:iot:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:topic/*`, + Effect: "Allow" + } + ] + }; + const policyDocument: iam.PolicyDocument = iam.PolicyDocument.fromJson(policyJSON); + const iamRoleProps: iam.RoleProps = { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), + path: '/', + inlinePolicies: {testPolicy: policyDocument} + }; + + // Create a policy that overrides the default policy that gets created with the construct + const apiGatewayExecutionRole: iam.Role = new iam.Role(stack, 'apigateway-iot-role', iamRoleProps); + + // Api gateway setup + const props: ApiGatewayToIotProps = { + iotEndpoint: `a1234567890123-ats`, + apiGatewayCreateApiKey: true, + apiGatewayExecutionRole, + apiGatewayProps + }; + new ApiGatewayToIot(stack, 'test-apigateway-iot-overriden-params', props); + + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Check for Api Name and Desc +// -------------------------------------------------------------- +test('Test for Api Name and Desc', () => { + // Stack + const stack = new cdk.Stack(); + const apiGatewayProps = { + restApiName: 'RestApi-Regional', + description: 'Description for the Regional Rest Api' + }; + // Helper declaration + new ApiGatewayToIot(stack, 'test-apigateway-iot-name-desc', { + iotEndpoint: 'a1234567890123-ats', + apiGatewayProps + } + ); + // Assertion 1 + expect(stack).toHaveResourceLike("AWS::ApiGateway::RestApi", { + Name: 'RestApi-Regional', + Description: 'Description for the Regional Rest Api' + }); +}); + +// -------------------------------------------------------------- +// Check for Overriden IAM Role +// -------------------------------------------------------------- +test('Test for overriden IAM Role', () => { + // Initial Setup + const stack = new cdk.Stack(); + + const policyJSON = { + Version: "2012-10-17", + Statement: [ + { + Action: [ + "iot:UpdateThingShadow" + ], + Resource: `arn:aws:iot:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:thing/mything1`, + Effect: "Allow" + }, + { + Action: [ + "iot:Publish" + ], + Resource: `arn:aws:iot:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:topic/topic-abc`, + Effect: "Allow" + } + ] + }; + const policyDocument: iam.PolicyDocument = iam.PolicyDocument.fromJson(policyJSON); + const iamRoleProps: iam.RoleProps = { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), + path: '/', + inlinePolicies: {testPolicy: policyDocument} + }; + + // Create a policy that overrides the default policy that gets created with the construct + const apiGatewayExecutionRole: iam.Role = new iam.Role(stack, 'apigateway-iot-role', iamRoleProps); + + const props: ApiGatewayToIotProps = { + iotEndpoint: `a1234567890123-ats`, + apiGatewayExecutionRole, + }; + + new ApiGatewayToIot(stack, 'test-apigateway-iot-overriden-iam-role', props); + // Check whether default IAM role is creted to access IoT core + expect(stack).toHaveResource("AWS::IAM::Role", { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: "sts:AssumeRole", + Effect: "Allow", + Principal: { + Service: "apigateway.amazonaws.com" + } + } + ], + Version: "2012-10-17" + }, + Path: "/", + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: "iot:UpdateThingShadow", + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:iot:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":thing/mything1" + ] + ] + } + }, + { + Action: "iot:Publish", + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:iot:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":topic/topic-abc" + ] + ] + } + } + ], + Version: "2012-10-17" + }, + PolicyName: "testPolicy" + } + ] + }); +}); + +// -------------------------------------------------------------- +// Check for Api Key Source +// -------------------------------------------------------------- +test('Test for APi Key Source', () => { + // Stack + const stack = new cdk.Stack(); + const apiGatewayProps = { + apiKeySourceType: api.ApiKeySourceType.AUTHORIZER, + }; + + // Helper declaration + new ApiGatewayToIot(stack, 'test-apigateway-iot-api-key-source', { + iotEndpoint: 'a1234567890123-ats', + apiGatewayProps + } + ); + // Assertion 1 + expect(stack).toHaveResourceLike("AWS::ApiGateway::RestApi", { + ApiKeySourceType: "AUTHORIZER" + }); +}); + +// -------------------------------------------------------------- +// Check for Api Key Creation +// -------------------------------------------------------------- +test('Test for Api Key Creation', () => { + // Initial Setup + const stack = new cdk.Stack(); + const props: ApiGatewayToIotProps = { + iotEndpoint: `a1234567890123-ats`, + apiGatewayCreateApiKey: true + }; + new ApiGatewayToIot(stack, 'test-apigateway-iot-api-key', props); + + // Assertion to check for ApiKey + expect(stack).toHaveResourceLike("AWS::ApiGateway::Method", { + Properties : { + ApiKeyRequired: true + }, + Metadata: { + cfn_nag: { + rules_to_suppress: [ + { + id: "W59" + } + ] + } + } + }, ResourcePart.CompleteDefinition); + expect(stack).toHaveResourceLike("AWS::ApiGateway::ApiKey", { + Enabled: true + }); + // Assertion to check for UsagePlan Api Key Mapping + expect(stack).toHaveResourceLike("AWS::ApiGateway::UsagePlanKey", { + KeyType: "API_KEY" + }); +}); + +// ----------------------------------------------------------------- +// Test deployment for ApiGateway endPointCongiurationOverride +// ----------------------------------------------------------------- +test('Test for deployment ApiGateway AuthorizationType override', () => { + // Stack + const stack = new cdk.Stack(); + // Helper declaration + new ApiGatewayToIot(stack, 'test-apigateway-iot-auth-none', { + iotEndpoint: 'a1234567890123-ats', + apiGatewayProps: { + endpointConfiguration: { + types: [api.EndpointType.REGIONAL] + } + } + }); + // Assertion 1 + expect(stack).toHaveResourceLike("AWS::ApiGateway::RestApi", { + EndpointConfiguration: { + Types: ["REGIONAL"] + } + }); +}); + +// ----------------------------------------------------------------- +// Test deployment for override ApiGateway AuthorizationType to NONE +// ----------------------------------------------------------------- +test('Test for deployment ApiGateway AuthorizationType override', () => { + // Stack + const stack = new cdk.Stack(); + // Helper declaration + new ApiGatewayToIot(stack, 'test-apigateway-iot-override-auth', { + iotEndpoint: 'a1234567890123-ats', + apiGatewayProps: { + defaultMethodOptions: { + authorizationType: api.AuthorizationType.NONE + } + } + }); + // Assertion 1 + expect(stack).toHaveResourceLike("AWS::ApiGateway::Method", { + HttpMethod: "POST", + AuthorizationType: "NONE" + }); + }); + + // ----------------------------------------------------------------- +// Test deployment for fully qualified iotEndpoint name +// ----------------------------------------------------------------- +test('Test for handling fully qualified iotEndpoint', () => { + // Stack + const stack = new cdk.Stack(); + // Helper declaration + new ApiGatewayToIot(stack, 'test-apigateway-iot-override-auth', { + iotEndpoint: 'a1234567890123-ats.iot.ap-south-1.amazonaws.com', + apiGatewayProps: { + defaultMethodOptions: { + authorizationType: api.AuthorizationType.NONE + } + } + }); + // Assertion 1 + expect(stack).toHaveResourceLike("AWS::ApiGateway::Method", { + Integration: { + Uri: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":apigateway:", + { + Ref: "AWS::Region" + }, + ":a1234567890123-ats.iotdata:path/topics/{topic-level-1}/{topic-level-2}/{topic-level-3}" + ] + ] + } } + }); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-events-rule-lambda/README.md index 3b53d408e..286ff78bc 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-lambda/README.md @@ -24,10 +24,10 @@ This AWS Solutions Construct implements an AWS Events rule and an AWS Lambda function. -Here is a minimal deployable pattern definition: +Here is a minimal deployable pattern definition in Typescript: ``` javascript -const { EventsRuleToLambdaProps, EventsRuleToLambda } = require('@aws-solutions-constructs/aws-events-rule-lambda'); +const { EventsRuleToLambdaProps, EventsRuleToLambda } from '@aws-solutions-constructs/aws-events-rule-lambda'; const props: EventsRuleToLambdaProps = { lambdaFunctionProps: { @@ -40,7 +40,7 @@ const props: EventsRuleToLambdaProps = { } }; -new EventsRuleToLambda(stack, 'test-events-rule-lambda', props); +new EventsRuleToLambda(this, 'test-events-rule-lambda', props); ``` ## Initializer diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/README.md b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/README.md index c001096b1..32c310661 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/README.md @@ -24,12 +24,12 @@ This AWS Solutions Construct implements an AWS Events rule and an AWS Step function. -Here is a minimal deployable pattern definition: +Here is a minimal deployable pattern definition in Typescript: ``` javascript -const { EventsRuleToStepFunction, EventsRuleToStepFunctionProps } = require('@aws-solutions-constructs/aws-events-rule-step-function'); +const { EventsRuleToStepFunction, EventsRuleToStepFunctionProps } from '@aws-solutions-constructs/aws-events-rule-step-function'; -const startState = new stepfunctions.Pass(stack, 'StartState'); +const startState = new stepfunctions.Pass(this, 'StartState'); const props: EventsRuleToStepFunctionProps = { stateMachineProps: { diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/README.md b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/README.md index aaede2815..f2e4b3277 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/README.md @@ -24,10 +24,10 @@ This AWS Solutions Construct implements an AWS IoT MQTT topic rule to send data to an Amazon Kinesis Data Firehose delivery stream connected to an Amazon S3 bucket. -Here is a minimal deployable pattern definition: +Here is a minimal deployable pattern definition in Typescript: ``` javascript -const { IotToKinesisFirehoseToS3Props, IotToKinesisFirehoseToS3 } = require('@aws-solutions-constructs/aws-iot-kinesisfirehose-s3'); +const { IotToKinesisFirehoseToS3Props, IotToKinesisFirehoseToS3 } from '@aws-solutions-constructs/aws-iot-kinesisfirehose-s3'; const props: IotToKinesisFirehoseToS3Props = { iotTopicRuleProps: { @@ -40,7 +40,7 @@ const props: IotToKinesisFirehoseToS3Props = { } }; -new IotToKinesisFirehoseToS3(stack, 'test-iot-firehose-s3', props); +new IotToKinesisFirehoseToS3(this, 'test-iot-firehose-s3', props); ``` diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/README.md b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/README.md index 91d3c3410..e5533108e 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/README.md @@ -24,10 +24,10 @@ This AWS Solutions Construct implements an AWS IoT topic rule, an AWS Lambda function and Amazon DynamoDB table with the least privileged permissions. -Here is a minimal deployable pattern definition: +Here is a minimal deployable pattern definition in Typescript: ``` javascript -const { IotToLambdaToDynamoDBProps, IotToLambdaToDynamoDB } = require('@aws-solutions-constructs/aws-iot-lambda-dynamodb'); +const { IotToLambdaToDynamoDBProps, IotToLambdaToDynamoDB } from '@aws-solutions-constructs/aws-iot-lambda-dynamodb'; const props: IotToLambdaToDynamoDBProps = { lambdaFunctionProps: { @@ -45,7 +45,7 @@ const props: IotToLambdaToDynamoDBProps = { } }; -new IotToLambdaToDynamoDB(stack, 'test-iot-lambda-dynamodb-stack', props); +new IotToLambdaToDynamoDB(this, 'test-iot-lambda-dynamodb-stack', props); ``` diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-iot-lambda/README.md index 51d9b08e6..58f214db0 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-iot-lambda/README.md @@ -24,10 +24,10 @@ This AWS Solutions Construct implements an AWS IoT MQTT topic rule and an AWS Lambda function pattern. -Here is a minimal deployable pattern definition: +Here is a minimal deployable pattern definition in Typescript: ``` javascript -const { IotToLambdaProps, IotToLambda } = require('@aws-solutions-constructs/aws-iot-lambda'); +const { IotToLambdaProps, IotToLambda } from '@aws-solutions-constructs/aws-iot-lambda'; const props: IotToLambdaProps = { lambdaFunctionProps: { @@ -45,7 +45,7 @@ const props: IotToLambdaProps = { } }; -new IotToLambda(stack, 'test-iot-lambda-integration', props); +new IotToLambda(this, 'test-iot-lambda-integration', props); ``` ## Initializer diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3-and-kinesisanalytics/README.md b/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3-and-kinesisanalytics/README.md index 2ebf919fd..a7a2a9c4b 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3-and-kinesisanalytics/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3-and-kinesisanalytics/README.md @@ -24,12 +24,12 @@ This AWS Solutions Construct implements an Amazon Kinesis Firehose delivery stream connected to an Amazon S3 bucket, and an Amazon Kinesis Analytics application. -Here is a minimal deployable pattern definition: +Here is a minimal deployable pattern definition in Typescript: ``` javascript -const { KinesisFirehoseToAnalyticsAndS3 } = require('@aws-solutions-constructs/aws-kinesisfirehose-s3-and-kinesisanalytics'); +const { KinesisFirehoseToAnalyticsAndS3 } from '@aws-solutions-constructs/aws-kinesisfirehose-s3-and-kinesisanalytics'; -new KinesisFirehoseToAnalyticsAndS3(stack, 'FirehoseToS3AndAnalyticsPattern', { +new KinesisFirehoseToAnalyticsAndS3(this, 'FirehoseToS3AndAnalyticsPattern', { kinesisAnalyticsProps: { inputs: [{ inputSchema: { diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/README.md b/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/README.md index 1d91156fc..6eddbea68 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/README.md @@ -24,12 +24,12 @@ This AWS Solutions Construct implements an Amazon Kinesis Data Firehose delivery stream connected to an Amazon S3 bucket. -Here is a minimal deployable pattern definition: +Here is a minimal deployable pattern definition in Typescript: ``` javascript -const { KinesisFirehoseToS3 } = require('@aws-solutions-constructs/aws-kinesisfirehose-s3'); +const { KinesisFirehoseToS3 } from '@aws-solutions-constructs/aws-kinesisfirehose-s3'; -new KinesisFirehoseToS3(stack, 'test-firehose-s3', {}); +new KinesisFirehoseToS3(this, 'test-firehose-s3', {}); ``` diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/README.md index 68c51819b..832f1929c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/README.md @@ -24,12 +24,12 @@ This AWS Solutions Construct deploys a Kinesis Stream and Lambda function with the appropriate resources/properties for interaction and security. -Here is a minimal deployable pattern definition: +Here is a minimal deployable pattern definition in Typescript: ``` javascript -const { KinesisStreamsToLambda } = require('@aws-solutions-constructs/aws-kinesisstreams-lambda'); +const { KinesisStreamsToLambda } from '@aws-solutions-constructs/aws-kinesisstreams-lambda'; -new KinesisStreamsToLambda(stack, 'KinesisToLambdaPattern', { +new KinesisStreamsToLambda(this, 'KinesisToLambdaPattern', { eventSourceProps: { startingPosition: lambda.StartingPosition.TRIM_HORIZON, batchSize: 1 diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/README.md index 3eb296f69..a10f568b9 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/README.md @@ -24,10 +24,10 @@ This AWS Solutions Construct implements the AWS Lambda function and Amazon DynamoDB table with the least privileged permissions. -Here is a minimal deployable pattern definition: +Here is a minimal deployable pattern definition in Typescript: ``` javascript -const { LambdaToDynamoDBProps, LambdaToDynamoDB } = require('@aws-solutions-constructs/aws-lambda-dynamodb'); +const { LambdaToDynamoDBProps, LambdaToDynamoDB } from '@aws-solutions-constructs/aws-lambda-dynamodb'; const props: LambdaToDynamoDBProps = { lambdaFunctionProps: { @@ -37,7 +37,7 @@ const props: LambdaToDynamoDBProps = { }, }; -new LambdaToDynamoDB(stack, 'test-lambda-dynamodb-stack', props); +new LambdaToDynamoDB(this, 'test-lambda-dynamodb-stack', props); ``` diff --git a/source/patterns/@aws-solutions-constructs/core/lib/apigateway-defaults.ts b/source/patterns/@aws-solutions-constructs/core/lib/apigateway-defaults.ts index 63f0c5275..995acb254 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/apigateway-defaults.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/apigateway-defaults.ts @@ -76,10 +76,16 @@ export function DefaultRegionalLambdaRestApiProps(_existingLambdaObj: lambda.Fun /** * Provides the default set of properties for Edge/Global RestApi - * @param scope - the construct to which the RestApi should be attached to. - * @param _endpointType - endpoint type for Api Gateway e.g. Regional, Global, Private * @param _logGroup - CW Log group for Api Gateway access logging */ export function DefaultGlobalRestApiProps(_logGroup: LogGroup) { return DefaultRestApiProps([api.EndpointType.EDGE], _logGroup); +} + +/** + * Provides the default set of properties for Regional RestApi + * @param _logGroup - CW Log group for Api Gateway access logging + */ +export function DefaultRegionalRestApiProps(_logGroup: LogGroup) { + return DefaultRestApiProps([api.EndpointType.REGIONAL], _logGroup); } \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts index d38ef4568..d05aa1633 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/apigateway-helper.ts @@ -20,7 +20,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as apiDefaults from './apigateway-defaults'; import { DefaultLogGroupProps } from './cloudwatch-log-group-defaults'; import { overrideProps } from './utils'; -import { Role } from '@aws-cdk/aws-iam'; +import { IRole } from '@aws-cdk/aws-iam'; /** * Create and configures access logging for API Gateway resources. @@ -78,6 +78,12 @@ function configureCloudwatchRoleForApi(scope: cdk.Construct, _api: api.RestApi): */ function configureLambdaRestApi(scope: cdk.Construct, defaultApiGatewayProps: api.LambdaRestApiProps, apiGatewayProps?: api.LambdaRestApiProps): [api.RestApi, iam.Role] { + + // API Gateway doesn't allow both endpointTypes and endpointConfiguration, check whether endPointTypes exists + if (apiGatewayProps?.endpointTypes) { + throw Error('Solutions Constructs internally uses endpointConfiguration, use endpointConfiguration instead of endpointTypes'); + } + // Define the API object let _api: api.RestApi; if (apiGatewayProps) { @@ -91,13 +97,20 @@ function configureLambdaRestApi(scope: cdk.Construct, defaultApiGatewayProps: ap // Configure API access logging const cwRole = configureCloudwatchRoleForApi(scope, _api); - // Configure Usage Plan - _api.addUsagePlan('UsagePlan', { + let usagePlanProps: api.UsagePlanProps = { apiStages: [{ - api: _api, - stage: _api.deploymentStage + api: _api, + stage: _api.deploymentStage }] - }); + }; + // If requireApiKey param is set to true, create a api key & associate to Usage Plan + if (apiGatewayProps?.defaultMethodOptions?.apiKeyRequired === true) { + const extraParams = { apiKey: _api.addApiKey('ApiKey')}; + usagePlanProps = Object.assign(usagePlanProps, extraParams); + } + + // Configure Usage Plan + _api.addUsagePlan('UsagePlan', usagePlanProps); // Return the API and CW Role return [_api, cwRole]; @@ -111,6 +124,12 @@ function configureLambdaRestApi(scope: cdk.Construct, defaultApiGatewayProps: ap */ function configureRestApi(scope: cdk.Construct, defaultApiGatewayProps: api.RestApiProps, apiGatewayProps?: api.RestApiProps): [api.RestApi, iam.Role] { + + // API Gateway doesn't allow both endpointTypes and endpointConfiguration, check whether endPointTypes exists + if (apiGatewayProps?.endpointTypes) { + throw Error('Solutions Constructs internally uses endpointConfiguration, use endpointConfiguration instead of endpointTypes'); + } + // Define the API let _api: api.RestApi; if (apiGatewayProps) { @@ -124,13 +143,21 @@ function configureRestApi(scope: cdk.Construct, defaultApiGatewayProps: api.Rest // Configure API access logging const cwRole = configureCloudwatchRoleForApi(scope, _api); - // Configure Usage Plan - _api.addUsagePlan('UsagePlan', { + let usagePlanProps: api.UsagePlanProps = { apiStages: [{ - api: _api, - stage: _api.deploymentStage + api: _api, + stage: _api.deploymentStage }] - }); + }; + + // If requireApiKey param is set to true, create a api key & associate to Usage Plan + if (apiGatewayProps?.defaultMethodOptions?.apiKeyRequired === true) { + const extraParams = { apiKey: _api.addApiKey('ApiKey')}; + usagePlanProps = Object.assign(usagePlanProps, extraParams); + } + + // Configure Usage Plan + _api.addUsagePlan('UsagePlan', usagePlanProps); // Return the API and CW Role return [_api, cwRole]; @@ -183,21 +210,37 @@ export function GlobalRestApi(scope: cdk.Construct, apiGatewayProps?: api.RestAp return [restApi, apiCWRole, logGroup ]; } +/** + * Builds and returns a Regional api.RestApi. + * @param scope - the construct to which the RestApi should be attached to. + * @param apiGatewayProps - (optional) user-specified properties to override the default properties. + */ +export function RegionalRestApi(scope: cdk.Construct, apiGatewayProps?: api.RestApiProps): [api.RestApi, iam.Role, logs.LogGroup] { + // Configure log group for API Gateway AccessLogging + const logGroup = new logs.LogGroup(scope, 'ApiAccessLogGroup', DefaultLogGroupProps()); + + const defaultProps = apiDefaults.DefaultRegionalRestApiProps(logGroup); + const [restApi, apiCWRole] = configureRestApi(scope, defaultProps, apiGatewayProps); + return [restApi, apiCWRole, logGroup ]; +} + export interface AddProxyMethodToApiResourceInputParams { readonly service: string, readonly action?: string, readonly path?: string, readonly apiResource: api.IResource, readonly apiMethod: string, - readonly apiGatewayRole: Role, + readonly apiGatewayRole: IRole, readonly requestTemplate: string, readonly contentType?: string, - readonly requestValidator?: api.RequestValidator, - readonly requestModel?: { [contentType: string]: api.IModel; } + readonly requestValidator?: api.IRequestValidator, + readonly requestModel?: { [contentType: string]: api.IModel; }, + readonly awsIntegrationProps?: api.AwsIntegrationProps | any, + readonly methodOptions?: api.MethodOptions } -export function addProxyMethodToApiResource(params: AddProxyMethodToApiResourceInputParams) { - const baseProps: api.AwsIntegrationProps = { +export function addProxyMethodToApiResource(params: AddProxyMethodToApiResourceInputParams): api.Method { + let baseProps: api.AwsIntegrationProps = { service: params.service, integrationHttpMethod: "POST", options: { @@ -239,10 +282,16 @@ export function addProxyMethodToApiResource(params: AddProxyMethodToApiResourceI } // Setup the API Gateway AWS Integration - const apiGatewayIntegration = new api.AwsIntegration(Object.assign(baseProps, extraProps)); + baseProps = Object.assign(baseProps, extraProps); + let apiGatewayIntegration; + if (params.awsIntegrationProps) { + const overridenProps = overrideProps(baseProps, params.awsIntegrationProps); + apiGatewayIntegration = new api.AwsIntegration(overridenProps); + } else { + apiGatewayIntegration = new api.AwsIntegration(baseProps); + } - // Setup the API Gateway method - params.apiResource.addMethod(params.apiMethod, apiGatewayIntegration, { + const defaultMethodOptions = { methodResponses: [ { statusCode: "200", @@ -259,5 +308,15 @@ export function addProxyMethodToApiResource(params: AddProxyMethodToApiResourceI ], requestValidator: params.requestValidator, requestModels: params.requestModel - }); - } \ No newline at end of file + }; + + let apiMethod; + // Setup the API Gateway method + if (params.methodOptions) { + const overridenProps = overrideProps(defaultMethodOptions, params.methodOptions); + apiMethod = params.apiResource.addMethod(params.apiMethod, apiGatewayIntegration, overridenProps); + } else { + apiMethod = params.apiResource.addMethod(params.apiMethod, apiGatewayIntegration, defaultMethodOptions); + } + return apiMethod; +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts index 990ffb33a..9198530af 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/apigateway-helper.test.ts @@ -328,3 +328,337 @@ test('Test default RestApi w/ request model and validator', () => { RequestModels: { "application/json": "Empty" } }); }); + +// ----------------------------------------------------------------------- +// Test for Regional ApiGateway Creation +// ----------------------------------------------------------------------- +test('Test for RegionalRestApiGateway', () => { + const stack = new Stack(); + + const [regionalApi] = defaults.RegionalRestApi(stack, { + restApiName: "HelloWorld-RegionalApi" + }); + // Setup the API Gateway role + const apiGatewayRole = new iam.Role(stack, 'api-gateway-role', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com') + }); + + // Setup the API Gateway resource + const apiGatewayResource = regionalApi.root.addResource('hello'); + + defaults.addProxyMethodToApiResource( + { + service: 'iotdata', + path: 'hello', + apiGatewayRole, + apiMethod: 'POST', + apiResource: apiGatewayResource, + requestTemplate: "$input.json('$')" + }); + + expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Type: "AWS::ApiGateway::RestApi", + Properties: { + EndpointConfiguration: { + Types: [ + "REGIONAL" + ] + }, + Name: "HelloWorld-RegionalApi" + } + }, ResourcePart.CompleteDefinition); +}); + +// ----------------------------------------------------------------------- +// Tests for exception while overriding restApiProps using endPointTypes +// ----------------------------------------------------------------------- +test('Test for Exception while overriding restApiProps using endPointTypes', () => { + const stack = new Stack(); + try { + defaults.RegionalRestApi(stack, { + endpointTypes: [api.EndpointType.REGIONAL] + }); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); + +// ----------------------------------------------------------------------- +// Tests for exception while overriding LambdaRestApiProps using endPointTypes +// ----------------------------------------------------------------------- +test('Test for Exception while overriding LambdaRestApiProps using endPointTypes', () => { + const stack = new Stack(); + const lambdaFunctionProps: lambda.FunctionProps = { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }; + + const fn = defaults.deployLambdaFunction(stack, lambdaFunctionProps); + + try { + defaults.GlobalLambdaRestApi(stack, fn, { + handler: fn, + endpointTypes: [api.EndpointType.REGIONAL] + }); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); + +// ----------------------------------------------------------------------- +// Test for Integration Request Props Override +// ----------------------------------------------------------------------- +test('Test for Integration Request Props Override', () => { + const stack = new Stack(); + + const [regionalApi] = defaults.RegionalRestApi(stack); + + // Setup the API Gateway role + const apiGatewayRole = new iam.Role(stack, 'api-gateway-role', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com') + }); + + // Setup the API Gateway resource + const apiGatewayResource = regionalApi.root.addResource('hello'); + const integReqParams = {'integration.request.path.topic-level-1': "'method.request.path.topic-level-1'"}; + const integResp: api.IntegrationResponse[] = [ + { + statusCode: "200", + selectionPattern: "2\\d{2}", + responseTemplates : { + "application/json": "$input.json('$')" + } + }]; + // Override the default Integration Request Props + const integrationReqProps = { + subdomain: 'abcdefgh12345', + options: { + requestParameters: integReqParams, + integrationResponses: integResp, + passthroughBehavior: api.PassthroughBehavior.WHEN_NO_MATCH + } + }; + defaults.addProxyMethodToApiResource( + { + service: 'iotdata', + path: 'hello', + apiGatewayRole, + apiMethod: 'POST', + apiResource: apiGatewayResource, + requestTemplate: "$input.json('$')", + awsIntegrationProps: integrationReqProps + }); + + expect(stack).toHaveResourceLike("AWS::ApiGateway::Method", { + HttpMethod: "POST", + AuthorizationType: "AWS_IAM", + Integration: { + IntegrationHttpMethod: "POST", + IntegrationResponses: [ + { + ResponseTemplates: { + "application/json": "$input.json('$')" + }, + SelectionPattern: "2\\d{2}", + StatusCode: "200" + } + ], + PassthroughBehavior: "WHEN_NO_MATCH", + RequestParameters: { + "integration.request.header.Content-Type": "'application/json'", + "integration.request.path.topic-level-1": "'method.request.path.topic-level-1'", + }, + RequestTemplates: { + "application/json": "$input.json('$')" + }, + Type: "AWS", + Uri: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":apigateway:", + { + Ref: "AWS::Region" + }, + `:abcdefgh12345.iotdata:path/hello` + ] + ] + } + }, + MethodResponses: [ + { + StatusCode: "200", + ResponseParameters: { + "method.response.header.Content-Type": true + } + } + ] + }); +}); + +// ----------------------------------------------------------------------- +// Test for Method Options Override +// ----------------------------------------------------------------------- +test('Test for Method Request Props Override', () => { + const stack = new Stack(); + + const [globalApi] = defaults.GlobalRestApi(stack); + + // Setup the API Gateway role + const apiGatewayRole = new iam.Role(stack, 'api-gateway-role', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com') + }); + + // Setup the API Gateway resource + const apiGatewayResource = globalApi.root.addResource('hello'); + const methodReqParams = {'method.request.path.topic-level-1': true}; + const methodResp: api.MethodResponse[] = [ + { + statusCode: "403" + } + ]; + const resourceMethodOptions = { + requestParameters: methodReqParams, + methodResponses: methodResp, + }; + defaults.addProxyMethodToApiResource( + { + service: 'iotdata', + path: 'hello', + apiGatewayRole, + apiMethod: 'POST', + apiResource: apiGatewayResource, + requestTemplate: "$input.json('$')", + methodOptions: resourceMethodOptions + }); + + expect(stack).toHaveResourceLike("AWS::ApiGateway::Method", { + HttpMethod: "POST", + AuthorizationType: "AWS_IAM", + Integration: { + IntegrationHttpMethod: "POST", + IntegrationResponses: [ + { + StatusCode: "200" + }, + { + StatusCode: "500", + ResponseTemplates: { + "text/html": "Error" + }, + SelectionPattern: "500" + } + ], + PassthroughBehavior: "NEVER", + RequestParameters: { + "integration.request.header.Content-Type": "'application/json'", + }, + RequestTemplates: { + "application/json": "$input.json('$')" + }, + Type: "AWS", + Uri: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":apigateway:", + { + Ref: "AWS::Region" + }, + `:iotdata:path/hello` + ] + ] + } + }, + MethodResponses: [ + { + StatusCode: "403" + } + ], + RequestParameters: { + "method.request.path.topic-level-1": true + } + }); +}); + +// ----------------------------------------------------------------------- +// Test for ApiKey Creation of RestApi +// ----------------------------------------------------------------------- +test('Test for ApiKey creation using restApiProps', () => { + const stack = new Stack(); + const [globalRestApi] = defaults.GlobalRestApi(stack, { + defaultMethodOptions: { + apiKeyRequired: true + } + }); + + // Setup the API Gateway role + const apiGatewayRole = new iam.Role(stack, 'api-gateway-role', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com') + }); + + // Setup the API Gateway resource + const apiGatewayResource = globalRestApi.root.addResource('hello'); + + defaults.addProxyMethodToApiResource( + { + service: 'iotdata', + path: 'hello', + apiGatewayRole, + apiMethod: 'POST', + apiResource: apiGatewayResource, + requestTemplate: "$input.json('$')" + }); + // Assertion to check for ApiKey + expect(stack).toHaveResourceLike("AWS::ApiGateway::Method", { + ApiKeyRequired: true + }); + expect(stack).toHaveResourceLike("AWS::ApiGateway::ApiKey", { + Enabled: true + }); + // Assertion to check for UsagePlan Api Key Mapping + expect(stack).toHaveResourceLike("AWS::ApiGateway::UsagePlanKey", { + KeyType: "API_KEY" + }); +}); + +// ----------------------------------------------------------------------- +// Test for ApiKey Creation of LambdaRestApi +// ----------------------------------------------------------------------- +test('Test for ApiKey creation using lambdaApiProps', () => { + const stack = new Stack(); + const lambdaFunctionProps: lambda.FunctionProps = { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }; + + const fn = defaults.deployLambdaFunction(stack, lambdaFunctionProps); + defaults.RegionalLambdaRestApi(stack, fn, { + handler: fn, + defaultMethodOptions: { + apiKeyRequired: true + } + }); + + // Assertion to check for ApiKey + expect(stack).toHaveResourceLike("AWS::ApiGateway::Method", { + ApiKeyRequired: true + }); + expect(stack).toHaveResourceLike("AWS::ApiGateway::ApiKey", { + Enabled: true + }); + // Assertion to check for UsagePlan Api Key Mapping + expect(stack).toHaveResourceLike("AWS::ApiGateway::UsagePlanKey", { + KeyType: "API_KEY" + }); +}); \ No newline at end of file diff --git a/source/use_cases/aws-serverless-image-handler/README.md b/source/use_cases/aws-serverless-image-handler/README.md index 11a7750e3..a8097aeae 100644 --- a/source/use_cases/aws-serverless-image-handler/README.md +++ b/source/use_cases/aws-serverless-image-handler/README.md @@ -4,12 +4,12 @@ This use case construct implements an Amazon CloudFront distribution, an Amazon function, and necessary permissions/logic to provision a functional image handler API for serving image content from one or more Amazon S3 buckets within the deployment account. -Here is a minimal deployable pattern definition: +Here is a minimal deployable pattern definition in Typescript: -``` -const { ServerlessImageHandler } = require('@aws-solutions-constructs/aws-serverless-image-handler'); +```javascript +const { ServerlessImageHandler } from '@aws-solutions-constructs/aws-serverless-image-handler'; -new ServerlessImageHandler(stack, 'ServerlessImageHandlerPattern', { +new ServerlessImageHandler(this, 'ServerlessImageHandlerPattern', { lambdaFunctionProps: { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler',