From 13845f9ce40d441ccf879400fbc424ebacf5546f Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 9 Jan 2018 09:23:09 -0800 Subject: [PATCH 1/3] New Resource: aws_guardduty_member --- aws/provider.go | 1 + aws/resource_aws_guardduty_member.go | 121 ++++++++++++++++ aws/resource_aws_guardduty_member_test.go | 133 ++++++++++++++++++ aws/resource_aws_guardduty_test.go | 4 + website/aws.erb | 4 + website/docs/r/guardduty_member.html.markdown | 55 ++++++++ 6 files changed, 318 insertions(+) create mode 100644 aws/resource_aws_guardduty_member.go create mode 100644 aws/resource_aws_guardduty_member_test.go create mode 100644 website/docs/r/guardduty_member.html.markdown diff --git a/aws/provider.go b/aws/provider.go index b3b3ac340c2..4c3af722c88 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -356,6 +356,7 @@ func Provider() terraform.ResourceProvider { "aws_flow_log": resourceAwsFlowLog(), "aws_glacier_vault": resourceAwsGlacierVault(), "aws_guardduty_detector": resourceAwsGuardDutyDetector(), + "aws_guardduty_member": resourceAwsGuardDutyMember(), "aws_iam_access_key": resourceAwsIamAccessKey(), "aws_iam_account_alias": resourceAwsIamAccountAlias(), "aws_iam_account_password_policy": resourceAwsIamAccountPasswordPolicy(), diff --git a/aws/resource_aws_guardduty_member.go b/aws/resource_aws_guardduty_member.go new file mode 100644 index 00000000000..937cce3f760 --- /dev/null +++ b/aws/resource_aws_guardduty_member.go @@ -0,0 +1,121 @@ +package aws + +import ( + "fmt" + "log" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/guardduty" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsGuardDutyMember() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsGuardDutyMemberCreate, + Read: resourceAwsGuardDutyMemberRead, + Delete: resourceAwsGuardDutyMemberDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "account_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateAwsAccountId, + }, + "detector_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "email": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsGuardDutyMemberCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).guarddutyconn + accountID := d.Get("account_id").(string) + detectorID := d.Get("detector_id").(string) + + input := guardduty.CreateMembersInput{ + AccountDetails: []*guardduty.AccountDetail{{ + AccountId: aws.String(accountID), + Email: aws.String(d.Get("email").(string)), + }}, + DetectorId: aws.String(detectorID), + } + + log.Printf("[DEBUG] Creating GuardDuty Member: %s", input) + _, err := conn.CreateMembers(&input) + if err != nil { + return fmt.Errorf("Creating GuardDuty Member failed: %s", err.Error()) + } + d.SetId(fmt.Sprintf("%s:%s", detectorID, accountID)) + + return resourceAwsGuardDutyMemberRead(d, meta) +} + +func resourceAwsGuardDutyMemberRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).guarddutyconn + + idParts := strings.Split(d.Id(), ":") + accountID := idParts[1] + detectorID := idParts[0] + + input := guardduty.GetMembersInput{ + AccountIds: []*string{aws.String(accountID)}, + DetectorId: aws.String(detectorID), + } + + log.Printf("[DEBUG] Reading GuardDuty Member: %s", input) + gmo, err := conn.GetMembers(&input) + if err != nil { + if isAWSErr(err, guardduty.ErrCodeBadRequestException, "The request is rejected because the input detectorId is not owned by the current account.") { + log.Printf("[WARN] GuardDuty detector %q not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("Reading GuardDuty Member '%s' failed: %s", d.Id(), err.Error()) + } + + if gmo.Members == nil || (len(gmo.Members) < 1) { + log.Printf("[WARN] GuardDuty Member %q not found, removing from state", d.Id()) + d.SetId("") + return nil + } + member := gmo.Members[0] + d.Set("account_id", *member.AccountId) + d.Set("detector_id", detectorID) + d.Set("email", *member.Email) + + return nil +} + +func resourceAwsGuardDutyMemberDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).guarddutyconn + + idParts := strings.Split(d.Id(), ":") + accountID := idParts[1] + detectorID := idParts[0] + + input := guardduty.DeleteMembersInput{ + AccountIds: []*string{aws.String(accountID)}, + DetectorId: aws.String(detectorID), + } + + log.Printf("[DEBUG] Delete GuardDuty Member: %s", input) + _, err := conn.DeleteMembers(&input) + if err != nil { + return fmt.Errorf("Deleting GuardDuty Member '%s' failed: %s", d.Id(), err.Error()) + } + return nil +} diff --git a/aws/resource_aws_guardduty_member_test.go b/aws/resource_aws_guardduty_member_test.go new file mode 100644 index 00000000000..db674af55fb --- /dev/null +++ b/aws/resource_aws_guardduty_member_test.go @@ -0,0 +1,133 @@ +package aws + +import ( + "fmt" + "strings" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/guardduty" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func testAccAwsGuardDutyMember_basic(t *testing.T) { + resourceName := "aws_guardduty_member.test" + accountID := "111111111111" + email := "required@example.com" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsGuardDutyMemberDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGuardDutyMemberConfig_basic(accountID, email), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyMemberExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "account_id", accountID), + resource.TestCheckResourceAttrSet(resourceName, "detector_id"), + resource.TestCheckResourceAttr(resourceName, "email", email), + ), + }, + }, + }) +} + +func testAccAwsGuardDutyMember_import(t *testing.T) { + resourceName := "aws_guardduty_member.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckSesTemplateDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccGuardDutyMemberConfig_basic("111111111111", "required@example.com"), + }, + + resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAwsGuardDutyMemberDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).guarddutyconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_guardduty_member" { + continue + } + + idParts := strings.Split(rs.Primary.ID, ":") + accountID := idParts[1] + detectorID := idParts[0] + + input := &guardduty.GetMembersInput{ + AccountIds: []*string{aws.String(accountID)}, + DetectorId: aws.String(detectorID), + } + + gmo, err := conn.GetMembers(input) + if err != nil { + if isAWSErr(err, guardduty.ErrCodeBadRequestException, "The request is rejected because the input detectorId is not owned by the current account.") { + return nil + } + return err + } + + if len(gmo.Members) < 1 { + continue + } + + return fmt.Errorf("Expected GuardDuty Detector to be destroyed, %s found", rs.Primary.ID) + } + + return nil +} + +func testAccCheckAwsGuardDutyMemberExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + idParts := strings.Split(rs.Primary.ID, ":") + accountID := idParts[1] + detectorID := idParts[0] + + input := &guardduty.GetMembersInput{ + AccountIds: []*string{aws.String(accountID)}, + DetectorId: aws.String(detectorID), + } + + conn := testAccProvider.Meta().(*AWSClient).guarddutyconn + gmo, err := conn.GetMembers(input) + if err != nil { + return err + } + + if len(gmo.Members) < 1 { + return fmt.Errorf("Not found: %s", name) + } + + return nil + } +} + +func testAccGuardDutyMemberConfig_basic(accountID, email string) string { + return fmt.Sprintf(` +%[1]s + +resource "aws_guardduty_member" "test" { + account_id = "%[2]s" + detector_id = "${aws_guardduty_detector.test.id}" + email = "%[3]s" +} +`, testAccGuardDutyDetectorConfig_basic1, accountID, email) +} diff --git a/aws/resource_aws_guardduty_test.go b/aws/resource_aws_guardduty_test.go index 5fb8d764f34..bee271457ca 100644 --- a/aws/resource_aws_guardduty_test.go +++ b/aws/resource_aws_guardduty_test.go @@ -10,6 +10,10 @@ func TestAccAWSGuardDuty(t *testing.T) { "basic": testAccAwsGuardDutyDetector_basic, "import": testAccAwsGuardDutyDetector_import, }, + "Member": { + "basic": testAccAwsGuardDutyMember_basic, + "import": testAccAwsGuardDutyMember_import, + }, } for group, m := range testCases { diff --git a/website/aws.erb b/website/aws.erb index ee605f3fb6f..530c8d4fef8 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -925,6 +925,10 @@ > aws_guardduty_detector + + > + aws_guardduty_member + diff --git a/website/docs/r/guardduty_member.html.markdown b/website/docs/r/guardduty_member.html.markdown new file mode 100644 index 00000000000..13e8d6a0382 --- /dev/null +++ b/website/docs/r/guardduty_member.html.markdown @@ -0,0 +1,55 @@ +--- +layout: "aws" +page_title: "AWS: aws_guardduty_member" +sidebar_current: "docs-aws-resource-guardduty-member" +description: |- + Provides a resource to manage a GuardDuty member +--- + +# aws_guardduty_member + +Provides a resource to manage a GuardDuty member. + +~> **NOTE:** Currently after using this resource, you must manually invite and accept member account invitations before GuardDuty will begin sending cross-account events. More information for how to accomplish this via the AWS Console or API can be found in the [GuardDuty User Guide](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_accounts.html). Terraform implementation of member invitation and acceptance resources can be tracked in [Github](https://github.com/terraform-providers/terraform-provider-aws/issues/2489). + +## Example Usage + +```hcl +resource "aws_guardduty_detector" "master" { + enable = true +} + +resource "aws_guardduty_detector" "member" { + provider = "aws.dev" + + enable = true +} + +resource "aws_guardduty_member" "member" { + account_id = "${aws_guardduty_detector.member.account_id}" + detector_id = "${aws_guardduty_detector.master.id}" + email = "required@example.com" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `account_id` - (Required) AWS account ID for member account. +* `detector_id` - (Required) The detector ID of the GuardDuty account where you want to create member accounts. +* `email` - (Required) Email address for member account. + +## Attributes Reference + +The following additional attributes are exported: + +* `id` - The ID of the GuardDuty member + +## Import + +GuardDuty members can be imported using the the master GuardDuty detector ID and member AWS account ID, e.g. + +``` +$ terraform import aws_guardduty_member.MyMember 00b00fd5aecc0ab60a708659477e9617:123456789012 +``` From 33d551c45448b914be63e5ccdab5118b6957cc86 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 11 Jan 2018 11:03:41 -0500 Subject: [PATCH 2/3] r/aws_guardduty_member: #2911 PR review * Do not unsafely dereference member attributes for d.Set() * Separate ID parsing into decodeGuardDutyMemberID with error checking --- aws/resource_aws_guardduty_member.go | 31 ++++++++++++++++------- aws/resource_aws_guardduty_member_test.go | 15 ++++++----- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/aws/resource_aws_guardduty_member.go b/aws/resource_aws_guardduty_member.go index 937cce3f760..936037c0351 100644 --- a/aws/resource_aws_guardduty_member.go +++ b/aws/resource_aws_guardduty_member.go @@ -67,9 +67,10 @@ func resourceAwsGuardDutyMemberCreate(d *schema.ResourceData, meta interface{}) func resourceAwsGuardDutyMemberRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).guarddutyconn - idParts := strings.Split(d.Id(), ":") - accountID := idParts[1] - detectorID := idParts[0] + accountID, detectorID, err := decodeGuardDutyMemberID(d.Id()) + if err != nil { + return err + } input := guardduty.GetMembersInput{ AccountIds: []*string{aws.String(accountID)}, @@ -93,9 +94,9 @@ func resourceAwsGuardDutyMemberRead(d *schema.ResourceData, meta interface{}) er return nil } member := gmo.Members[0] - d.Set("account_id", *member.AccountId) + d.Set("account_id", member.AccountId) d.Set("detector_id", detectorID) - d.Set("email", *member.Email) + d.Set("email", member.Email) return nil } @@ -103,9 +104,10 @@ func resourceAwsGuardDutyMemberRead(d *schema.ResourceData, meta interface{}) er func resourceAwsGuardDutyMemberDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).guarddutyconn - idParts := strings.Split(d.Id(), ":") - accountID := idParts[1] - detectorID := idParts[0] + accountID, detectorID, err := decodeGuardDutyMemberID(d.Id()) + if err != nil { + return err + } input := guardduty.DeleteMembersInput{ AccountIds: []*string{aws.String(accountID)}, @@ -113,9 +115,20 @@ func resourceAwsGuardDutyMemberDelete(d *schema.ResourceData, meta interface{}) } log.Printf("[DEBUG] Delete GuardDuty Member: %s", input) - _, err := conn.DeleteMembers(&input) + _, err = conn.DeleteMembers(&input) if err != nil { return fmt.Errorf("Deleting GuardDuty Member '%s' failed: %s", d.Id(), err.Error()) } return nil } + +func decodeGuardDutyMemberID(id string) (accountID, detectorID string, err error) { + parts := strings.Split(id, ":") + if len(parts) != 2 { + err = fmt.Errorf("GuardDuty Member ID must be of the form :") + return + } + accountID = parts[1] + detectorID = parts[0] + return +} diff --git a/aws/resource_aws_guardduty_member_test.go b/aws/resource_aws_guardduty_member_test.go index db674af55fb..f7f6d6dcf25 100644 --- a/aws/resource_aws_guardduty_member_test.go +++ b/aws/resource_aws_guardduty_member_test.go @@ -2,7 +2,6 @@ package aws import ( "fmt" - "strings" "testing" "github.com/aws/aws-sdk-go/aws" @@ -63,9 +62,10 @@ func testAccCheckAwsGuardDutyMemberDestroy(s *terraform.State) error { continue } - idParts := strings.Split(rs.Primary.ID, ":") - accountID := idParts[1] - detectorID := idParts[0] + accountID, detectorID, err := decodeGuardDutyMemberID(rs.Primary.ID) + if err != nil { + return err + } input := &guardduty.GetMembersInput{ AccountIds: []*string{aws.String(accountID)}, @@ -97,9 +97,10 @@ func testAccCheckAwsGuardDutyMemberExists(name string) resource.TestCheckFunc { return fmt.Errorf("Not found: %s", name) } - idParts := strings.Split(rs.Primary.ID, ":") - accountID := idParts[1] - detectorID := idParts[0] + accountID, detectorID, err := decodeGuardDutyMemberID(rs.Primary.ID) + if err != nil { + return err + } input := &guardduty.GetMembersInput{ AccountIds: []*string{aws.String(accountID)}, From c3ab8de2a93b2b646148fef83ec980123daa8bf3 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 11 Jan 2018 13:37:00 -0500 Subject: [PATCH 3/3] r/aws_guardduty_member: Provide given ID in error message when incorrect format --- aws/resource_aws_guardduty_member.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/resource_aws_guardduty_member.go b/aws/resource_aws_guardduty_member.go index 936037c0351..90cfbdc83d0 100644 --- a/aws/resource_aws_guardduty_member.go +++ b/aws/resource_aws_guardduty_member.go @@ -125,7 +125,7 @@ func resourceAwsGuardDutyMemberDelete(d *schema.ResourceData, meta interface{}) func decodeGuardDutyMemberID(id string) (accountID, detectorID string, err error) { parts := strings.Split(id, ":") if len(parts) != 2 { - err = fmt.Errorf("GuardDuty Member ID must be of the form :") + err = fmt.Errorf("GuardDuty Member ID must be of the form :, was provided: %s", id) return } accountID = parts[1]