Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(cloudtrail): accept existing S3 bucket #3680

Merged
merged 18 commits into from
Aug 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 31 additions & 17 deletions packages/@aws-cdk/aws-cloudtrail/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ export interface TrailProps {
* @default - No prefix.
*/
readonly s3KeyPrefix?: string;

/** The Amazon S3 bucket
*
* @default - if not supplied a bucket will be created with all the correct permisions
*/
readonly bucket?: s3.IBucket
}

export enum ReadWriteType {
Expand All @@ -109,6 +115,10 @@ export enum ReadWriteType {
*
* const cloudTrail = new CloudTrail(this, 'MyTrail');
*
* NOTE the above example creates an UNENCRYPTED bucket by default,
* If you are required to use an Encrypted bucket you can supply a preconfigured bucket
* via TrailProps
*
*/
export class Trail extends Resource {

Expand All @@ -122,33 +132,36 @@ export class Trail extends Resource {
*/
public readonly trailSnsTopicArn: string;

private s3bucket: s3.IBucket;
private eventSelectors: EventSelector[] = [];

constructor(scope: Construct, id: string, props: TrailProps = {}) {
super(scope, id, {
physicalName: props.trailName,
});

const s3bucket = new s3.Bucket(this, 'S3', {encryption: s3.BucketEncryption.UNENCRYPTED});
const cloudTrailPrincipal = new iam.ServicePrincipal("cloudtrail.amazonaws.com");

s3bucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [s3bucket.bucketArn],
actions: ['s3:GetBucketAcl'],
principals: [cloudTrailPrincipal],
}));

s3bucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [s3bucket.arnForObjects(`AWSLogs/${Stack.of(this).account}/*`)],
actions: ["s3:PutObject"],
principals: [cloudTrailPrincipal],
conditions: {
StringEquals: {'s3:x-amz-acl': "bucket-owner-full-control"}
}
}));
this.s3bucket = props.bucket || new s3.Bucket(this, 'S3', {encryption: s3.BucketEncryption.UNENCRYPTED});

this.s3bucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [this.s3bucket.bucketArn],
actions: ['s3:GetBucketAcl'],
principals: [cloudTrailPrincipal],
}));

this.s3bucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [this.s3bucket.arnForObjects(`AWSLogs/${Stack.of(this).account}/*`)],
actions: ["s3:PutObject"],
principals: [cloudTrailPrincipal],
conditions: {
StringEquals: {'s3:x-amz-acl': "bucket-owner-full-control"}
}
}));

let logGroup: logs.CfnLogGroup | undefined;
let logsRole: iam.IRole | undefined;

if (props.sendToCloudWatchLogs) {
logGroup = new logs.CfnLogGroup(this, "LogGroup", {
retentionInDays: props.cloudWatchLogsRetention || logs.RetentionDays.ONE_YEAR
Expand All @@ -161,6 +174,7 @@ export class Trail extends Resource {
resources: [logGroup.attrArn],
}));
}

if (props.managementEvents) {
const managementEvent = {
includeManagementEvents: true,
Expand All @@ -177,7 +191,7 @@ export class Trail extends Resource {
includeGlobalServiceEvents: props.includeGlobalServiceEvents == null ? true : props.includeGlobalServiceEvents,
trailName: this.physicalName,
kmsKeyId: props.kmsKey && props.kmsKey.keyArn,
s3BucketName: s3bucket.bucketName,
s3BucketName: this.s3bucket.bucketName,
s3KeyPrefix: props.s3KeyPrefix,
cloudWatchLogsLogGroupArn: logGroup && logGroup.attrArn,
cloudWatchLogsRoleArn: logsRole && logsRole.roleArn,
Expand All @@ -192,7 +206,7 @@ export class Trail extends Resource {
});
this.trailSnsTopicArn = trail.attrSnsTopicArn;

const s3BucketPolicy = s3bucket.node.findChild("Policy").node.findChild("Resource") as s3.CfnBucketPolicy;
const s3BucketPolicy = this.s3bucket.node.findChild("Policy").node.findChild("Resource") as s3.CfnBucketPolicy;
trail.node.addDependency(s3BucketPolicy);

// If props.sendToCloudWatchLogs is set to true then the trail needs to depend on the created logsRole
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
{
"Resources": {
"Bucket83908E77": {
"Type": "AWS::S3::Bucket",
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"S3486F821D": {
"Type": "AWS::S3::Bucket",
"UpdateReplacePolicy": "Retain",
"DeletionPolicy": "Retain"
},
"S3Policy2E4AA1D6": {
"Type": "AWS::S3::BucketPolicy",
"Properties": {
"Bucket": {
"Ref": "S3486F821D"
},
"PolicyDocument": {
"Statement": [
{
"Action": "s3:GetBucketAcl",
"Effect": "Allow",
"Principal": {
"Service": "cloudtrail.amazonaws.com"
},
"Resource": {
"Fn::GetAtt": [
"S3486F821D",
"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": [
"S3486F821D",
"Arn"
]
},
"/AWSLogs/",
{
"Ref": "AWS::AccountId"
},
"/*"
]
]
}
}
],
"Version": "2012-10-17"
}
}
},
"Trail022F0CF2": {
"Type": "AWS::CloudTrail::Trail",
"Properties": {
"IsLogging": true,
"S3BucketName": {
"Ref": "S3486F821D"
},
"EnableLogFileValidation": true,
"EventSelectors": [
{
"DataResources": [
{
"Type": "AWS::S3::Object",
"Values": [
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"Bucket83908E77",
"Arn"
]
},
"/"
]
]
}
]
}
]
}
],
"IncludeGlobalServiceEvents": true,
"IsMultiRegionTrail": true
},
"DependsOn": [
"S3Policy2E4AA1D6"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import iam = require('@aws-cdk/aws-iam');
import s3 = require('@aws-cdk/aws-s3');
import cdk = require('@aws-cdk/core');
import { Stack } from '@aws-cdk/core';

import cloudtrail = require('../lib');

const app = new cdk.App();
const stack = new cdk.Stack(app, 'integ-cloudtrail');

const bucket = new s3.Bucket(stack, 'Bucket', { removalPolicy: cdk.RemovalPolicy.DESTROY });

// using exctecy the same code as inside the cloudtrail class to produce the supplied bucket and policy
const cloudTrailPrincipal = new iam.ServicePrincipal("cloudtrail.amazonaws.com");

const Trailbucket = new s3.Bucket(stack, 'S3', {encryption: s3.BucketEncryption.UNENCRYPTED});

Trailbucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [Trailbucket.bucketArn],
actions: ['s3:GetBucketAcl'],
principals: [cloudTrailPrincipal],
}));

Trailbucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [Trailbucket.arnForObjects(`AWSLogs/${Stack.of(stack).account}/*`)],
actions: ["s3:PutObject"],
principals: [cloudTrailPrincipal],
conditions: {
StringEquals: {'s3:x-amz-acl': "bucket-owner-full-control"}
}
}));

const trail = new cloudtrail.Trail(stack, 'Trail', {bucket: Trailbucket});

trail.addS3EventSelector([bucket.arnForObjects('')]);

app.synth();
29 changes: 29 additions & 0 deletions packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { expect, haveResource, not, SynthUtils } from '@aws-cdk/assert';
import iam = require('@aws-cdk/aws-iam');
import { RetentionDays } from '@aws-cdk/aws-logs';
import s3 = require('@aws-cdk/aws-s3');
import { Stack } from '@aws-cdk/core';
import { Test } from 'nodeunit';
import { ReadWriteType, Trail } from '../lib';
Expand Down Expand Up @@ -67,6 +69,33 @@ export = {
test.deepEqual(trail.DependsOn, ['MyAmazingCloudTrailS3Policy39C120B0']);
test.done();
},
'with s3bucket'(test: Test) {
const stack = getTestStack();
const Trailbucket = new s3.Bucket(stack, 'S3');
const cloudTrailPrincipal = new iam.ServicePrincipal("cloudtrail.amazonaws.com");
Trailbucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [Trailbucket.bucketArn],
actions: ['s3:GetBucketAcl'],
principals: [cloudTrailPrincipal],
}));

Trailbucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [Trailbucket.arnForObjects(`AWSLogs/${Stack.of(stack).account}/*`)],
actions: ["s3:PutObject"],
principals: [cloudTrailPrincipal],
conditions: {
StringEquals: {'s3:x-amz-acl': "bucket-owner-full-control"}
}
}));

new Trail(stack, 'Trail', {bucket: Trailbucket});

expect(stack).to(haveResource("AWS::CloudTrail::Trail"));
expect(stack).to(haveResource("AWS::S3::Bucket"));
expect(stack).to(haveResource("AWS::S3::BucketPolicy"));
expect(stack).to(not(haveResource("AWS::Logs::LogGroup")));
test.done();
},
'with cloud watch logs': {
'enabled'(test: Test) {
const stack = getTestStack();
Expand Down