From c34d09e17107affc8a3c7efd2a0ff210682bb093 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Fri, 29 Jun 2018 16:01:00 -0400 Subject: [PATCH 1/4] Add 'aws_macie_s3_bucket_association' resource. --- aws/config.go | 3 + aws/provider.go | 1 + ...esource_aws_macie_s3_bucket_association.go | 235 ++++++++++++++++++ ...ce_aws_macie_s3_bucket_association_test.go | 174 +++++++++++++ website/aws.erb | 11 + .../macie_s3_bucket_association.html.markdown | 43 ++++ 6 files changed, 467 insertions(+) create mode 100644 aws/resource_aws_macie_s3_bucket_association.go create mode 100644 aws/resource_aws_macie_s3_bucket_association_test.go create mode 100644 website/docs/r/macie_s3_bucket_association.html.markdown diff --git a/aws/config.go b/aws/config.go index df113d1869e..12613f4c47b 100644 --- a/aws/config.go +++ b/aws/config.go @@ -70,6 +70,7 @@ import ( "github.com/aws/aws-sdk-go/service/lambda" "github.com/aws/aws-sdk-go/service/lexmodelbuildingservice" "github.com/aws/aws-sdk-go/service/lightsail" + "github.com/aws/aws-sdk-go/service/macie" "github.com/aws/aws-sdk-go/service/mediastore" "github.com/aws/aws-sdk-go/service/mq" "github.com/aws/aws-sdk-go/service/neptune" @@ -210,6 +211,7 @@ type AWSClient struct { elastictranscoderconn *elastictranscoder.ElasticTranscoder lambdaconn *lambda.Lambda lightsailconn *lightsail.Lightsail + macieconn *macie.Macie mqconn *mq.MQ opsworksconn *opsworks.OpsWorks organizationsconn *organizations.Organizations @@ -501,6 +503,7 @@ func (c *Config) Client() (interface{}, error) { client.lambdaconn = lambda.New(awsLambdaSess) client.lexmodelconn = lexmodelbuildingservice.New(sess) client.lightsailconn = lightsail.New(sess) + client.macieconn = macie.New(sess) client.mqconn = mq.New(sess) client.neptuneconn = neptune.New(sess) client.opsworksconn = opsworks.New(sess) diff --git a/aws/provider.go b/aws/provider.go index fc8848da8f9..67b8866104f 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -491,6 +491,7 @@ func Provider() terraform.ResourceProvider { "aws_load_balancer_backend_server_policy": resourceAwsLoadBalancerBackendServerPolicies(), "aws_load_balancer_listener_policy": resourceAwsLoadBalancerListenerPolicies(), "aws_lb_ssl_negotiation_policy": resourceAwsLBSSLNegotiationPolicy(), + "aws_macie_s3_bucket_association": resourceAwsMacieS3BucketAssociation(), "aws_main_route_table_association": resourceAwsMainRouteTableAssociation(), "aws_mq_broker": resourceAwsMqBroker(), "aws_mq_configuration": resourceAwsMqConfiguration(), diff --git a/aws/resource_aws_macie_s3_bucket_association.go b/aws/resource_aws_macie_s3_bucket_association.go new file mode 100644 index 00000000000..e656e0ebfd7 --- /dev/null +++ b/aws/resource_aws_macie_s3_bucket_association.go @@ -0,0 +1,235 @@ +package aws + +import ( + "fmt" + "log" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/macie" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsMacieS3BucketAssociation() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsMacieS3BucketAssociationCreate, + Read: resourceAwsMacieS3BucketAssociationRead, + Update: resourceAwsMacieS3BucketAssociationUpdate, + Delete: resourceAwsMacieS3BucketAssociationDelete, + + Schema: map[string]*schema.Schema{ + "bucket_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "prefix": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "member_account_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateAwsAccountId, + }, + "classification_type": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "one_time": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + }, + }, + }, + }, + } +} + +func resourceAwsMacieS3BucketAssociationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).macieconn + + req := &macie.AssociateS3ResourcesInput{ + S3Resources: []*macie.S3ResourceClassification{ + { + BucketName: aws.String(d.Get("bucket_name").(string)), + }, + }, + } + if v, ok := d.GetOk("member_account_id"); ok { + req.MemberAccountId = aws.String(v.(string)) + } + if v, ok := d.GetOk("prefix"); ok { + req.S3Resources[0].Prefix = aws.String(v.(string)) + } + + ct := &macie.ClassificationType{ + Continuous: aws.String(macie.S3ContinuousClassificationTypeFull), + } + ct.OneTime = aws.String(macieS3BucketAssociationOneTimeClassification(d)) + req.S3Resources[0].ClassificationType = ct + + log.Printf("[DEBUG] Creating Macie S3 bucket association: %#v", req) + resp, err := conn.AssociateS3Resources(req) + if err != nil { + return fmt.Errorf("Error creating Macie S3 bucket association: %s", err) + } + if len(resp.FailedS3Resources) > 0 { + return fmt.Errorf("Error creating Macie S3 bucket association: %s", resp.FailedS3Resources[0]) + } + + d.SetId(macieS3BucketAssociationId(d)) + return resourceAwsMacieS3BucketAssociationRead(d, meta) +} + +func resourceAwsMacieS3BucketAssociationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).macieconn + + req := &macie.ListS3ResourcesInput{} + if v, ok := d.GetOk("member_account_id"); ok { + req.MemberAccountId = aws.String(v.(string)) + } + + bucketName := d.Get("bucket_name").(string) + prefix := d.Get("prefix") + var res *macie.S3ResourceClassification + for { + resp, err := conn.ListS3Resources(req) + if err != nil { + return fmt.Errorf("Error listing Macie S3 bucket associations: %s", err) + } + + for _, v := range resp.S3Resources { + if aws.StringValue(v.BucketName) == bucketName && aws.StringValue(v.Prefix) == prefix { + res = v + break + } + } + if res != nil { + break + } + + if resp.NextToken == nil { + break + } + req.NextToken = resp.NextToken + } + + if res == nil { + log.Printf("[WARN] Macie S3 bucket association (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + m := map[string]interface{}{} + if aws.StringValue(res.ClassificationType.OneTime) == macie.S3OneTimeClassificationTypeFull { + m["one_time"] = true + } else { + m["one_time"] = false + } + if err := d.Set("classification_type", []map[string]interface{}{m}); err != nil { + return err + } + + return nil +} + +func resourceAwsMacieS3BucketAssociationUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).macieconn + + if d.HasChange("classification_type") { + req := &macie.UpdateS3ResourcesInput{ + S3ResourcesUpdate: []*macie.S3ResourceClassificationUpdate{ + { + BucketName: aws.String(d.Get("bucket_name").(string)), + ClassificationTypeUpdate: &macie.ClassificationTypeUpdate{}, + }, + }, + } + if v, ok := d.GetOk("member_account_id"); ok { + req.MemberAccountId = aws.String(v.(string)) + } + if v, ok := d.GetOk("prefix"); ok { + req.S3ResourcesUpdate[0].Prefix = aws.String(v.(string)) + } + req.S3ResourcesUpdate[0].ClassificationTypeUpdate.OneTime = aws.String(macieS3BucketAssociationOneTimeClassification(d)) + + log.Printf("[DEBUG] Updating Macie S3 bucket association: %#v", req) + resp, err := conn.UpdateS3Resources(req) + if err != nil { + return fmt.Errorf("Error updating Macie S3 bucket association: %s", err) + } + if len(resp.FailedS3Resources) > 0 { + return fmt.Errorf("Error updating Macie S3 bucket association: %s", resp.FailedS3Resources[0]) + } + } + + return resourceAwsMacieS3BucketAssociationRead(d, meta) +} + +func resourceAwsMacieS3BucketAssociationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).macieconn + + log.Printf("[DEBUG] Deleting Macie S3 bucket association: %s", d.Id()) + + req := &macie.DisassociateS3ResourcesInput{ + AssociatedS3Resources: []*macie.S3Resource{ + { + BucketName: aws.String(d.Get("bucket_name").(string)), + }, + }, + } + if v, ok := d.GetOk("member_account_id"); ok { + req.MemberAccountId = aws.String(v.(string)) + } + if v, ok := d.GetOk("prefix"); ok { + req.AssociatedS3Resources[0].Prefix = aws.String(v.(string)) + } + + resp, err := conn.DisassociateS3Resources(req) + if err != nil { + return fmt.Errorf("Error deleting Macie S3 bucket association: %s", err) + } + if len(resp.FailedS3Resources) > 0 { + failed := resp.FailedS3Resources[0] + // { + // ErrorCode: "InvalidInputException", + // ErrorMessage: "The request was rejected. The specified S3 resource (bucket or prefix) is not associated with Macie.", + // FailedItem: { + // BucketName: "tf-macie-example-002" + // } + // } + if aws.StringValue(failed.ErrorCode) == macie.ErrCodeInvalidInputException && + strings.Contains(aws.StringValue(failed.ErrorMessage), "is not associated with Macie") { + return nil + } + return fmt.Errorf("Error deleting Macie S3 bucket association: %s", failed) + } + + return nil +} + +func macieS3BucketAssociationId(d *schema.ResourceData) string { + return fmt.Sprintf("%s/%s", d.Get("bucket_name"), d.Get("prefix")) +} + +func macieS3BucketAssociationOneTimeClassification(d *schema.ResourceData) string { + oneTime := false + if v := d.Get("classification_type").([]interface{}); len(v) > 0 { + if m := v[0].(map[string]interface{}); m["one_time"].(bool) { + oneTime = true + } + } + if oneTime { + return macie.S3OneTimeClassificationTypeFull + } else { + return macie.S3OneTimeClassificationTypeNone + } +} diff --git a/aws/resource_aws_macie_s3_bucket_association_test.go b/aws/resource_aws_macie_s3_bucket_association_test.go new file mode 100644 index 00000000000..ddc60512db8 --- /dev/null +++ b/aws/resource_aws_macie_s3_bucket_association_test.go @@ -0,0 +1,174 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/macie" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSMacieS3BucketAssociation_basic(t *testing.T) { + rInt := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSMacieS3BucketAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSMacieS3BucketAssociationConfig_basic(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSMacieS3BucketAssociationExists("aws_macie_s3_bucket_association.test"), + resource.TestCheckResourceAttr("aws_macie_s3_bucket_association.test", "classification_type.0.one_time", "false"), + ), + }, + { + Config: testAccAWSMacieS3BucketAssociationConfig_basicOneTime(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSMacieS3BucketAssociationExists("aws_macie_s3_bucket_association.test"), + resource.TestCheckResourceAttr("aws_macie_s3_bucket_association.test", "classification_type.0.one_time", "true"), + ), + }, + }, + }) +} + +func TestAccAWSMacieS3BucketAssociation_accountIdAndPrefix(t *testing.T) { + rInt := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSMacieS3BucketAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSMacieS3BucketAssociationConfig_accountIdAndPrefix(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSMacieS3BucketAssociationExists("aws_macie_s3_bucket_association.test"), + resource.TestCheckResourceAttr("aws_macie_s3_bucket_association.test", "classification_type.0.one_time", "false"), + ), + }, + { + Config: testAccAWSMacieS3BucketAssociationConfig_accountIdAndPrefixOneTime(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSMacieS3BucketAssociationExists("aws_macie_s3_bucket_association.test"), + resource.TestCheckResourceAttr("aws_macie_s3_bucket_association.test", "classification_type.0.one_time", "true"), + ), + }, + }, + }) +} + +func testAccCheckAWSMacieS3BucketAssociationDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).macieconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_macie_s3_bucket_association" { + continue + } + + req := &macie.ListS3ResourcesInput{} + acctId := rs.Primary.Attributes["member_account_id"] + if acctId != "" { + req.MemberAccountId = aws.String(acctId) + } + + for { + resp, err := conn.ListS3Resources(req) + if err != nil { + return err + } + + for _, v := range resp.S3Resources { + if aws.StringValue(v.BucketName) == rs.Primary.Attributes["bucket_name"] && aws.StringValue(v.Prefix) == rs.Primary.Attributes["prefix"] { + return fmt.Errorf("S3 resource %s/%s is not dissociated from Macie", rs.Primary.Attributes["bucket_name"], rs.Primary.Attributes["prefix"]) + } + } + + if resp.NextToken == nil { + break + } + req.NextToken = resp.NextToken + } + } + return nil +} + +func testAccCheckAWSMacieS3BucketAssociationExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + return nil + } +} + +func testAccAWSMacieS3BucketAssociationConfig_basic(randInt int) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = "tf-macie-test-bucket-%d" +} + +resource "aws_macie_s3_bucket_association" "test" { + bucket_name = "${aws_s3_bucket.test.id}" +} +`, randInt) +} + +func testAccAWSMacieS3BucketAssociationConfig_basicOneTime(randInt int) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = "tf-macie-test-bucket-%d" +} + +resource "aws_macie_s3_bucket_association" "test" { + bucket_name = "${aws_s3_bucket.test.id}" + + classification_type { + one_time = true + } +} +`, randInt) +} + +func testAccAWSMacieS3BucketAssociationConfig_accountIdAndPrefix(randInt int) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = "tf-macie-test-bucket-%d" +} + +data "aws_caller_identity" "current" {} + +resource "aws_macie_s3_bucket_association" "test" { + bucket_name = "${aws_s3_bucket.test.id}" + member_account_id = "${data.aws_caller_identity.current.account_id}" + prefix = "data" +} +`, randInt) +} + +func testAccAWSMacieS3BucketAssociationConfig_accountIdAndPrefixOneTime(randInt int) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = "tf-macie-test-bucket-%d" +} + +data "aws_caller_identity" "current" {} + +resource "aws_macie_s3_bucket_association" "test" { + bucket_name = "${aws_s3_bucket.test.id}" + member_account_id = "${data.aws_caller_identity.current.account_id}" + prefix = "data" + + classification_type { + one_time = true + } +} +`, randInt) +} diff --git a/website/aws.erb b/website/aws.erb index 828e278e00e..d9cc28defb5 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1509,6 +1509,17 @@ + > + Macie Resources + + + > MQ Resources