diff --git a/.changelog/16581.txt b/.changelog/16581.txt new file mode 100644 index 000000000000..e4e79453e2f9 --- /dev/null +++ b/.changelog/16581.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +data-source/aws_s3_bucket_object: Add `bucket_key_enabled` attribute (Support S3 Bucket Keys) +``` + +```release-note:enhancement +resource/aws_s3_bucket: Add `bucket_key_enabled` argument to `server_side_encryption_configuration` `rule` configuration block (Support S3 Bucket Keys) +``` + +```release-note:enhancement +resource/aws_s3_bucket_object: Add `bucket_key_enabled` attribute (Support S3 Bucket Keys) +``` diff --git a/aws/data_source_aws_s3_bucket_object.go b/aws/data_source_aws_s3_bucket_object.go index 6bab45bade98..3c48e5ac54f3 100644 --- a/aws/data_source_aws_s3_bucket_object.go +++ b/aws/data_source_aws_s3_bucket_object.go @@ -27,6 +27,10 @@ func dataSourceAwsS3BucketObject() *schema.Resource { Type: schema.TypeString, Required: true, }, + "bucket_key_enabled": { + Type: schema.TypeBool, + Computed: true, + }, "cache_control": { Type: schema.TypeString, Computed: true, @@ -157,6 +161,7 @@ func dataSourceAwsS3BucketObjectRead(d *schema.ResourceData, meta interface{}) e d.SetId(uniqueId) + d.Set("bucket_key_enabled", out.BucketKeyEnabled) d.Set("cache_control", out.CacheControl) d.Set("content_disposition", out.ContentDisposition) d.Set("content_encoding", out.ContentEncoding) diff --git a/aws/data_source_aws_s3_bucket_object_test.go b/aws/data_source_aws_s3_bucket_object_test.go index 9c67dd70a519..db66fe256af9 100644 --- a/aws/data_source_aws_s3_bucket_object_test.go +++ b/aws/data_source_aws_s3_bucket_object_test.go @@ -146,6 +146,43 @@ func TestAccDataSourceAWSS3BucketObject_kmsEncrypted(t *testing.T) { }) } +func TestAccDataSourceAWSS3BucketObject_bucketKeyEnabled(t *testing.T) { + rInt := acctest.RandInt() + + var rObj s3.GetObjectOutput + var dsObj s3.GetObjectOutput + + resourceName := "aws_s3_bucket_object.object" + dataSourceName := "data.aws_s3_bucket_object.obj" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), + Providers: testAccProviders, + PreventPostDestroyRefresh: true, + Steps: []resource.TestStep{ + { + Config: testAccAWSDataSourceS3ObjectConfig_bucketKeyEnabled(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketObjectExists(resourceName, &rObj), + testAccCheckAwsS3ObjectDataSourceExists(dataSourceName, &dsObj), + resource.TestCheckResourceAttr(dataSourceName, "content_length", "22"), + resource.TestCheckResourceAttrPair(dataSourceName, "content_type", resourceName, "content_type"), + resource.TestCheckResourceAttrPair(dataSourceName, "etag", resourceName, "etag"), + resource.TestCheckResourceAttrPair(dataSourceName, "server_side_encryption", resourceName, "server_side_encryption"), + resource.TestCheckResourceAttrPair(dataSourceName, "sse_kms_key_id", resourceName, "kms_key_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "bucket_key_enabled", resourceName, "bucket_key_enabled"), + resource.TestMatchResourceAttr(dataSourceName, "last_modified", regexp.MustCompile(rfc1123RegexPattern)), + resource.TestCheckResourceAttrPair(dataSourceName, "object_lock_legal_hold_status", resourceName, "object_lock_legal_hold_status"), + resource.TestCheckResourceAttrPair(dataSourceName, "object_lock_mode", resourceName, "object_lock_mode"), + resource.TestCheckResourceAttrPair(dataSourceName, "object_lock_retain_until_date", resourceName, "object_lock_retain_until_date"), + resource.TestCheckResourceAttr(dataSourceName, "body", "Keep Calm and Carry On"), + ), + }, + }, + }) +} + func TestAccDataSourceAWSS3BucketObject_allParams(t *testing.T) { rInt := acctest.RandInt() @@ -172,6 +209,7 @@ func TestAccDataSourceAWSS3BucketObject_allParams(t *testing.T) { resource.TestMatchResourceAttr(dataSourceName, "last_modified", regexp.MustCompile(rfc1123RegexPattern)), resource.TestCheckResourceAttrPair(dataSourceName, "version_id", resourceName, "version_id"), resource.TestCheckNoResourceAttr(dataSourceName, "body"), + resource.TestCheckResourceAttrPair(dataSourceName, "bucket_key_enabled", resourceName, "bucket_key_enabled"), resource.TestCheckResourceAttrPair(dataSourceName, "cache_control", resourceName, "cache_control"), resource.TestCheckResourceAttrPair(dataSourceName, "content_disposition", resourceName, "content_disposition"), resource.TestCheckResourceAttrPair(dataSourceName, "content_encoding", resourceName, "content_encoding"), @@ -506,6 +544,33 @@ data "aws_s3_bucket_object" "obj" { `, randInt) } +func testAccAWSDataSourceS3ObjectConfig_bucketKeyEnabled(randInt int) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "object_bucket" { + bucket = "tf-object-test-bucket-%[1]d" +} + +resource "aws_kms_key" "example" { + description = "TF Acceptance Test KMS key" + deletion_window_in_days = 7 +} + +resource "aws_s3_bucket_object" "object" { + bucket = aws_s3_bucket.object_bucket.bucket + key = "tf-testing-obj-%[1]d-encrypted" + content = "Keep Calm and Carry On" + content_type = "text/plain" + kms_key_id = aws_kms_key.example.arn + bucket_key_enabled = true +} + +data "aws_s3_bucket_object" "obj" { + bucket = aws_s3_bucket.object_bucket.bucket + key = aws_s3_bucket_object.object.key +} +`, randInt) +} + func testAccAWSDataSourceS3ObjectConfig_allParams(randInt int) string { return fmt.Sprintf(` resource "aws_s3_bucket" "object_bucket" { diff --git a/aws/resource_aws_s3_bucket.go b/aws/resource_aws_s3_bucket.go index 82a05ea6a39b..a0405a655224 100644 --- a/aws/resource_aws_s3_bucket.go +++ b/aws/resource_aws_s3_bucket.go @@ -561,6 +561,10 @@ func resourceAwsS3Bucket() *schema.Resource { }, }, }, + "bucket_key_enabled": { + Type: schema.TypeBool, + Optional: true, + }, }, }, }, @@ -1931,6 +1935,10 @@ func resourceAwsS3BucketServerSideEncryptionConfigurationUpdate(s3conn *s3.S3, d ApplyServerSideEncryptionByDefault: rcDefaultRule, } + if val, ok := rr["bucket_key_enabled"].(bool); ok { + rcRule.BucketKeyEnabled = aws.Bool(val) + } + rules = append(rules, rcRule) } @@ -2291,6 +2299,7 @@ func flattenAwsS3ServerSideEncryptionConfiguration(c *s3.ServerSideEncryptionCon d["kms_master_key_id"] = aws.StringValue(v.ApplyServerSideEncryptionByDefault.KMSMasterKeyID) d["sse_algorithm"] = aws.StringValue(v.ApplyServerSideEncryptionByDefault.SSEAlgorithm) r["apply_server_side_encryption_by_default"] = []map[string]interface{}{d} + r["bucket_key_enabled"] = aws.BoolValue(v.BucketKeyEnabled) rules = append(rules, r) } } diff --git a/aws/resource_aws_s3_bucket_object.go b/aws/resource_aws_s3_bucket_object.go index f8334e067fb9..23c58f04d937 100644 --- a/aws/resource_aws_s3_bucket_object.go +++ b/aws/resource_aws_s3_bucket_object.go @@ -56,6 +56,12 @@ func resourceAwsS3BucketObject() *schema.Resource { ValidateFunc: validation.StringInSlice(s3.ObjectCannedACL_Values(), false), }, + "bucket_key_enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "cache_control": { Type: schema.TypeString, Optional: true, @@ -259,6 +265,10 @@ func resourceAwsS3BucketObjectPut(d *schema.ResourceData, meta interface{}) erro putInput.ContentDisposition = aws.String(v.(string)) } + if v, ok := d.GetOk("bucket_key_enabled"); ok { + putInput.BucketKeyEnabled = aws.Bool(v.(bool)) + } + if v, ok := d.GetOk("server_side_encryption"); ok { putInput.ServerSideEncryption = aws.String(v.(string)) } @@ -347,6 +357,7 @@ func resourceAwsS3BucketObjectRead(d *schema.ResourceData, meta interface{}) err log.Printf("[DEBUG] Reading S3 Bucket Object meta: %s", resp) + d.Set("bucket_key_enabled", resp.BucketKeyEnabled) d.Set("cache_control", resp.CacheControl) d.Set("content_disposition", resp.ContentDisposition) d.Set("content_encoding", resp.ContentEncoding) @@ -537,6 +548,7 @@ func resourceAwsS3BucketObjectCustomizeDiff(_ context.Context, d *schema.Resourc func hasS3BucketObjectContentChanges(d resourceDiffer) bool { for _, key := range []string{ + "bucket_key_enabled", "cache_control", "content_base64", "content_disposition", diff --git a/aws/resource_aws_s3_bucket_object_test.go b/aws/resource_aws_s3_bucket_object_test.go index ed3a8238ed11..2421eb82ab60 100644 --- a/aws/resource_aws_s3_bucket_object_test.go +++ b/aws/resource_aws_s3_bucket_object_test.go @@ -1112,6 +1112,52 @@ func TestAccAWSS3BucketObject_ObjectLockRetentionStartWithSet(t *testing.T) { }) } +func TestAccAWSS3BucketObject_objectBucketKeyEnabled(t *testing.T) { + var obj s3.GetObjectOutput + resourceName := "aws_s3_bucket_object.object" + rInt := acctest.RandInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3BucketObjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3BucketObjectConfig_objectBucketKeyEnabled(rInt, "stuff"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketObjectExists(resourceName, &obj), + testAccCheckAWSS3BucketObjectBody(&obj, "stuff"), + resource.TestCheckResourceAttr(resourceName, "bucket_key_enabled", "true"), + ), + }, + }, + }) +} + +func TestAccAWSS3BucketObject_bucketBucketKeyEnabled(t *testing.T) { + var obj s3.GetObjectOutput + resourceName := "aws_s3_bucket_object.object" + rInt := acctest.RandInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3BucketObjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3BucketObjectConfig_bucketBucketKeyEnabled(rInt, "stuff"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketObjectExists(resourceName, &obj), + testAccCheckAWSS3BucketObjectBody(&obj, "stuff"), + resource.TestCheckResourceAttr(resourceName, "bucket_key_enabled", "true"), + ), + }, + }, + }) +} + func TestAccAWSS3BucketObject_defaultBucketSSE(t *testing.T) { var obj1 s3.GetObjectOutput resourceName := "aws_s3_bucket_object.object" @@ -1861,6 +1907,56 @@ resource "aws_s3_bucket_object" "object" { `, randInt, source) } +func testAccAWSS3BucketObjectConfig_objectBucketKeyEnabled(randInt int, content string) string { + return fmt.Sprintf(` +resource "aws_kms_key" "test" { + description = "Encrypts test bucket objects" + deletion_window_in_days = 7 +} + +resource "aws_s3_bucket" "object_bucket" { + bucket = "tf-object-test-bucket-%[1]d" +} + +resource "aws_s3_bucket_object" "object" { + bucket = aws_s3_bucket.object_bucket.bucket + key = "test-key" + content = %q + kms_key_id = aws_kms_key.test.arn + bucket_key_enabled = true +} +`, randInt, content) +} + +func testAccAWSS3BucketObjectConfig_bucketBucketKeyEnabled(randInt int, content string) string { + return fmt.Sprintf(` +resource "aws_kms_key" "test" { + description = "Encrypts test bucket objects" + deletion_window_in_days = 7 +} + +resource "aws_s3_bucket" "object_bucket" { + bucket = "tf-object-test-bucket-%[1]d" + + server_side_encryption_configuration { + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = aws_kms_key.test.arn + sse_algorithm = "aws:kms" + } + bucket_key_enabled = true + } + } +} + +resource "aws_s3_bucket_object" "object" { + bucket = aws_s3_bucket.object_bucket.bucket + key = "test-key" + content = %q +} +`, randInt, content) +} + func testAccAWSS3BucketObjectConfig_defaultBucketSSE(randInt int, content string) string { return fmt.Sprintf(` resource "aws_kms_key" "test" { diff --git a/aws/resource_aws_s3_bucket_test.go b/aws/resource_aws_s3_bucket_test.go index f4c9a1a0af31..a3ab7e44f0ec 100644 --- a/aws/resource_aws_s3_bucket_test.go +++ b/aws/resource_aws_s3_bucket_test.go @@ -1004,6 +1004,38 @@ func TestAccAWSS3Bucket_disableDefaultEncryption_whenDefaultEncryptionIsEnabled( }) } +func TestAccAWSS3Bucket_bucketKeyEnabled(t *testing.T) { + bucketName := acctest.RandomWithPrefix("tf-test-bucket") + resourceName := "aws_s3_bucket.arbitrary" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, s3.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3BucketDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3BucketKeyEnabled(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "server_side_encryption_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "server_side_encryption_configuration.0.rule.#", "1"), + resource.TestCheckResourceAttr(resourceName, "server_side_encryption_configuration.0.rule.0.apply_server_side_encryption_by_default.#", "1"), + resource.TestCheckResourceAttr(resourceName, "server_side_encryption_configuration.0.rule.0.apply_server_side_encryption_by_default.0.sse_algorithm", "aws:kms"), + resource.TestMatchResourceAttr(resourceName, "server_side_encryption_configuration.0.rule.0.apply_server_side_encryption_by_default.0.kms_master_key_id", regexp.MustCompile("^arn")), + resource.TestCheckResourceAttr(resourceName, "server_side_encryption_configuration.0.rule.0.bucket_key_enabled", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"force_destroy", "acl"}, + }, + }, + }) +} + // Test TestAccAWSS3Bucket_shouldFailNotFound is designed to fail with a "plan // not empty" error in Terraform, to check against regresssions. // See https://github.com/hashicorp/terraform/pull/2925 @@ -3543,6 +3575,29 @@ resource "aws_s3_bucket" "arbitrary" { `, bucketName) } +func testAccAWSS3BucketKeyEnabled(bucketName string) string { + return fmt.Sprintf(` +resource "aws_kms_key" "arbitrary" { + description = "KMS Key for Bucket %[1]s" + deletion_window_in_days = 7 +} + +resource "aws_s3_bucket" "arbitrary" { + bucket = %[1]q + + server_side_encryption_configuration { + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = aws_kms_key.arbitrary.arn + sse_algorithm = "aws:kms" + } + bucket_key_enabled = true + } + } +} +`, bucketName) +} + func testAccAWSS3BucketEnableDefaultEncryptionWithAES256(bucketName string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "arbitrary" { diff --git a/website/docs/d/s3_bucket_object.html.markdown b/website/docs/d/s3_bucket_object.html.markdown index 494f0217ca24..c09e01276108 100644 --- a/website/docs/d/s3_bucket_object.html.markdown +++ b/website/docs/d/s3_bucket_object.html.markdown @@ -66,6 +66,7 @@ The following arguments are supported: In addition to all arguments above, the following attributes are exported: * `body` - Object data (see **limitations above** to understand cases in which this field is actually available) +* `bucket_key_enabled` - (Optional) Whether or not to use [Amazon S3 Bucket Keys](https://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-key.html) for SSE-KMS. * `cache_control` - Specifies caching behavior along the request/reply chain. * `content_disposition` - Specifies presentational information for the object. * `content_encoding` - Specifies what content encodings have been applied to the object and thus what decoding mechanisms must be applied to obtain the media-type referenced by the Content-Type header field. diff --git a/website/docs/r/s3_bucket.html.markdown b/website/docs/r/s3_bucket.html.markdown index b9e5a7dab681..6ef81bf958e5 100644 --- a/website/docs/r/s3_bucket.html.markdown +++ b/website/docs/r/s3_bucket.html.markdown @@ -476,6 +476,7 @@ The `server_side_encryption_configuration` object supports the following: The `rule` object supports the following: * `apply_server_side_encryption_by_default` - (required) A single object for setting server-side encryption by default. (documented below) +* `bucket_key_enabled` - (Optional) Whether or not to use [Amazon S3 Bucket Keys](https://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-key.html) for SSE-KMS. The `apply_server_side_encryption_by_default` object supports the following: diff --git a/website/docs/r/s3_bucket_object.html.markdown b/website/docs/r/s3_bucket_object.html.markdown index 8d0698a43a11..968a1f9a4d17 100644 --- a/website/docs/r/s3_bucket_object.html.markdown +++ b/website/docs/r/s3_bucket_object.html.markdown @@ -135,6 +135,7 @@ This attribute is not compatible with KMS encryption, `kms_key_id` or `server_si * `kms_key_id` - (Optional) Amazon Resource Name (ARN) of the KMS Key to use for object encryption. If the S3 Bucket has server-side encryption enabled, that value will automatically be used. If referencing the `aws_kms_key` resource, use the `arn` attribute. If referencing the `aws_kms_alias` data source or resource, use the `target_key_arn` attribute. Terraform will only perform drift detection if a configuration value is provided. +* `bucket_key_enabled` - (Optional) Whether or not to use [Amazon S3 Bucket Keys](https://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-key.html) for SSE-KMS. * `metadata` - (Optional) A map of keys/values to provision metadata (will be automatically prefixed by `x-amz-meta-`, note that only lowercase label are currently supported by the AWS Go API). * `tags` - (Optional) A map of tags to assign to the object. * `force_destroy` - (Optional) Allow the object to be deleted by removing any legal hold on any object version.