Skip to content

Commit

Permalink
feat(s3-deployment): implement new signContent option
Browse files Browse the repository at this point in the history
  • Loading branch information
AMZN-hgoffin committed Mar 21, 2023
1 parent 3b7431b commit cafb3bf
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 3 deletions.
15 changes: 15 additions & 0 deletions packages/@aws-cdk/aws-s3-deployment/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,21 @@ new s3deploy.BucketDeployment(this, 'DeployWithInvalidation', {
});
```

## Signed Content Payloads

By default, deployment uses streaming uploads which set the `x-amz-content-sha256`
request header to `UNSIGNED-PAYLOAD` (matching the behavior of the AWS CLI tool).
In cases where IAM resource policy requires signed content payloads, you can enable
generation of a signed `x-amz-content-sha256` request header with `signContent: true`.

```ts
new s3deploy.BucketDeployment(this, 'DeployWithSignedPayloads', {
sources: [s3deploy.Source.asset('./website-dist')],
destinationBucket: bucket,
signContent: true,
});
```

## Size Limits

The default memory limit for the deployment resource is 128MiB. If you need to
Expand Down
20 changes: 17 additions & 3 deletions packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,14 @@ export interface BucketDeploymentProps {
* @default - the Vpc default strategy if not specified
*/
readonly vpcSubnets?: ec2.SubnetSelection;

/**
* If set to true, uploads will precompute the value of `x-amz-content-sha256`
* and include it in the signed S3 request headers.
*
* @default false - `x-amz-content-sha256` will not be computed
*/
readonly signContent?: boolean;
}

/**
Expand Down Expand Up @@ -307,14 +315,20 @@ export class BucketDeployment extends Construct {
}

const mountPath = `/mnt${accessPointPath}`;

const lambdaEnv : any = {};
if (props.useEfs) {
lambdaEnv.MOUNT_PATH = mountPath;
}
if (props.signContent) {
lambdaEnv.PAYLOAD_SIGNING_ENABLED = '1';
}
const handler = new lambda.SingletonFunction(this, 'CustomResourceHandler', {
uuid: this.renderSingletonUuid(props.memoryLimit, props.ephemeralStorageSize, props.vpc),
code: lambda.Code.fromAsset(path.join(__dirname, 'lambda')),
layers: [new AwsCliLayer(this, 'AwsCliLayer')],
runtime: lambda.Runtime.PYTHON_3_9,
environment: props.useEfs ? {
MOUNT_PATH: mountPath,
} : undefined,
environment: lambdaEnv,
handler: 'index.handler',
lambdaPurpose: 'Custom::CDKBucketDeployment',
timeout: cdk.Duration.minutes(15),
Expand Down
10 changes: 10 additions & 0 deletions packages/@aws-cdk/aws-s3-deployment/lib/lambda/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
cloudfront = boto3.client('cloudfront')
s3 = boto3.client('s3')

payload_signing_configured = False

CFN_SUCCESS = "SUCCESS"
CFN_FAILED = "FAILED"
ENV_KEY_MOUNT_PATH = "MOUNT_PATH"
ENV_KEY_SKIP_CLEANUP = "SKIP_CLEANUP"
ENV_KEY_PAYLOAD_SIGNING_ENABLED = "PAYLOAD_SIGNING_ENABLED"

CUSTOM_RESOURCE_OWNER_TAG = "aws-cdk:cr-owned"

Expand Down Expand Up @@ -133,6 +136,8 @@ def cfn_error(message=None):
#---------------------------------------------------------------------------------------------------
# populate all files from s3_source_zips to a destination bucket
def s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune, exclude, include, source_markers, extract):
global payload_signing_configured

# list lengths are equal
if len(s3_source_zips) != len(source_markers):
raise Exception("'source_markers' and 's3_source_zips' must be the same length")
Expand All @@ -150,6 +155,11 @@ def s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune, ex
contents_dir=os.path.join(workdir, 'contents')
os.mkdir(contents_dir)

# configure global payload_signing_enabled value if necessary
if not payload_signing_configured and os.getenv(ENV_KEY_PAYLOAD_SIGNING_ENABLED):
aws_command("configure", "set", "default.s3.payload_signing_enabled", "true")
payload_signing_configured = True

try:
# download the archive from the source and extract to "contents"
for i in range(len(s3_source_zips)):
Expand Down
21 changes: 21 additions & 0 deletions packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,27 @@ test('deploy without deleting missing files from destination', () => {
});
});

test('deploy with payload signing enabled', () => {
// GIVEN
const stack = new cdk.Stack();
const bucket = new s3.Bucket(stack, 'Dest');

// WHEN
new s3deploy.BucketDeployment(stack, 'Deploy', {
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
destinationBucket: bucket,
signContent: true,
});

Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
Environment: {
Variables: {
PAYLOAD_SIGNING_ENABLED: '1',
},
},
});
});

test('deploy with excluded files from destination', () => {
// GIVEN
const stack = new cdk.Stack();
Expand Down
19 changes: 19 additions & 0 deletions packages/@aws-cdk/aws-s3-deployment/test/lambda/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,25 @@ def test_create_update_with_metadata(self):
["s3", "sync", "--delete", "contents.zip", "s3://<dest-bucket-name>/<dest-key-prefix>", "--content-type", "text/html", "--content-language", "en", "--metadata", "{\"best\":\"game\"}", "--metadata-directive", "REPLACE"]
)

def test_create_update_with_payload_signing(self):
self.assertFalse(index.payload_signing_configured)
try:
os.environ["PAYLOAD_SIGNING_ENABLED"] = "1"
invoke_handler("Create", {
"SourceBucketNames": ["<source-bucket>"],
"SourceObjectKeys": ["<source-object-key>"],
"DestinationBucketName": "<dest-bucket-name>"
})
self.assertAwsCommands(
["configure", "set", "default.s3.payload_signing_enabled", "true"],
["s3", "cp", "s3://<source-bucket>/<source-object-key>", "archive.zip"],
["s3", "sync", "--delete", "contents.zip", "s3://<dest-bucket-name>/"]
)
self.assertTrue(index.payload_signing_configured)
finally:
del os.environ["PAYLOAD_SIGNING_ENABLED"]
index.payload_signing_configured = False

def test_delete_no_retain(self):
def mock_make_api_call(self, operation_name, kwarg):
if operation_name == 'GetBucketTagging':
Expand Down

0 comments on commit cafb3bf

Please sign in to comment.