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 cd411969fb1..83a8203337a 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -492,6 +492,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..8f8606eba09 --- /dev/null +++ b/aws/resource_aws_macie_s3_bucket_association.go @@ -0,0 +1,206 @@ +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" + "github.com/hashicorp/terraform/helper/validation" +) + +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{ + "continuous": { + Type: schema.TypeString, + Optional: true, + Default: macie.S3ContinuousClassificationTypeFull, + ValidateFunc: validation.StringInSlice([]string{macie.S3ContinuousClassificationTypeFull}, false), + }, + "one_time": { + Type: schema.TypeString, + Optional: true, + Default: macie.S3OneTimeClassificationTypeNone, + ValidateFunc: validation.StringInSlice([]string{macie.S3OneTimeClassificationTypeFull, macie.S3OneTimeClassificationTypeNone}, 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)), + ClassificationType: expandMacieClassificationType(d), + }, + }, + } + 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)) + } + + 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(fmt.Sprintf("%s/%s", d.Get("bucket_name"), d.Get("prefix"))) + 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 + err := conn.ListS3ResourcesPages(req, func(page *macie.ListS3ResourcesOutput, lastPage bool) bool { + for _, v := range page.S3Resources { + if aws.StringValue(v.BucketName) == bucketName && aws.StringValue(v.Prefix) == prefix { + res = v + return false + } + } + + return true + }) + if err != nil { + return fmt.Errorf("Error listing Macie S3 bucket associations: %s", err) + } + + if res == nil { + log.Printf("[WARN] Macie S3 bucket association (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err := d.Set("classification_type", flattenMacieClassificationType(res.ClassificationType)); err != nil { + return fmt.Errorf("error setting classification_type: %s", 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: expandMacieClassificationTypeUpdate(d), + }, + }, + } + 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)) + } + + 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 +} 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..9d09b06efac --- /dev/null +++ b/aws/resource_aws_macie_s3_bucket_association_test.go @@ -0,0 +1,206 @@ +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.continuous", "FULL"), + resource.TestCheckResourceAttr("aws_macie_s3_bucket_association.test", "classification_type.0.one_time", "NONE"), + ), + }, + { + 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.continuous", "FULL"), + resource.TestCheckResourceAttr("aws_macie_s3_bucket_association.test", "classification_type.0.one_time", "FULL"), + ), + }, + }, + }) +} + +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.continuous", "FULL"), + resource.TestCheckResourceAttr("aws_macie_s3_bucket_association.test", "classification_type.0.one_time", "NONE"), + ), + }, + { + 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.continuous", "FULL"), + resource.TestCheckResourceAttr("aws_macie_s3_bucket_association.test", "classification_type.0.one_time", "FULL"), + ), + }, + }, + }) +} + +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) + } + + dissociated := true + err := conn.ListS3ResourcesPages(req, func(page *macie.ListS3ResourcesOutput, lastPage bool) bool { + for _, v := range page.S3Resources { + if aws.StringValue(v.BucketName) == rs.Primary.Attributes["bucket_name"] && aws.StringValue(v.Prefix) == rs.Primary.Attributes["prefix"] { + dissociated = false + return false + } + } + + return true + }) + if err != nil { + return err + } + + if !dissociated { + return fmt.Errorf("S3 resource %s/%s is not dissociated from Macie", rs.Primary.Attributes["bucket_name"], rs.Primary.Attributes["prefix"]) + } + } + return nil +} + +func testAccCheckAWSMacieS3BucketAssociationExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).macieconn + + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + req := &macie.ListS3ResourcesInput{} + acctId := rs.Primary.Attributes["member_account_id"] + if acctId != "" { + req.MemberAccountId = aws.String(acctId) + } + + exists := false + err := conn.ListS3ResourcesPages(req, func(page *macie.ListS3ResourcesOutput, lastPage bool) bool { + for _, v := range page.S3Resources { + if aws.StringValue(v.BucketName) == rs.Primary.Attributes["bucket_name"] && aws.StringValue(v.Prefix) == rs.Primary.Attributes["prefix"] { + exists = true + return false + } + } + + return true + }) + if err != nil { + return err + } + + if !exists { + return fmt.Errorf("S3 resource %s/%s is not associated with Macie", rs.Primary.Attributes["bucket_name"], rs.Primary.Attributes["prefix"]) + } + + 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 = "FULL" + } +} +`, 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 = "FULL" + } +} +`, randInt) +} diff --git a/aws/structure.go b/aws/structure.go index fd8136c8b7a..3cc071721d1 100644 --- a/aws/structure.go +++ b/aws/structure.go @@ -32,6 +32,7 @@ import ( "github.com/aws/aws-sdk-go/service/iot" "github.com/aws/aws-sdk-go/service/kinesis" "github.com/aws/aws-sdk-go/service/lambda" + "github.com/aws/aws-sdk-go/service/macie" "github.com/aws/aws-sdk-go/service/mq" "github.com/aws/aws-sdk-go/service/neptune" "github.com/aws/aws-sdk-go/service/rds" @@ -4309,3 +4310,44 @@ func flattenDxRouteFilterPrefixes(prefixes []*directconnect.RouteFilterPrefix) * } return schema.NewSet(schema.HashString, out) } + +func expandMacieClassificationType(d *schema.ResourceData) *macie.ClassificationType { + continuous := macie.S3ContinuousClassificationTypeFull + oneTime := macie.S3OneTimeClassificationTypeNone + if v := d.Get("classification_type").([]interface{}); len(v) > 0 { + m := v[0].(map[string]interface{}) + continuous = m["continuous"].(string) + oneTime = m["one_time"].(string) + } + + return &macie.ClassificationType{ + Continuous: aws.String(continuous), + OneTime: aws.String(oneTime), + } +} + +func expandMacieClassificationTypeUpdate(d *schema.ResourceData) *macie.ClassificationTypeUpdate { + continuous := macie.S3ContinuousClassificationTypeFull + oneTime := macie.S3OneTimeClassificationTypeNone + if v := d.Get("classification_type").([]interface{}); len(v) > 0 { + m := v[0].(map[string]interface{}) + continuous = m["continuous"].(string) + oneTime = m["one_time"].(string) + } + + return &macie.ClassificationTypeUpdate{ + Continuous: aws.String(continuous), + OneTime: aws.String(oneTime), + } +} + +func flattenMacieClassificationType(classificationType *macie.ClassificationType) []map[string]interface{} { + if classificationType == nil { + return []map[string]interface{}{} + } + m := map[string]interface{}{ + "continuous": aws.StringValue(classificationType.Continuous), + "one_time": aws.StringValue(classificationType.OneTime), + } + return []map[string]interface{}{m} +} diff --git a/website/aws.erb b/website/aws.erb index 4949abc67b4..9f315c478f1 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1512,6 +1512,17 @@ + > + Macie Resources + + + > MQ Resources