From ec47e4ad902cb7b485dd499c242d30e57842dec1 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 2 Nov 2018 13:36:09 -0700 Subject: [PATCH] feat(aws-s3): add the option to not poll with CloudTrail to the CodePipeline Action. --- .../@aws-cdk/aws-cloudtrail-api/.gitignore | 15 ++ .../@aws-cdk/aws-cloudtrail-api/.npmignore | 16 ++ packages/@aws-cdk/aws-cloudtrail-api/LICENSE | 201 +++++++++++++++ packages/@aws-cdk/aws-cloudtrail-api/NOTICE | 2 + .../@aws-cdk/aws-cloudtrail-api/README.md | 5 + .../@aws-cdk/aws-cloudtrail-api/lib/index.ts | 1 + .../@aws-cdk/aws-cloudtrail-api/lib/trail.ts | 29 +++ .../@aws-cdk/aws-cloudtrail-api/package.json | 65 +++++ packages/@aws-cdk/aws-cloudtrail/README.md | 23 +- packages/@aws-cdk/aws-cloudtrail/lib/index.ts | 31 +-- packages/@aws-cdk/aws-cloudtrail/package.json | 4 +- .../aws-cloudtrail/test/test.cloudtrail.ts | 7 +- .../@aws-cdk/aws-codepipeline/package.json | 1 + .../test/integ.lambda-pipeline.expected.json | 239 +++++++++++++++++- .../test/integ.lambda-pipeline.ts | 3 + packages/@aws-cdk/aws-s3/lib/bucket-policy.ts | 18 +- packages/@aws-cdk/aws-s3/lib/bucket.ts | 48 ++++ .../@aws-cdk/aws-s3/lib/pipeline-action.ts | 38 ++- packages/@aws-cdk/aws-s3/package.json | 6 +- .../aws-s3/test/test.notifications.ts | 90 ++++++- 20 files changed, 794 insertions(+), 48 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudtrail-api/.gitignore create mode 100644 packages/@aws-cdk/aws-cloudtrail-api/.npmignore create mode 100644 packages/@aws-cdk/aws-cloudtrail-api/LICENSE create mode 100644 packages/@aws-cdk/aws-cloudtrail-api/NOTICE create mode 100644 packages/@aws-cdk/aws-cloudtrail-api/README.md create mode 100644 packages/@aws-cdk/aws-cloudtrail-api/lib/index.ts create mode 100644 packages/@aws-cdk/aws-cloudtrail-api/lib/trail.ts create mode 100644 packages/@aws-cdk/aws-cloudtrail-api/package.json diff --git a/packages/@aws-cdk/aws-cloudtrail-api/.gitignore b/packages/@aws-cdk/aws-cloudtrail-api/.gitignore new file mode 100644 index 0000000000000..b668f79a44a03 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudtrail-api/.gitignore @@ -0,0 +1,15 @@ +*.js +tsconfig.json +tslint.json +*.js.map +*.d.ts +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudtrail-api/.npmignore b/packages/@aws-cdk/aws-cloudtrail-api/.npmignore new file mode 100644 index 0000000000000..b757d55c46996 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudtrail-api/.npmignore @@ -0,0 +1,16 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudtrail-api/LICENSE b/packages/@aws-cdk/aws-cloudtrail-api/LICENSE new file mode 100644 index 0000000000000..1739faaebb745 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudtrail-api/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2018 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. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-cloudtrail-api/NOTICE b/packages/@aws-cdk/aws-cloudtrail-api/NOTICE new file mode 100644 index 0000000000000..95fd48569c743 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudtrail-api/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-cloudtrail-api/README.md b/packages/@aws-cdk/aws-cloudtrail-api/README.md new file mode 100644 index 0000000000000..c3e9603089970 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudtrail-api/README.md @@ -0,0 +1,5 @@ +## Internal API for AWS CloudTrail + +This package is used for breaking a cyclical dependency between the CloudTrail and S3 modules. +You should never need to depend on it directly - +instead, depend on the `@aws-cdk/aws-cloudtrail` package. diff --git a/packages/@aws-cdk/aws-cloudtrail-api/lib/index.ts b/packages/@aws-cdk/aws-cloudtrail-api/lib/index.ts new file mode 100644 index 0000000000000..fd59e8b2b7d48 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudtrail-api/lib/index.ts @@ -0,0 +1 @@ +export * from './trail'; diff --git a/packages/@aws-cdk/aws-cloudtrail-api/lib/trail.ts b/packages/@aws-cdk/aws-cloudtrail-api/lib/trail.ts new file mode 100644 index 0000000000000..06af1d964fcc3 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudtrail-api/lib/trail.ts @@ -0,0 +1,29 @@ +/** + * What events to subscribe to - + * read events, write events, or both. + */ +export enum ReadWriteType { + ReadOnly = "ReadOnly", + WriteOnly = "WriteOnly", + All = "All" +} + +/** + * The abstract interface of a CloudTrail Trail. + */ +export interface ITrail { + /** + * When an event occurs in your account, + * CloudTrail evaluates whether the event matches the settings for your trails. + * Only events that match your trail settings are delivered to your Amazon S3 bucket and Amazon CloudWatch Logs log group. + * + * This method adds an S3 Data Event Selector for filtering events that match S3 operations. + * + * Data events: These events provide insight into the resource operations performed on or within a resource. + * These are also known as data plane operations. + * + * @param readWriteType the configuration type to log for this data event; + * eg, `ReadWriteType.ReadOnly` will only log "read" events for S3 objects that match a filter + */ + addS3EventSelector(prefixes: string[], readWriteType: ReadWriteType): void; +} diff --git a/packages/@aws-cdk/aws-cloudtrail-api/package.json b/packages/@aws-cdk/aws-cloudtrail-api/package.json new file mode 100644 index 0000000000000..e2ffc740db019 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudtrail-api/package.json @@ -0,0 +1,65 @@ +{ + "name": "@aws-cdk/aws-cloudtrail-api", + "version": "0.18.1", + "description": "Internal API for AWS CloudTrail", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.services.cloudtrail.api", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "cloudtrail-api" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.AWS.CloudTrail.Api", + "packageId": "Amazon.CDK.AWS.CloudTrail.Api", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk" + }, + "sphinx": {} + } + }, + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-cdk.git" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "pkglint": "pkglint -f", + "package": "cdk-package" + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "cloudtrail" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "^0.18.1", + "aws-sdk": "^2.259.1", + "cdk-build-tools": "^0.18.1", + "cfn2ts": "^0.18.1", + "colors": "^1.2.1", + "pkglint": "^0.18.1" + }, + "dependencies": { + "@aws-cdk/cdk": "^0.18.1" + }, + "homepage": "https://github.com/awslabs/aws-cdk", + "peerDependencies": { + "@aws-cdk/cdk": "^0.18.1" + } +} diff --git a/packages/@aws-cdk/aws-cloudtrail/README.md b/packages/@aws-cdk/aws-cloudtrail/README.md index 024951eadb889..4073ba725a534 100644 --- a/packages/@aws-cdk/aws-cloudtrail/README.md +++ b/packages/@aws-cdk/aws-cloudtrail/README.md @@ -35,21 +35,26 @@ const trail = new cloudtrail.CloudTrail(stack, 'CloudTrail', { }); ``` -This creates the same setup as above - but also logs events to a created CloudWatch Log stream. By default, the created log group has a retention period of 365 Days, but this is also configurable. +This creates the same setup as above - but also logs events to a created CloudWatch Log stream. +By default, the created log group has a retention period of 365 Days, but this is also configurable. - -For using CloudTrail event selector to log specific S3 events, you can use the `CloudTrailProps` configuration object - -For example - this logs all ReadWriteEvents for the `magic-bucket` bucket: +For using CloudTrail event selector to log specific S3 events, +you can use the `CloudTrailProps` configuration object. +Example: ```ts import cloudtrail = require('@aws-cdk/aws-cloudtrail'); +import ctapi = require('@aws-cdk/aws-cloudtrail-api'); -const trail = new cloudtrail.CloudTrail(stack, 'MyAmazingCloudTrail') +const trail = new cloudtrail.CloudTrail(stack, 'MyAmazingCloudTrail'); -trail.addS3Filter("arn:aws:s3:::magic-bucket/"); // Adds an event selector to the bucket magic-bucket. By default, this includes management events and all operations (Read + Write) +// Adds an event selector to the bucket magic-bucket. +// By default, this includes management events and all operations (Read + Write) +trail.addS3Filter(["arn:aws:s3:::magic-bucket/"]); -const configuration = { includeManagementEvents = false, readWriteType = ReadWriteType.All }; -trail.addS3Filter(["arn:aws:s3:::foo"], configuration ); // Adds an event selector to the bucket foo, with a specific configuration +// Adds an event selector to the bucket foo, with a specific configuration +trail.addS3Filter(["arn:aws:s3:::foo"], { + includeManagementEvents: false, + readWriteType: ctapi.ReadWriteType.All, }); ``` diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts index 792d86b37f958..87eca314f3775 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts @@ -1,3 +1,4 @@ +import ctapi = require('@aws-cdk/aws-cloudtrail-api'); import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import logs = require('@aws-cdk/aws-logs'); @@ -37,7 +38,7 @@ export interface CloudTrailProps { * If managementEvents is undefined, we'll not log management events by default. * @param managementEvents the management configuration type to log */ - managementEvents?: ReadWriteType; + managementEvents?: ctapi.ReadWriteType; /** * To determine whether a log file was modified, deleted, or unchanged after CloudTrail delivered it, @@ -84,12 +85,6 @@ export interface CloudTrailProps { s3KeyPrefix?: string; } -export enum ReadWriteType { - ReadOnly = "ReadOnly", - WriteOnly = "WriteOnly", - All = "All" -} - // TODO: This belongs in a CWL L2 export enum LogRetention { OneDay = 1, @@ -119,8 +114,7 @@ export enum LogRetention { * const cloudTrail = new CloudTrail(this, 'MyTrail'); * */ -export class CloudTrail extends cdk.Construct { - +export class CloudTrail extends cdk.Construct implements ctapi.ITrail { public readonly cloudTrailArn: string; private readonly cloudWatchLogsRoleArn?: string; private readonly cloudWatchLogsGroupArn?: string; @@ -138,7 +132,7 @@ export class CloudTrail extends cdk.Construct { .addServicePrincipal(cloudTrailPrincipal)); s3bucket.addToResourcePolicy(new iam.PolicyStatement() - .addResource(s3bucket.arnForObjects(new cdk.FnConcat('/AWSLogs/', new cdk.AwsAccountId()))) + .addResource(s3bucket.arnForObjects('AWSLogs/', new cdk.AwsAccountId(), '/*')) .addActions("s3:PutObject") .addServicePrincipal(cloudTrailPrincipal) .setCondition("StringEquals", {'s3:x-amz-acl': "bucket-owner-full-control"})); @@ -182,20 +176,11 @@ export class CloudTrail extends cdk.Construct { eventSelectors: this.eventSelectors }); this.cloudTrailArn = trail.trailArn; + // required, otherwise we have a race condition in CloudTrail + s3bucket.dependOnBucketPolicy(trail); } - /** - * When an event occurs in your account, CloudTrail evaluates whether the event matches the settings for your trails. - * Only events that match your trail settings are delivered to your Amazon S3 bucket and Amazon CloudWatch Logs log group. - * - * This method adds an S3 Data Event Selector for filtering events that match S3 operations. - * - * Data events: These events provide insight into the resource operations performed on or within a resource. - * These are also known as data plane operations. - * @param readWriteType the configuration type to log for this data event - * Eg, ReadWriteType.ReadOnly will only log "read" events for S3 objects that match a filter) - */ - public addS3EventSelector(prefixes: string[], readWriteType: ReadWriteType) { + public addS3EventSelector(prefixes: string[], readWriteType: ctapi.ReadWriteType) { if (prefixes.length > 250) { throw new Error("A maximum of 250 data elements can be in one event selector"); } @@ -215,7 +200,7 @@ export class CloudTrail extends cdk.Construct { interface EventSelector { readonly includeManagementEvents?: boolean; - readonly readWriteType?: ReadWriteType; + readonly readWriteType?: ctapi.ReadWriteType; readonly dataResources?: EventSelectorData[]; } diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index 8c5e5c4a2ae2c..d8aefbec9db55 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -59,6 +59,7 @@ "pkglint": "^0.18.1" }, "dependencies": { + "@aws-cdk/aws-cloudtrail-api": "^0.18.1", "@aws-cdk/aws-iam": "^0.18.1", "@aws-cdk/aws-kms": "^0.18.1", "@aws-cdk/aws-logs": "^0.18.1", @@ -67,7 +68,8 @@ }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-cloudtrail-api": "^0.18.1", "@aws-cdk/aws-kms": "^0.18.1", "@aws-cdk/cdk": "^0.18.1" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts index 84aa74638261c..186e11ebb1a67 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts @@ -1,7 +1,8 @@ import { expect, haveResource, not } from '@aws-cdk/assert'; +import ctapi = require('@aws-cdk/aws-cloudtrail-api'); import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; -import { CloudTrail, LogRetention, ReadWriteType } from '../lib'; +import { CloudTrail, LogRetention } from '../lib'; export = { 'constructs the expected resources': { @@ -56,7 +57,7 @@ export = { const stack = getTestStack(); const cloudTrail = new CloudTrail(stack, 'MyAmazingCloudTrail'); - cloudTrail.addS3EventSelector(["arn:aws:s3:::"], ReadWriteType.All); + cloudTrail.addS3EventSelector(["arn:aws:s3:::"], ctapi.ReadWriteType.All); expect(stack).to(haveResource("AWS::CloudTrail::Trail")); expect(stack).to(haveResource("AWS::S3::Bucket")); @@ -80,7 +81,7 @@ export = { 'with management event'(test: Test) { const stack = getTestStack(); - new CloudTrail(stack, 'MyAmazingCloudTrail', {managementEvents: ReadWriteType.WriteOnly}); + new CloudTrail(stack, 'MyAmazingCloudTrail', {managementEvents: ctapi.ReadWriteType.WriteOnly}); const trail: any = stack.toCloudFormation().Resources.MyAmazingCloudTrail54516E8D; test.equals(trail.Properties.EventSelectors.length, 1); diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index 958b3c0015916..c9723b1c53120 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -60,6 +60,7 @@ "devDependencies": { "@aws-cdk/assert": "^0.18.1", "@aws-cdk/aws-cloudformation": "^0.18.1", + "@aws-cdk/aws-cloudtrail": "^0.18.1", "@aws-cdk/aws-codebuild": "^0.18.1", "@aws-cdk/aws-codecommit": "^0.18.1", "@aws-cdk/aws-codedeploy": "^0.18.1", diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.expected.json index c8b43657dfa5c..6dbd31b49d8d4 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.expected.json @@ -145,7 +145,7 @@ "Ref": "PipelineBucketB967BD35" }, "S3ObjectKey": "key", - "PollForSourceChanges": true + "PollForSourceChanges": false }, "InputArtifacts": [], "Name": "Source", @@ -188,6 +188,66 @@ "PipelineRoleDefaultPolicyC7A05455" ] }, + "PipelineEventsRole46BEEA7C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineEventsRoleDefaultPolicyFF4FCCE0": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "codepipeline:StartPipelineExecution", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codepipeline:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "PipelineC660917D" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineEventsRoleDefaultPolicyFF4FCCE0", + "Roles": [ + { + "Ref": "PipelineEventsRole46BEEA7C" + } + ] + } + }, "PipelineBucketB967BD35": { "Type": "AWS::S3::Bucket", "Properties": { @@ -196,6 +256,181 @@ } } }, + "PipelineBucketawscdkcodepipelinelambdaPipeline87A4B3D3SourceEventRuleCE4D4505": { + "Type": "AWS::Events::Rule", + "Properties": { + "EventPattern": { + "source": [ + "aws.s3" + ], + "detail-type": [ + "AWS API Call via CloudTrail" + ], + "detail": { + "eventSource": [ + "s3.amazonaws.com" + ], + "eventName": [ + "PutObject" + ], + "resources": { + "ARN": [ + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/key" + ] + ] + } + ] + } + } + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codepipeline:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "PipelineC660917D" + } + ] + ] + }, + "Id": "Pipeline", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineEventsRole46BEEA7C", + "Arn" + ] + } + } + ] + } + }, + "CloudTrailS310CD22F2": { + "Type": "AWS::S3::Bucket" + }, + "CloudTrailS3PolicyEA49A03E": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "CloudTrailS310CD22F2" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetBucketAcl", + "Effect": "Allow", + "Principal": { + "Service": "cloudtrail.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "CloudTrailS310CD22F2", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", + "Condition": { + "StringEquals": { + "s3:x-amz-acl": "bucket-owner-full-control" + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cloudtrail.amazonaws.com" + }, + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CloudTrailS310CD22F2", + "Arn" + ] + }, + "/AWSLogs/", + { + "Ref": "AWS::AccountId" + }, + "/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + } + } + }, + "CloudTrailA62D711D": { + "Type": "AWS::CloudTrail::Trail", + "Properties": { + "IsLogging": true, + "S3BucketName": { + "Ref": "CloudTrailS310CD22F2" + }, + "EnableLogFileValidation": true, + "EventSelectors": [ + { + "DataResources": [ + { + "Type": "AWS::S3::Object", + "Values": [ + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/key" + ] + ] + } + ] + } + ], + "IncludeManagementEvents": false, + "ReadWriteType": "WriteOnly" + } + ], + "IncludeGlobalServiceEvents": true, + "IsMultiRegionTrail": true + }, + "DependsOn": [ + "CloudTrailS3PolicyEA49A03E" + ] + }, "LambdaFunServiceRoleF0979767": { "Type": "AWS::IAM::Role", "Properties": { @@ -272,4 +507,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.ts b/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.ts index b623e59d132ca..b5ecbe14cecfb 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.lambda-pipeline.ts @@ -1,3 +1,4 @@ +import cloudtrail = require('@aws-cdk/aws-cloudtrail'); import lambda = require('@aws-cdk/aws-lambda'); import s3 = require('@aws-cdk/aws-s3'); import cdk = require('@aws-cdk/cdk'); @@ -13,11 +14,13 @@ const sourceStage = new codepipeline.Stage(pipeline, 'Source', { pipeline }); const bucket = new s3.Bucket(stack, 'PipelineBucket', { versioned: true, }); +const trail = new cloudtrail.CloudTrail(stack, 'CloudTrail'); new s3.PipelineSourceAction(stack, 'Source', { stage: sourceStage, outputArtifactName: 'SourceArtifact', bucket, bucketKey: 'key', + cloudTrail: trail, }); const lambdaFun = new lambda.Function(stack, 'LambdaFun', { diff --git a/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts b/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts index 280304d837688..3650daaf29853 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts @@ -1,5 +1,5 @@ import { PolicyDocument } from '@aws-cdk/aws-iam'; -import { Construct } from '@aws-cdk/cdk'; +import cdk = require('@aws-cdk/cdk'); import { BucketRef } from './bucket'; import { cloudformation } from './s3.generated'; @@ -13,7 +13,7 @@ export interface BucketPolicyProps { /** * Applies an Amazon S3 bucket policy to an Amazon S3 bucket. */ -export class BucketPolicy extends Construct { +export class BucketPolicy extends cdk.Construct { /** * A policy document containing permissions to add to the specified bucket. @@ -21,17 +21,27 @@ export class BucketPolicy extends Construct { * Simple Storage Service Developer Guide. */ public readonly document = new PolicyDocument(); + private readonly bucketPolicyResource: cloudformation.BucketPolicyResource; - constructor(parent: Construct, name: string, props: BucketPolicyProps) { + constructor(parent: cdk.Construct, name: string, props: BucketPolicyProps) { super(parent, name); if (!props.bucket.bucketName) { throw new Error('Bucket doesn\'t have a bucketName defined'); } - new cloudformation.BucketPolicyResource(this, 'Resource', { + this.bucketPolicyResource = new cloudformation.BucketPolicyResource(this, 'Resource', { bucket: props.bucket.bucketName, policyDocument: this.document, }); } + + /** + * Makes the given resource depend on this Bucket Policy. + * + * @param resource the resource to make dependent on this Bucket Policy + */ + public addDependant(resource: cdk.Resource): void { + resource.addDependency(this.bucketPolicyResource); + } } diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index ffa144a1cf2f9..be9ef235e3be9 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -1,4 +1,5 @@ import actions = require('@aws-cdk/aws-codepipeline-api'); +import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import { IBucketNotificationDestination } from '@aws-cdk/aws-s3-notifications'; @@ -131,6 +132,42 @@ export abstract class BucketRef extends cdk.Construct { }); } + /** + * Defines a CloudWatch Event Rule that triggers upon putting an object into the Bucket. + * + * @param name the logical ID of the newly created Event Rule + * @param target the target of the Event Rule + * @param path the optional path inside the Bucket that will be watched for changes + * @returns a new {@link events.EventRule} instance + */ + public onPutObject(name: string, target?: events.IEventRuleTarget, path?: string): events.EventRule { + const eventRule = new events.EventRule(this, name, { + eventPattern: { + source: [ + 'aws.s3', + ], + detailType: [ + 'AWS API Call via CloudTrail', + ], + detail: { + eventSource: [ + 's3.amazonaws.com', + ], + eventName: [ + 'PutObject', + ], + resources: { + ARN: [ + path ? this.arnForObjects(path) : this.bucketArn, + ], + }, + }, + }, + }); + eventRule.addTarget(target); + return eventRule; + } + /** * Adds a statement to the resource policy for a principal (i.e. * account/role/service) to perform actions on this bucket and/or it's @@ -147,6 +184,17 @@ export abstract class BucketRef extends cdk.Construct { } } + /** + * Makes the given Resource depend on the Bucket's resource Policy document. + * + * @param resource the resource to make dependent on the Bucket's resource Policy + */ + public dependOnBucketPolicy(resource: cdk.Resource): void { + if (this.policy) { + this.policy.addDependant(resource); + } + } + /** * The https:// URL of this bucket. * @example https://s3.us-west-1.amazonaws.com/onlybucket diff --git a/packages/@aws-cdk/aws-s3/lib/pipeline-action.ts b/packages/@aws-cdk/aws-s3/lib/pipeline-action.ts index a4f03b9c79be5..0dc7f850b8656 100644 --- a/packages/@aws-cdk/aws-s3/lib/pipeline-action.ts +++ b/packages/@aws-cdk/aws-s3/lib/pipeline-action.ts @@ -1,3 +1,4 @@ +import cloudtrail = require('@aws-cdk/aws-cloudtrail-api'); import codepipeline = require('@aws-cdk/aws-codepipeline-api'); import cdk = require('@aws-cdk/cdk'); import { BucketRef } from './bucket'; @@ -23,11 +24,20 @@ export interface CommonPipelineSourceActionProps extends codepipeline.CommonActi */ bucketKey: string; - // TODO: use CloudWatch events instead /** - * Whether or not AWS CodePipeline should poll for source changes + * The AWS CloudTrail Trail used for the source Bucket. + * Note that this parameter is required if `pollForSourceChanges` is `false`. * - * @default true + * @default the source Bucket will not be added to a Trail + */ + cloudTrail?: cloudtrail.ITrail; + + /** + * Whether AWS CodePipeline should poll for source changes. + * If this is `false`, the Pipeline will use CloudWatch Events to detect source changes instead. + * Note that if this is `false`, then the `cloudTrail` property is required. + * + * @default `false` if the `cloudTrail` property was provided, `true` otherwise */ pollForSourceChanges?: boolean; } @@ -53,12 +63,32 @@ export class PipelineSourceAction extends codepipeline.SourceAction { configuration: { S3Bucket: props.bucket.bucketName, S3ObjectKey: props.bucketKey, - PollForSourceChanges: props.pollForSourceChanges || true + PollForSourceChanges: shouldPollForSourceChanges(props), }, ...props, }); + if (props.pollForSourceChanges === false && props.cloudTrail === undefined) { + throw new Error('When not polling for source changes, ' + + 'the cloudTrail property is required in the S3 source CodePipeline Action'); + } + + const pollForSourceChanges = shouldPollForSourceChanges(props); + if (!pollForSourceChanges) { + props.cloudTrail!.addS3EventSelector([ + props.bucket.arnForObjects(props.bucketKey), + ], cloudtrail.ReadWriteType.WriteOnly); + props.bucket.onPutObject(props.stage.pipeline.uniqueId + 'SourceEventRule', + props.stage.pipeline, props.bucketKey); + } + // pipeline needs permissions to read from the S3 bucket props.bucket.grantRead(props.stage.pipeline.role); } } + +function shouldPollForSourceChanges(props: PipelineSourceActionProps): boolean { + return props.pollForSourceChanges === undefined + ? props.cloudTrail === undefined + : props.pollForSourceChanges; +} diff --git a/packages/@aws-cdk/aws-s3/package.json b/packages/@aws-cdk/aws-s3/package.json index 76044d9f465f0..1bae922ea3ecd 100644 --- a/packages/@aws-cdk/aws-s3/package.json +++ b/packages/@aws-cdk/aws-s3/package.json @@ -59,7 +59,9 @@ "pkglint": "^0.18.1" }, "dependencies": { + "@aws-cdk/aws-cloudtrail-api": "^0.18.1", "@aws-cdk/aws-codepipeline-api": "^0.18.1", + "@aws-cdk/aws-events": "^0.18.1", "@aws-cdk/aws-iam": "^0.18.1", "@aws-cdk/aws-kms": "^0.18.1", "@aws-cdk/aws-s3-notifications": "^0.18.1", @@ -67,10 +69,12 @@ }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-cloudtrail-api": "^0.18.1", "@aws-cdk/aws-codepipeline-api": "^0.18.1", + "@aws-cdk/aws-events": "^0.18.1", "@aws-cdk/aws-iam": "^0.18.1", "@aws-cdk/aws-kms": "^0.18.1", "@aws-cdk/aws-s3-notifications": "^0.18.1", "@aws-cdk/cdk": "^0.18.1" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-s3/test/test.notifications.ts b/packages/@aws-cdk/aws-s3/test/test.notifications.ts index 69e183f98049a..7e93c608a482a 100644 --- a/packages/@aws-cdk/aws-s3/test/test.notifications.ts +++ b/packages/@aws-cdk/aws-s3/test/test.notifications.ts @@ -298,5 +298,93 @@ export = { }); test.done(); - } + }, + + 'CloudWatch Events': { + 'onPutItem contains the Bucket ARN itself when path is undefined'(test: Test) { + const stack = new cdk.Stack(); + const bucket = s3.BucketRef.import(stack, 'Bucket', { + bucketName: 'MyBucket', + }); + bucket.onPutObject('PutRule'); + + expect(stack).to(haveResource('AWS::Events::Rule', { + "EventPattern": { + "source": [ + "aws.s3", + ], + "detail": { + "eventSource": [ + "s3.amazonaws.com", + ], + "eventName": [ + "PutObject", + ], + "resources": { + "ARN": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":s3:::MyBucket", + ], + ], + }, + ], + }, + }, + }, + "State": "ENABLED", + })); + + test.done(); + }, + + "onPutItem contains the path when it's provided"(test: Test) { + const stack = new cdk.Stack(); + const bucket = s3.BucketRef.import(stack, 'Bucket', { + bucketName: 'MyBucket', + }); + bucket.onPutObject('PutRule', undefined, 'my/path.zip'); + + expect(stack).to(haveResource('AWS::Events::Rule', { + "EventPattern": { + "source": [ + "aws.s3", + ], + "detail": { + "eventSource": [ + "s3.amazonaws.com", + ], + "eventName": [ + "PutObject", + ], + "resources": { + "ARN": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":s3:::MyBucket/my/path.zip" + ], + ], + }, + ], + }, + }, + }, + "State": "ENABLED", + })); + + test.done(); + }, + }, };