From 28cee57ef5c7416ecd2e59851d981aa4b28bf39d Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 31 Jan 2017 20:20:48 +1100 Subject: [PATCH] S3 Bucket Object Sever Side Encryption (#11261) * added server_side_encryption to s3_bucket_object resource including associated acceptance test and documentation. * got acceptance tests passing. * made server_side_encryption a computed attribute and only set kms_key_id attribute if an S3 non-default master key is in use. * ensured kms api is only interrogated if required. --- .../aws/resource_aws_s3_bucket_object.go | 51 +++++++++++- .../aws/resource_aws_s3_bucket_object_test.go | 80 +++++++++++++++++++ .../aws/r/s3_bucket_object.html.markdown | 17 ++++ 3 files changed, 145 insertions(+), 3 deletions(-) diff --git a/builtin/providers/aws/resource_aws_s3_bucket_object.go b/builtin/providers/aws/resource_aws_s3_bucket_object.go index 45211f80301a..94257b45dc17 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket_object.go +++ b/builtin/providers/aws/resource_aws_s3_bucket_object.go @@ -14,6 +14,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/kms" "github.com/aws/aws-sdk-go/service/s3" ) @@ -89,6 +90,13 @@ func resourceAwsS3BucketObject() *schema.Resource { ValidateFunc: validateS3BucketObjectStorageClassType, }, + "server_side_encryption": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateS3BucketObjectServerSideEncryption, + Computed: true, + }, + "kms_key_id": { Type: schema.TypeString, Optional: true, @@ -102,7 +110,7 @@ func resourceAwsS3BucketObject() *schema.Resource { // See http://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html Optional: true, Computed: true, - ConflictsWith: []string{"kms_key_id"}, + ConflictsWith: []string{"kms_key_id", "server_side_encryption"}, }, "version_id": { @@ -171,9 +179,13 @@ func resourceAwsS3BucketObjectPut(d *schema.ResourceData, meta interface{}) erro putInput.ContentDisposition = aws.String(v.(string)) } + if v, ok := d.GetOk("server_side_encryption"); ok { + putInput.ServerSideEncryption = aws.String(v.(string)) + } + if v, ok := d.GetOk("kms_key_id"); ok { putInput.SSEKMSKeyId = aws.String(v.(string)) - putInput.ServerSideEncryption = aws.String("aws:kms") + putInput.ServerSideEncryption = aws.String(s3.ServerSideEncryptionAwsKms) } resp, err := s3conn.PutObject(putInput) @@ -218,7 +230,24 @@ func resourceAwsS3BucketObjectRead(d *schema.ResourceData, meta interface{}) err d.Set("content_language", resp.ContentLanguage) d.Set("content_type", resp.ContentType) d.Set("version_id", resp.VersionId) - d.Set("kms_key_id", resp.SSEKMSKeyId) + d.Set("server_side_encryption", resp.ServerSideEncryption) + + // Only set non-default KMS key ID (one that doesn't match default) + if resp.SSEKMSKeyId != nil { + // retrieve S3 KMS Default Master Key + kmsconn := meta.(*AWSClient).kmsconn + kmsresp, err := kmsconn.DescribeKey(&kms.DescribeKeyInput{ + KeyId: aws.String("alias/aws/s3"), + }) + if err != nil { + return fmt.Errorf("Failed to describe default S3 KMS key (alias/aws/s3): %s", err) + } + + if *resp.SSEKMSKeyId != *kmsresp.KeyMetadata.Arn { + log.Printf("[DEBUG] S3 object is encrypted using a non-default KMS Key ID: %s", *resp.SSEKMSKeyId) + d.Set("kms_key_id", resp.SSEKMSKeyId) + } + } d.Set("etag", strings.Trim(*resp.ETag, `"`)) // The "STANDARD" (which is also the default) storage @@ -328,3 +357,19 @@ func validateS3BucketObjectStorageClassType(v interface{}, k string) (ws []strin } return } + +func validateS3BucketObjectServerSideEncryption(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + serverSideEncryption := map[string]bool{ + s3.ServerSideEncryptionAes256: true, + s3.ServerSideEncryptionAwsKms: true, + } + + if _, ok := serverSideEncryption[value]; !ok { + errors = append(errors, fmt.Errorf( + "%q contains an invalid Server Side Encryption value %q. Valid values are %q and %q", + k, value, s3.ServerSideEncryptionAes256, s3.ServerSideEncryptionAwsKms)) + } + return +} diff --git a/builtin/providers/aws/resource_aws_s3_bucket_object_test.go b/builtin/providers/aws/resource_aws_s3_bucket_object_test.go index 824c5ba35936..aa6c1b3ac000 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket_object_test.go +++ b/builtin/providers/aws/resource_aws_s3_bucket_object_test.go @@ -267,6 +267,43 @@ func TestAccAWSS3BucketObject_kms(t *testing.T) { }) } +func TestAccAWSS3BucketObject_sse(t *testing.T) { + tmpFile, err := ioutil.TempFile("", "tf-acc-s3-obj-source-sse") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpFile.Name()) + + // first write some data to the tempfile just so it's not 0 bytes. + err = ioutil.WriteFile(tmpFile.Name(), []byte("{anything will do}"), 0644) + if err != nil { + t.Fatal(err) + } + + rInt := acctest.RandInt() + var obj s3.GetObjectOutput + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3BucketObjectDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + PreConfig: func() {}, + Config: testAccAWSS3BucketObjectConfig_withSSE(rInt, tmpFile.Name()), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketObjectExists( + "aws_s3_bucket_object.object", + &obj), + testAccCheckAWSS3BucketObjectSSE( + "aws_s3_bucket_object.object", + "aws:kms"), + ), + }, + }, + }) +} + func TestAccAWSS3BucketObject_acl(t *testing.T) { rInt := acctest.RandInt() var obj s3.GetObjectOutput @@ -467,6 +504,34 @@ func testAccCheckAWSS3BucketObjectStorageClass(n, expectedClass string) resource } } +func testAccCheckAWSS3BucketObjectSSE(n, expectedSSE string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, _ := s.RootModule().Resources[n] + s3conn := testAccProvider.Meta().(*AWSClient).s3conn + + out, err := s3conn.HeadObject(&s3.HeadObjectInput{ + Bucket: aws.String(rs.Primary.Attributes["bucket"]), + Key: aws.String(rs.Primary.Attributes["key"]), + }) + + if err != nil { + return fmt.Errorf("HeadObject error: %v", err) + } + + if out.ServerSideEncryption == nil { + return fmt.Errorf("Expected a non %v Server Side Encryption.", out.ServerSideEncryption) + } + + sse := *out.ServerSideEncryption + if sse != expectedSSE { + return fmt.Errorf("Expected Server Side Encryption %v, got %v.", + expectedSSE, sse) + } + + return nil + } +} + func testAccAWSS3BucketObjectConfigSource(randInt int, source string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "object_bucket" { @@ -561,6 +626,21 @@ resource "aws_s3_bucket_object" "object" { `, randInt) } +func testAccAWSS3BucketObjectConfig_withSSE(randInt int, source string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "object_bucket" { + bucket = "tf-object-test-bucket-%d" +} + +resource "aws_s3_bucket_object" "object" { + bucket = "${aws_s3_bucket.object_bucket.bucket}" + key = "test-key" + source = "%s" + server_side_encryption = "aws:kms" +} +`, randInt, source) +} + func testAccAWSS3BucketObjectConfig_acl(randInt int, acl string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "object_bucket" { diff --git a/website/source/docs/providers/aws/r/s3_bucket_object.html.markdown b/website/source/docs/providers/aws/r/s3_bucket_object.html.markdown index 52e6bdfe7361..a9b10297593f 100644 --- a/website/source/docs/providers/aws/r/s3_bucket_object.html.markdown +++ b/website/source/docs/providers/aws/r/s3_bucket_object.html.markdown @@ -44,6 +44,22 @@ resource "aws_s3_bucket_object" "examplebucket_object" { } ``` +### Server Side Encryption with S3 Default Master Key + +``` +resource "aws_s3_bucket" "examplebucket" { + bucket = "examplebuckettftest" + acl = "private" +} + +resource "aws_s3_bucket_object" "examplebucket_object" { + key = "someobject" + bucket = "${aws_s3_bucket.examplebucket.bucket}" + source = "index.html" + server_side_encryption = "aws:kms" +} +``` + ## Argument Reference The following arguments are supported: @@ -62,6 +78,7 @@ The following arguments are supported: for the object. Can be either "`STANDARD`", "`REDUCED_REDUNDANCY`", or "`STANDARD_IA`". Defaults to "`STANDARD`". * `etag` - (Optional) Used to trigger updates. The only meaningful value is `${md5(file("path/to/file"))}`. This attribute is not compatible with `kms_key_id`. +* `server_side_encryption` - (Optional) Specifies server-side encryption of the object in S3. Valid values are "`AES256`" and "`aws:kms`". * `kms_key_id` - (Optional) Specifies the AWS KMS Key ARN to use for object encryption. This value is a fully qualified **ARN** of the KMS Key. If using `aws_kms_key`, use the exported `arn` attribute: