From b8a3d124a57706034cffc875b83991140cf90e13 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Tue, 4 Jan 2022 16:39:47 -0500 Subject: [PATCH 1/2] Update DESIGN_GUIDELINES.md --- DESIGN_GUIDELINES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DESIGN_GUIDELINES.md b/DESIGN_GUIDELINES.md index dd96132d9..98307a6a5 100644 --- a/DESIGN_GUIDELINES.md +++ b/DESIGN_GUIDELINES.md @@ -265,7 +265,8 @@ Existing Inconsistencies would not be published, that’s for our internal use | Name | Type | Notes | | --- | --- | --- | -| existingBucketObj? | s3.Bucket | Either this or bucketProps must be provided | +| existingBucketObj? | s3.Bucket | Either this, existingBucketInterface or bucketProps must be provided | +| existingBucketInterface? | s3.IBucket | Either this, existingBucketObject or bucketProps must be provided | | bucketProps? | s3.BucketProps | | | s3EventTypes? | s3.EventType | Only required when construct responds to S3 events | | s3EventFilters? | s3.NotificationKeyFilter |Only required when construct responds to S3 events | From ea024fc87f40b288fc47f3a681907193c0f7ca6c Mon Sep 17 00:00:00 2001 From: surukonda <65268382+surukonda@users.noreply.github.com> Date: Thu, 6 Jan 2022 20:31:21 +0530 Subject: [PATCH 2/2] feat(aws-iot-s3): new construct implementation (#469) * aws-iot-s3 construt implementation * fix broken test * fix cfn_nag issues * update KMS Key test * fix pr comments * update test cases & address pr review comments * fix tests for cdk v2 * clean access logging buckets in integ tests * Used IBucket instead of Bucket as existing bucket prop * address pr review comments Co-authored-by: santhosh <> --- .../aws-iot-s3/.eslintignore | 4 + .../aws-iot-s3/.gitignore | 15 + .../aws-iot-s3/.npmignore | 21 + .../aws-iot-s3/README.md | 106 +++++ .../aws-iot-s3/architecture.png | Bin 0 -> 21383 bytes .../aws-iot-s3/lib/index.ts | 119 ++++++ .../aws-iot-s3/package.json | 92 +++++ .../integ.iot-s3-defaultprops.expected.json | 215 ++++++++++ .../test/integ.iot-s3-defaultprops.ts | 38 ++ ...integ.iot-s3-existing-bucket.expected.json | 215 ++++++++++ .../test/integ.iot-s3-existing-bucket.ts | 49 +++ ....iot-s3-new-encrypted-bucket.expected.json | 371 ++++++++++++++++++ .../test/integ.iot-s3-new-encrypted-bucket.ts | 50 +++ .../aws-iot-s3/test/iot-s3.test.ts | 322 +++++++++++++++ 14 files changed, 1617 insertions(+) create mode 100644 source/patterns/@aws-solutions-constructs/aws-iot-s3/.eslintignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-iot-s3/.gitignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-iot-s3/.npmignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-iot-s3/README.md create mode 100644 source/patterns/@aws-solutions-constructs/aws-iot-s3/architecture.png create mode 100644 source/patterns/@aws-solutions-constructs/aws-iot-s3/lib/index.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-iot-s3/package.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-defaultprops.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-defaultprops.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-existing-bucket.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-existing-bucket.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-new-encrypted-bucket.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-new-encrypted-bucket.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-iot-s3/test/iot-s3.test.ts diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-s3/.eslintignore b/source/patterns/@aws-solutions-constructs/aws-iot-s3/.eslintignore new file mode 100644 index 000000000..910cb0513 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-s3/.eslintignore @@ -0,0 +1,4 @@ +lib/*.js +test/*.js +*.d.ts +coverage \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-s3/.gitignore b/source/patterns/@aws-solutions-constructs/aws-iot-s3/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-s3/.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-iot-s3/.npmignore b/source/patterns/@aws-solutions-constructs/aws-iot-s3/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-s3/.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-iot-s3/README.md b/source/patterns/@aws-solutions-constructs/aws-iot-s3/README.md new file mode 100644 index 000000000..6db3afc11 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-s3/README.md @@ -0,0 +1,106 @@ +# aws-iot-s3 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_iot_s3`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png) Typescript|`@aws-solutions-constructs/aws-iot-s3`| +|![Java Logo](https://docs.aws.amazon.com/cdk/api/latest/img/java32.png) Java|`software.amazon.awsconstructs.services.iots3`| + +This AWS Solutions Construct implements an AWS IoT MQTT topic rule and an Amazon S3 Bucket pattern. + +Here is a minimal deployable pattern definition in Typescript: + +``` typescript +const { IotToS3Props, IotToS3 } from '@aws-solutions-constructs/aws-iot-s3'; + +const props: IotToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Testing the IotToS3 Pattern", + sql: "SELECT * FROM 'solutions/constructs'", + actions: [] + } + } +}; + +new IotToS3(this, 'test-iot-s3-integration', props); +``` + +## Initializer + +``` text +new IotToS3(scope: Construct, id: string, props: IotToS3Props); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`IotToS3Props`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|existingBucketInterface?|[`s3.IBucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.IBucket.html)|Existing S3 Bucket interface. Providing this property and `bucketProps` results in an error.| +|bucketProps?|[`s3.BucketProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html)|Optional user provided props to override the default props for the S3 Bucket. Providing this and `existingBucketObj` reults in an error.| +|loggingBucketProps?|[`s3.BucketProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html)|Optional user provided props to override the default props for the S3 Logging Bucket.| +|iotTopicRuleProps?|[`iot.CfnTopicRuleProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iot.CfnTopicRuleProps.html)|User provided CfnTopicRuleProps to override the defaults.| +|s3Key|`string`|User provided s3Key to override the default (`${topic()}/${timestamp()}`) object key. Used to store messages matched by the IoT Rule.| +|logS3AccessLogs?|`boolean`|Whether to turn on Access Logging for the S3 bucket. Creates an S3 bucket with associated storage costs for the logs. Enabling Access Logging is a best practice. default - true| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|s3Bucket?|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Returns an instance of the S3 bucket created by the pattern. If an existingBucketInterface is provided in IotToS3Props, then this value will be undefined| +|s3BucketInterface?|[`s3.IBucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.IBucket.html)|Returns S3 Bucket interface created or used by the pattern. If an existingBucketInterface is provided in IotToS3Props, then only this value will be set and s3Bucket will be undefined. If the construct creates the bucket, then both properties will be set.| +|s3LoggingBucket?|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Returns an instance of `s3.Bucket` created by the construct as the logging bucket for the primary bucket.| +|iotActionsRole|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iam.Role.html)|Returns an instance of `iam.Role` created by the construct, which allows IoT to publish messages to the S3 bucket.| +|iotTopicRule|[`iot.CfnTopicRule`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iot.CfnTopicRule.html)|Returns an instance of `iot.CfnTopicRule` created by the construct| + +## Default settings + +Out of the box implementation of the Construct without any override will set the following defaults: + +### Amazon IoT Rule + +* Configure an IoT Rule to send messages to the S3 Bucket + +### Amazon IAM Role + +* Configure least privilege access IAM role for Amazon IoT to be able to publish messages to the S3 Bucket + +### Amazon S3 Bucket + +* Configure Access logging for S3 Bucket +* Enable server-side encryption for S3 Bucket using AWS managed KMS Key +* Enforce encryption of data in transit +* Turn on the versioning for S3 Bucket +* Don't allow public access for S3 Bucket +* Retain the S3 Bucket when deleting the CloudFormation stack +* Applies Lifecycle rule to move noncurrent object versions to Glacier storage after 90 days + +## Architecture + +![Architecture Diagram](architecture.png) + +--- +© Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-s3/architecture.png b/source/patterns/@aws-solutions-constructs/aws-iot-s3/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..1f9b4489ecd93103914b973352565e51c357ffff GIT binary patch literal 21383 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", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-defaultprops.ts b/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-defaultprops.ts new file mode 100644 index 000000000..a043c263e --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-defaultprops.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, RemovalPolicy, Stack } from "@aws-cdk/core"; +import { IotToS3, IotToS3Props } from "../lib"; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; + +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); + +const props: IotToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "process solutions constructs messages", + sql: "SELECT * FROM 'solutions/constructs'", + actions: [] + } + }, + logS3AccessLogs: false, + bucketProps: { + removalPolicy: RemovalPolicy.DESTROY, + serverAccessLogsPrefix: 'logs' + } +}; +new IotToS3(stack, 'test-iot-s3-integration', props); +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-existing-bucket.expected.json b/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-existing-bucket.expected.json new file mode 100644 index 000000000..11b70045c --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-existing-bucket.expected.json @@ -0,0 +1,215 @@ +{ + "Resources": { + "S3Bucket07682993": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "LifecycleConfiguration": { + "Rules": [ + { + "NoncurrentVersionTransitions": [ + { + "StorageClass": "GLACIER", + "TransitionInDays": 90 + } + ], + "Status": "Enabled" + } + ] + }, + "LoggingConfiguration": { + "LogFilePrefix": "logs" + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "S3BucketPolicyF560589A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "S3Bucket07682993" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "S3Bucket07682993", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "S3Bucket07682993", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "testiots3integrationiotactionsrole04473665": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testiots3integrationiotactionsroleDefaultPolicy735A8FB6": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "S3Bucket07682993", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "S3Bucket07682993", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testiots3integrationiotactionsroleDefaultPolicy735A8FB6", + "Roles": [ + { + "Ref": "testiots3integrationiotactionsrole04473665" + } + ] + } + }, + "testiots3integrationIotTopicRule0C8409CE": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "S3": { + "BucketName": { + "Ref": "S3Bucket07682993" + }, + "Key": "test/${timestamp()}", + "RoleArn": { + "Fn::GetAtt": [ + "testiots3integrationiotactionsrole04473665", + "Arn" + ] + } + } + } + ], + "Description": "process solutions constructs messages", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'solutions/constructs'" + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-existing-bucket.ts b/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-existing-bucket.ts new file mode 100644 index 000000000..06e820aeb --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-existing-bucket.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, RemovalPolicy, Stack } from "@aws-cdk/core"; +import { IotToS3, IotToS3Props } from "../lib"; +import * as defaults from '@aws-solutions-constructs/core'; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; +import { BucketEncryption } from "@aws-cdk/aws-s3"; + +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); + +let existingBucketObj; + +[existingBucketObj] = defaults.buildS3Bucket(stack, { + bucketProps: { + removalPolicy: RemovalPolicy.DESTROY, + encryption: BucketEncryption.KMS_MANAGED, + serverAccessLogsPrefix: 'logs' + }, + logS3AccessLogs: false +}); +const props: IotToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "process solutions constructs messages", + sql: "SELECT * FROM 'solutions/constructs'", + actions: [] + } + }, + existingBucketInterface: existingBucketObj, + s3Key: 'test/${timestamp()}' +}; + +new IotToS3(stack, 'test-iot-s3-integration', props); + +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-new-encrypted-bucket.expected.json b/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-new-encrypted-bucket.expected.json new file mode 100644 index 000000000..52c1a2758 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-new-encrypted-bucket.expected.json @@ -0,0 +1,371 @@ +{ + "Resources": { + "existingKeyB52D6AF1": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "EnableKeyRotation": true + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "testiots3integrationS3LoggingBucket606446CC": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket" + } + ] + } + } + }, + "testiots3integrationS3LoggingBucketPolicy2DB45D12": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "testiots3integrationS3LoggingBucket606446CC" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "testiots3integrationS3LoggingBucket606446CC", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testiots3integrationS3LoggingBucket606446CC", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "testiots3integrationS3Bucket9B8B180C": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": [ + "existingKeyB52D6AF1", + "Arn" + ] + }, + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "LifecycleConfiguration": { + "Rules": [ + { + "NoncurrentVersionTransitions": [ + { + "StorageClass": "GLACIER", + "TransitionInDays": 90 + } + ], + "Status": "Enabled" + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Ref": "testiots3integrationS3LoggingBucket606446CC" + } + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "testiots3integrationS3BucketPolicy18905375": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "testiots3integrationS3Bucket9B8B180C" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "testiots3integrationS3Bucket9B8B180C", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testiots3integrationS3Bucket9B8B180C", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "testiots3integrationiotactionsrole04473665": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testiots3integrationiotactionsroleDefaultPolicy735A8FB6": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testiots3integrationS3Bucket9B8B180C", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testiots3integrationS3Bucket9B8B180C", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:Decrypt" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "existingKeyB52D6AF1", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "existingKeyB52D6AF1", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testiots3integrationiotactionsroleDefaultPolicy735A8FB6", + "Roles": [ + { + "Ref": "testiots3integrationiotactionsrole04473665" + } + ] + } + }, + "testiots3integrationIotTopicRule0C8409CE": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "S3": { + "BucketName": { + "Ref": "testiots3integrationS3Bucket9B8B180C" + }, + "Key": "${topic()}/${timestamp()}", + "RoleArn": { + "Fn::GetAtt": [ + "testiots3integrationiotactionsrole04473665", + "Arn" + ] + } + } + } + ], + "Description": "process solutions constructs messages", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'solutions/constructs'" + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-new-encrypted-bucket.ts b/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-new-encrypted-bucket.ts new file mode 100644 index 000000000..21375e6db --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/integ.iot-s3-new-encrypted-bucket.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, RemovalPolicy, Stack } from "@aws-cdk/core"; +import * as kms from '@aws-cdk/aws-kms'; +import { IotToS3, IotToS3Props } from "../lib"; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; +import { BucketEncryption } from "@aws-cdk/aws-s3"; + +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); +const existingKey = new kms.Key(stack, `existingKey`, { + enableKeyRotation: true, + removalPolicy: RemovalPolicy.DESTROY +}); + +const props: IotToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "process solutions constructs messages", + sql: "SELECT * FROM 'solutions/constructs'", + actions: [] + } + }, + bucketProps: { + encryption: BucketEncryption.KMS, + encryptionKey: existingKey, + removalPolicy: RemovalPolicy.DESTROY + }, + loggingBucketProps: { + encryption: BucketEncryption.KMS_MANAGED, + removalPolicy: RemovalPolicy.DESTROY + } +}; + +new IotToS3(stack, 'test-iot-s3-integration', props); + +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/iot-s3.test.ts b/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/iot-s3.test.ts new file mode 100644 index 000000000..433093095 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-s3/test/iot-s3.test.ts @@ -0,0 +1,322 @@ +/** + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { IotToS3, IotToS3Props } from "../lib"; +import * as cdk from "@aws-cdk/core"; +import '@aws-cdk/assert/jest'; +import * as s3 from "@aws-cdk/aws-s3"; +import { RemovalPolicy } from "@aws-cdk/core"; + +test('check for default props', () => { + const stack = new cdk.Stack(); + + const props: IotToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "process solutions constructs messages", + sql: "SELECT * FROM 'solutions/constructs'", + actions: [] + } + } + }; + const construct = new IotToS3(stack, 'test-iot-s3-integration', props); + + // Check whether construct has two s3 buckets for storing msgs and logging + expect(stack).toCountResources('AWS::S3::Bucket', 2); + + // Check for IoT Topic Rule Definition + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + S3: { + BucketName: { + Ref: "testiots3integrationS3Bucket9B8B180C" + }, + Key: "${topic()}/${timestamp()}", + RoleArn: { + "Fn::GetAtt": [ + "testiots3integrationiotactionsrole04473665", + "Arn" + ] + } + } + } + ], + Description: "process solutions constructs messages", + RuleDisabled: false, + Sql: "SELECT * FROM 'solutions/constructs'" + } + }); + + // Check for IAM policy to have access to s3 bucket + /** + * Due to difference in CDK V1 and V2 Synth, the policy documents doesn't match, hence checking only for number of policies + */ + expect(stack).toCountResources('AWS::IAM::Policy', 1); + + // Check for properties + expect(construct.s3Bucket).toBeDefined(); + expect(construct.s3BucketInterface).toBeDefined(); + expect(construct.s3LoggingBucket).toBeDefined(); + expect(construct.iotActionsRole).toBeDefined(); + expect(construct.iotTopicRule).toBeDefined(); +}); + +test('check for overriden props', () => { + const stack = new cdk.Stack(); + const props: IotToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: true, + description: "process solutions constructs messages", + sql: "SELECT * FROM 'test/constructs'", + actions: [] + } + }, + s3Key: 'test/key', + bucketProps: { + encryption: s3.BucketEncryption.KMS + }, + loggingBucketProps: { + encryption: s3.BucketEncryption.KMS_MANAGED + } + }; + const construct = new IotToS3(stack, 'test-iot-s3-integration', props); + + // Check whether construct has two s3 buckets for storing msgs and logging + expect(stack).toCountResources('AWS::S3::Bucket', 2); + + // Check logging bucket encryption type to be KMS_Managed + expect(stack).toHaveResourceLike('AWS::S3::Bucket', { + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + SSEAlgorithm: "aws:kms" + } + } + ] + } + }); + + // Check for bucket to have KMS CMK Encryption + expect(stack).toHaveResourceLike('AWS::S3::Bucket', { + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + KMSMasterKeyID: { + "Fn::GetAtt": [ + "testiots3integrationS3BucketKey127368C9", + "Arn" + ] + }, + SSEAlgorithm: "aws:kms" + } + } + ] + }, + }); + + // Check for IoT Topic Rule Definition + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + S3: { + BucketName: { + Ref: "testiots3integrationS3Bucket9B8B180C" + }, + Key: "test/key", + RoleArn: { + "Fn::GetAtt": [ + "testiots3integrationiotactionsrole04473665", + "Arn" + ] + } + } + } + ], + Description: "process solutions constructs messages", + RuleDisabled: true, + Sql: "SELECT * FROM 'test/constructs'" + } + }); + + /** + * Due to difference in CDK V1 and V2 Synth, the policy documents doesn't match, hence checking only for number of policies + */ + // Check for automatically created CMK KMS Key + expect(stack).toCountResources('AWS::KMS::Key', 1); + + // Check for IoT Topic Rule permissions to KMS key to store msgs to S3 Bucket and access to put data to s3 bucket + expect(stack).toCountResources('AWS::IAM::Policy', 1); + + // Check for properties + expect(construct.s3Bucket).toBeDefined(); + expect(construct.s3BucketInterface).toBeDefined(); + expect(construct.s3LoggingBucket).toBeDefined(); + expect(construct.iotActionsRole).toBeDefined(); + expect(construct.iotTopicRule).toBeDefined(); +}); + +test('check for existing bucket', () => { + const stack = new cdk.Stack(); + const existingBucket = new s3.Bucket(stack, `existingBucket`); + const props: IotToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "process solutions constructs messages", + sql: "SELECT * FROM 'test/constructs'", + actions: [] + } + }, + s3Key: 'existingtest/key', + existingBucketInterface: existingBucket + }; + const construct = new IotToS3(stack, 'test-iot-s3-integration', props); + + // Check whether construct has a single s3 bucket, no logging bucket should exist since existing bucket is supplied + expect(stack).toCountResources('AWS::S3::Bucket', 1); + + // Check for IoT Topic Rule Definition with existing Bucket Ref + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + S3: { + BucketName: { + Ref: "existingBucket9529822F" + }, + Key: "existingtest/key", + RoleArn: { + "Fn::GetAtt": [ + "testiots3integrationiotactionsrole04473665", + "Arn" + ] + } + } + } + ], + Description: "process solutions constructs messages", + RuleDisabled: false, + Sql: "SELECT * FROM 'test/constructs'" + } + }); + + /** + * Due to difference in CDK V1 and V2 Synth, the policy documents doesn't match, hence checking only for number of policies + */ + // Check for IAM policy to have access to s3 bucket + expect(stack).toCountResources('AWS::IAM::Policy', 1); + + // since existing bucket is supplied, no key should exist + expect(stack).not.toHaveResource('AWS::KMS::Key', {}); + + // Check for IoT Topic Rule permissions to KMS key to store msgs to S3 Bucket + expect(stack).toCountResources("AWS::IAM::Policy", 1); + + // Check for properties + expect(construct.s3Bucket).toBeUndefined(); + expect(construct.s3BucketInterface).toBeDefined(); + expect(construct.s3LoggingBucket).toBeUndefined(); + expect(construct.iotActionsRole).toBeDefined(); + expect(construct.iotTopicRule).toBeDefined(); +}); + +test('check for both bucketProps and existingBucket', () => { + const stack = new cdk.Stack(); + const existingBucket = new s3.Bucket(stack, `existingBucket`, {encryption: s3.BucketEncryption.KMS}); + const props: IotToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "process solutions constructs messages", + sql: "SELECT * FROM 'test/constructs'", + actions: [] + } + }, + bucketProps: { + encryption: s3.BucketEncryption.KMS_MANAGED + }, + existingBucketInterface: existingBucket + }; + + // since bucketprops and existing bucket is supplied, this should result in error + try { + new IotToS3(stack, 'test-iot-s3-integration', props); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); + +test('check for name collision', () => { + const stack = new cdk.Stack(); + const props: IotToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "process solutions constructs messages", + sql: "SELECT * FROM 'test/constructs'", + actions: [] + } + }, + bucketProps: { + autoDeleteObjects: true, + removalPolicy: RemovalPolicy.DESTROY + } + }; + + // since bucketprops and existing bucket is supplied, this should result in error + new IotToS3(stack, 'test-iot-s3-integration', props); + new IotToS3(stack, 'test-iot-s3-integration1', props); + + expect(stack).toCountResources('AWS::IoT::TopicRule', 2); + expect(stack).toCountResources('AWS::S3::Bucket', 4); +}); + +test('check for chaining of resource', () => { + const stack = new cdk.Stack(); + const props: IotToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "process solutions constructs messages", + sql: "SELECT * FROM 'test/constructs'", + actions: [] + } + } + }; + + // since bucketprops and existing bucket is supplied, this should result in error + const construct = new IotToS3(stack, 'test-iot-s3-integration', props); + + const props1: IotToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "process solutions constructs messages", + sql: "SELECT * FROM 'test/constructs'", + actions: [] + } + }, + existingBucketInterface: construct.s3Bucket + }; + new IotToS3(stack, 'test-iot-s3-integration1', props1); + + expect(stack).toCountResources('AWS::IoT::TopicRule', 2); + expect(stack).toCountResources('AWS::S3::Bucket', 2); +}); \ No newline at end of file