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

r/aws_s3_bucket: support default server side encryption configuration #2472

Closed
wants to merge 11 commits into from
148 changes: 148 additions & 0 deletions aws/resource_aws_s3_bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,42 @@ func resourceAwsS3Bucket() *schema.Resource {
},
},

"server_side_encryption_configuration": {
Type: schema.TypeList,
MaxItems: 1,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"rule": {
Type: schema.TypeList,
MaxItems: 1,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"apply_server_side_encryption_by_default": {
Type: schema.TypeList,
MaxItems: 1,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"kms_master_key_id": {
Type: schema.TypeString,
Optional: true,
},
"sse_algorithm": {
Type: schema.TypeString,
Required: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should consider adding a validation function here - the valid values are aws:kms or AES256. It's conceivable that this list will expand in the future, but additional values can easily be added. This will move a potential error from apply time to plan time, which is desirable.

},
},
},
},
},
},
},
},
},
},

"tags": tagsSchema(),
},
}
Expand Down Expand Up @@ -531,6 +567,12 @@ func resourceAwsS3BucketUpdate(d *schema.ResourceData, meta interface{}) error {
}
}

if d.HasChange("server_side_encryption_configuration") {
if err := resourceAwsS3BucketServerSideEncryptionConfigurationUpdate(s3conn, d); err != nil {
return err
}
}

return resourceAwsS3BucketRead(d, meta)
}

Expand Down Expand Up @@ -941,6 +983,31 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error {
}
}

// Read the bucket server side encryption configuration

encryptionResponse, err := retryOnAwsCode("NoSuchBucket", func() (interface{}, error) {
return s3conn.GetBucketEncryption(&s3.GetBucketEncryptionInput{
Bucket: aws.String(d.Id()),
})
})
if err != nil {
if isAWSErr(err, "ServerSideEncryptionConfigurationNotFoundError", "encryption configuration was not found") {
log.Printf("[DEBUG] Default encryption is not enabled for %s", d.Id())
d.Set("server_side_encryption_configuration", []map[string]interface{}{})
} else {
return err
}
} else {
encryption := encryptionResponse.(*s3.GetBucketEncryptionOutput)
log.Printf("[DEBUG] S3 Bucket: %s, read encryption configuration: %v", d.Id(), encryption)
if c := encryption.ServerSideEncryptionConfiguration; c != nil {
if err := d.Set("server_side_encryption_configuration", flatternAwsS3ServerSideEncryptionConfiguration(c)); err != nil {
log.Printf("[DEBUG] Error setting server side encryption configuration: %s", err)
return err
}
}
}

// Add the region as an attribute

locationResponse, err := retryOnAwsCode("NoSuchBucket", func() (interface{}, error) {
Expand Down Expand Up @@ -1493,6 +1560,68 @@ func resourceAwsS3BucketRequestPayerUpdate(s3conn *s3.S3, d *schema.ResourceData
return nil
}

func resourceAwsS3BucketServerSideEncryptionConfigurationUpdate(s3conn *s3.S3, d *schema.ResourceData) error {
bucket := d.Get("bucket").(string)
serverSideEncryptionConfiguration := d.Get("server_side_encryption_configuration").([]interface{})
if len(serverSideEncryptionConfiguration) == 0 {
log.Printf("[DEBUG] Delete server side encryption configuration: %#v", serverSideEncryptionConfiguration)
i := &s3.DeleteBucketEncryptionInput{
Bucket: aws.String(bucket),
}

err := resource.Retry(1*time.Minute, func() *resource.RetryError {
if _, err := s3conn.DeleteBucketEncryption(i); err != nil {
return resource.NonRetryableError(err)
}
return nil
})
if err != nil {
return fmt.Errorf("error removing S3 bucket server side encryption: %s", err)
}
return nil
}

c := serverSideEncryptionConfiguration[0].(map[string]interface{})

rc := &s3.ServerSideEncryptionConfiguration{}

rcRules := c["rule"].([]interface{})
var rules []*s3.ServerSideEncryptionRule
for _, v := range rcRules {
rr := v.(map[string]interface{})
rrDefault := rr["apply_server_side_encryption_by_default"].([]interface{})
sseAlgorithm := rrDefault[0].(map[string]interface{})["sse_algorithm"].(string)
kmsMasterKeyId := rrDefault[0].(map[string]interface{})["kms_master_key_id"].(string)
rcDefaultRule := &s3.ServerSideEncryptionByDefault{
SSEAlgorithm: aws.String(sseAlgorithm),
}
if kmsMasterKeyId != "" {
rcDefaultRule.KMSMasterKeyID = aws.String(kmsMasterKeyId)
}
rcRule := &s3.ServerSideEncryptionRule{
ApplyServerSideEncryptionByDefault: rcDefaultRule,
}

rules = append(rules, rcRule)
}

rc.Rules = rules
i := &s3.PutBucketEncryptionInput{
Bucket: aws.String(bucket),
ServerSideEncryptionConfiguration: rc,
}
log.Printf("[DEBUG] S3 put bucket replication configuration: %#v", i)

_, err := retryOnAwsCode("NoSuchBucket", func() (interface{}, error) {
return s3conn.PutBucketEncryption(i)
})
if err != nil {
return fmt.Errorf("error putting S3 server side encryption configuration: %s", err)
}

return nil
}

func resourceAwsS3BucketReplicationConfigurationUpdate(s3conn *s3.S3, d *schema.ResourceData) error {
bucket := d.Get("bucket").(string)
replicationConfiguration := d.Get("replication_configuration").([]interface{})
Expand Down Expand Up @@ -1739,6 +1868,25 @@ func resourceAwsS3BucketLifecycleUpdate(s3conn *s3.S3, d *schema.ResourceData) e
return nil
}

func flatternAwsS3ServerSideEncryptionConfiguration(c *s3.ServerSideEncryptionConfiguration) []map[string]interface{} {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: flattern -> flatten

var encryptionConfiguration []map[string]interface{}
rules := make([]interface{}, 0, len(c.Rules))
for _, v := range c.Rules {
if v.ApplyServerSideEncryptionByDefault != nil {
r := make(map[string]interface{})
d := make(map[string]interface{})
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}
rules = append(rules, r)
}
}
encryptionConfiguration = append(encryptionConfiguration, map[string]interface{}{
"rule": rules,
})
return encryptionConfiguration
}

func flattenAwsS3BucketReplicationConfiguration(r *s3.ReplicationConfiguration) []map[string]interface{} {
replication_configuration := make([]map[string]interface{}, 0, 1)
m := make(map[string]interface{})
Expand Down
127 changes: 127 additions & 0 deletions aws/resource_aws_s3_bucket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,74 @@ func TestAccAWSS3Bucket_WebsiteRoutingRules(t *testing.T) {
})
}

func TestAccAWSS3Bucket_enableDefaultEncryption_whenTypical(t *testing.T) {
rInt := acctest.RandInt()
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSS3BucketDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSS3BucketEnableDefaultEncryption(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSS3BucketExists("aws_s3_bucket.arbitrary"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "server_side_encryption_configuration.#", "1"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "server_side_encryption_configuration.0.rule.#", "1"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "server_side_encryption_configuration.0.rule.0.apply_server_side_encryption_by_default.#", "1"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "server_side_encryption_configuration.0.rule.0.apply_server_side_encryption_by_default.0.sse_algorithm", "aws:kms"),
resource.TestMatchResourceAttr("aws_s3_bucket.arbitrary", "server_side_encryption_configuration.0.rule.0.apply_server_side_encryption_by_default.0.kms_master_key_id", regexp.MustCompile("^arn")),
),
},
},
})
}

func TestAccAWSS3Bucket_enableDefaultEncryption_whenAES256IsUsed(t *testing.T) {
rInt := acctest.RandInt()
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSS3BucketDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSS3BucketEnableDefaultEncryptionWithAES256(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSS3BucketExists("aws_s3_bucket.arbitrary"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "server_side_encryption_configuration.#", "1"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "server_side_encryption_configuration.0.rule.#", "1"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "server_side_encryption_configuration.0.rule.0.apply_server_side_encryption_by_default.#", "1"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "server_side_encryption_configuration.0.rule.0.apply_server_side_encryption_by_default.0.sse_algorithm", "AES256"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "server_side_encryption_configuration.0.rule.0.apply_server_side_encryption_by_default.0.kms_master_key_id", ""),
),
},
},
})
}

func TestAccAWSS3Bucket_disableDefaultEncryption_whenDefaultEncryptionIsEnabled(t *testing.T) {
rInt := acctest.RandInt()
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSS3BucketDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSS3BucketEnableDefaultEncryptionWithDefaultKey(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSS3BucketExists("aws_s3_bucket.arbitrary"),
),
},
{
Config: testAccAWSS3BucketDisableDefaultEncryption(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSS3BucketExists("aws_s3_bucket.arbitrary"),
resource.TestCheckResourceAttr("aws_s3_bucket.arbitrary", "server_side_encryption_configuration.#", "0"),
),
},
},
})
}

// 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
Expand Down Expand Up @@ -1423,6 +1491,65 @@ resource "aws_s3_bucket" "bucket" {
`, randInt)
}

func testAccAWSS3BucketEnableDefaultEncryption(randInt int) string {
return fmt.Sprintf(`
resource "aws_kms_key" "arbitrary" {
description = "KMS Key for Bucket Testing %d"
deletion_window_in_days = 10
}

resource "aws_s3_bucket" "arbitrary" {
bucket = "tf-test-bucket-%d"
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = "${aws_kms_key.arbitrary.arn}"
sse_algorithm = "aws:kms"
}
}
}
}
`, randInt, randInt)
}

func testAccAWSS3BucketEnableDefaultEncryptionWithAES256(randInt int) string {
return fmt.Sprintf(`
resource "aws_s3_bucket" "arbitrary" {
bucket = "tf-test-bucket-%d"
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
}
`, randInt)
}

func testAccAWSS3BucketEnableDefaultEncryptionWithDefaultKey(randInt int) string {
return fmt.Sprintf(`
resource "aws_s3_bucket" "arbitrary" {
bucket = "tf-test-bucket-%d"
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
}
`, randInt)
}

func testAccAWSS3BucketDisableDefaultEncryption(randInt int) string {
return fmt.Sprintf(`
resource "aws_s3_bucket" "arbitrary" {
bucket = "tf-test-bucket-%d"
}
`, randInt)
}

func testAccAWSS3BucketConfigWithEmptyPolicy(randInt int) string {
return fmt.Sprintf(`
resource "aws_s3_bucket" "bucket" {
Expand Down
35 changes: 35 additions & 0 deletions website/docs/r/s3_bucket.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,27 @@ resource "aws_s3_bucket" "bucket" {
}
```

### Enable Default Server Side Encryption

```hcl
resource "aws_kms_key" "mykey" {
description = "This key is used to encrypt bucket objects"
deletion_window_in_days = 10
}

resource "aws_s3_bucket" "mybucket" {
bucket = "mybucket"
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = "${aws_kms_key.mykey.arn}"
sse_algorithm = "aws:kms"
}
}
}
}
```

## Argument Reference

The following arguments are supported:
Expand All @@ -310,6 +331,7 @@ Can be either `BucketOwner` or `Requester`. By default, the owner of the S3 buck
the costs of any data transfer. See [Requester Pays Buckets](http://docs.aws.amazon.com/AmazonS3/latest/dev/RequesterPaysBuckets.html)
developer guide for more information.
* `replication_configuration` - (Optional) A configuration of [replication configuration](http://docs.aws.amazon.com/AmazonS3/latest/dev/crr.html) (documented below).
* `server_side_encryption_configuration` - (Optional) A confguration of [server-side encryption configuration](http://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-encryption.html) (documented blow)

~> **NOTE:** You cannot use `acceleration_status` in `cn-north-1` or `us-gov-west-1`

Expand Down Expand Up @@ -391,6 +413,19 @@ The `destination` object supports the following:
* `bucket` - (Required) The ARN of the S3 bucket where you want Amazon S3 to store replicas of the object identified by the rule.
* `storage_class` - (Optional) The class of storage used to store the object.

The `server_side_encryption_configuration` object supports the following:

* `rule` - (required) A single object for server-side encryption by default configuration. (documented below)

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)

The `apply_server_side_encryption_by_default` object supports the following:

* `sse_algorithm` - (required) The server-side encryption algorithm to use. Valid values are `AES256` and `aws:kms`
* `kms_master_key_id` - (optional) The AWS KMS master key ID used for the SSE-KMS encryption. This can only be used when you set the value of `sse_algorithm` as `aws:kms`. The default `aws/s3` AWS KMS master key is used if this element is absent while the `sse_algorithm` is `aws:kms`.

## Attributes Reference

The following attributes are exported:
Expand Down