diff --git a/aws/resource_aws_guardduty_member.go b/aws/resource_aws_guardduty_member.go index 1cab4aaf94d3..49ca3beb9ca3 100644 --- a/aws/resource_aws_guardduty_member.go +++ b/aws/resource_aws_guardduty_member.go @@ -16,6 +16,7 @@ func resourceAwsGuardDutyMember() *schema.Resource { return &schema.Resource{ Create: resourceAwsGuardDutyMemberCreate, Read: resourceAwsGuardDutyMemberRead, + Update: resourceAwsGuardDutyMemberUpdate, Delete: resourceAwsGuardDutyMemberDelete, Importer: &schema.ResourceImporter{ @@ -46,8 +47,6 @@ func resourceAwsGuardDutyMember() *schema.Resource { "invite": { Type: schema.TypeBool, Optional: true, - ForceNew: true, - Computed: true, }, "disable_email_notification": { Type: schema.TypeBool, @@ -62,6 +61,7 @@ func resourceAwsGuardDutyMember() *schema.Resource { }, Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(60 * time.Second), + Update: schema.DefaultTimeout(60 * time.Second), }, } } @@ -98,48 +98,15 @@ func resourceAwsGuardDutyMemberCreate(d *schema.ResourceData, meta interface{}) Message: aws.String(d.Get("invitation_message").(string)), } + log.Printf("[INFO] Inviting GuardDuty Member: %s", input) _, err = conn.InviteMembers(imi) if err != nil { - return fmt.Errorf("Inviting GuardDuty Member failed: %s", err) + return fmt.Errorf("error inviting GuardDuty Member %q: %s", d.Id(), err) } - // wait until e-mail verification finishes - if err = resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { - input := guardduty.GetMembersInput{ - DetectorId: imi.DetectorId, - AccountIds: imi.AccountIds, - } - - 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 resource.NonRetryableError(fmt.Errorf("error reading GuardDuty Member %q: %s", d.Id(), err)) - } - - if gmo == nil || len(gmo.Members) == 0 { - return resource.RetryableError(fmt.Errorf("error reading GuardDuty Member %q: member missing from response", d.Id())) - } - - member := gmo.Members[0] - status := aws.StringValue(member.RelationshipStatus) - - if status == "Disabled" || status == "Enabled" || status == "Invited" { - return nil - } - - if status == "Created" || status == "EmailVerificationInProgress" { - return resource.RetryableError(fmt.Errorf("Expected member to be invited but was in state: %s", status)) - } - - return resource.NonRetryableError(fmt.Errorf("error inviting GuardDuty Member %q: invalid status: %s", d.Id(), status)) - }); err != nil { - return err + err = inviteGuardDutyMemberWaiter(accountID, detectorID, d.Timeout(schema.TimeoutUpdate), conn) + if err != nil { + return fmt.Errorf("error waiting for GuardDuty Member %q invite: %s", d.Id(), err) } return resourceAwsGuardDutyMemberRead(d, meta) @@ -192,6 +159,54 @@ func resourceAwsGuardDutyMemberRead(d *schema.ResourceData, meta interface{}) er return nil } +func resourceAwsGuardDutyMemberUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).guarddutyconn + + accountID, detectorID, err := decodeGuardDutyMemberID(d.Id()) + if err != nil { + return err + } + + if d.HasChange("invite") { + if d.Get("invite").(bool) { + input := &guardduty.InviteMembersInput{ + DetectorId: aws.String(detectorID), + AccountIds: []*string{aws.String(accountID)}, + DisableEmailNotification: aws.Bool(d.Get("disable_email_notification").(bool)), + Message: aws.String(d.Get("invitation_message").(string)), + } + + log.Printf("[INFO] Inviting GuardDuty Member: %s", input) + output, err := conn.InviteMembers(input) + if err != nil { + return fmt.Errorf("error inviting GuardDuty Member %q: %s", d.Id(), err) + } + + // {"unprocessedAccounts":[{"result":"The request is rejected because the current account has already invited or is already the GuardDuty master of the given member account ID.","accountId":"067819342479"}]} + if len(output.UnprocessedAccounts) > 0 { + return fmt.Errorf("error inviting GuardDuty Member %q: %s", d.Id(), aws.StringValue(output.UnprocessedAccounts[0].Result)) + } + + err = inviteGuardDutyMemberWaiter(accountID, detectorID, d.Timeout(schema.TimeoutUpdate), conn) + if err != nil { + return fmt.Errorf("error waiting for GuardDuty Member %q invite: %s", d.Id(), err) + } + } else { + input := &guardduty.DisassociateMembersInput{ + AccountIds: []*string{aws.String(accountID)}, + DetectorId: aws.String(detectorID), + } + log.Printf("[INFO] Disassociating GuardDuty Member: %s", input) + _, err := conn.DisassociateMembers(input) + if err != nil { + return fmt.Errorf("error disassociating GuardDuty Member %q: %s", d.Id(), err) + } + } + } + + return resourceAwsGuardDutyMemberRead(d, meta) +} + func resourceAwsGuardDutyMemberDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).guarddutyconn @@ -213,6 +228,40 @@ func resourceAwsGuardDutyMemberDelete(d *schema.ResourceData, meta interface{}) return nil } +func inviteGuardDutyMemberWaiter(accountID, detectorID string, timeout time.Duration, conn *guardduty.GuardDuty) error { + input := guardduty.GetMembersInput{ + DetectorId: aws.String(detectorID), + AccountIds: []*string{aws.String(accountID)}, + } + + // wait until e-mail verification finishes + return resource.Retry(timeout, func() *resource.RetryError { + log.Printf("[DEBUG] Reading GuardDuty Member: %s", input) + gmo, err := conn.GetMembers(&input) + + if err != nil { + return resource.NonRetryableError(fmt.Errorf("error reading GuardDuty Member %q: %s", accountID, err)) + } + + if gmo == nil || len(gmo.Members) == 0 { + return resource.RetryableError(fmt.Errorf("error reading GuardDuty Member %q: member missing from response", accountID)) + } + + member := gmo.Members[0] + status := aws.StringValue(member.RelationshipStatus) + + if status == "Disabled" || status == "Enabled" || status == "Invited" { + return nil + } + + if status == "Created" || status == "EmailVerificationInProgress" { + return resource.RetryableError(fmt.Errorf("Expected member to be invited but was in state: %s", status)) + } + + return resource.NonRetryableError(fmt.Errorf("error inviting GuardDuty Member %q: invalid status: %s", accountID, status)) + }) +} + func decodeGuardDutyMemberID(id string) (accountID, detectorID string, err error) { parts := strings.Split(id, ":") if len(parts) != 2 { diff --git a/aws/resource_aws_guardduty_member_test.go b/aws/resource_aws_guardduty_member_test.go index 4bc59716cd91..c32073d19d89 100644 --- a/aws/resource_aws_guardduty_member_test.go +++ b/aws/resource_aws_guardduty_member_test.go @@ -27,6 +27,7 @@ func testAccAwsGuardDutyMember_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "account_id", accountID), resource.TestCheckResourceAttrSet(resourceName, "detector_id"), resource.TestCheckResourceAttr(resourceName, "email", email), + resource.TestCheckResourceAttr(resourceName, "relationship_status", "Created"), ), }, { @@ -38,7 +39,83 @@ func testAccAwsGuardDutyMember_basic(t *testing.T) { }) } -func testAccAwsGuardDutyMember_invite(t *testing.T) { +func testAccAwsGuardDutyMember_invite_disassociate(t *testing.T) { + resourceName := "aws_guardduty_member.test" + accountID, email := testAccAWSGuardDutyMemberFromEnv(t) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsGuardDutyMemberDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGuardDutyMemberConfig_invite(accountID, email, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyMemberExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "invite", "true"), + resource.TestCheckResourceAttr(resourceName, "relationship_status", "Invited"), + ), + }, + // Disassociate member + { + Config: testAccGuardDutyMemberConfig_invite(accountID, email, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyMemberExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "invite", "false"), + resource.TestCheckResourceAttr(resourceName, "relationship_status", "Removed"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "disable_email_notification", + }, + }, + }, + }) +} + +func testAccAwsGuardDutyMember_invite_onUpdate(t *testing.T) { + resourceName := "aws_guardduty_member.test" + accountID, email := testAccAWSGuardDutyMemberFromEnv(t) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsGuardDutyMemberDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGuardDutyMemberConfig_invite(accountID, email, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyMemberExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "invite", "false"), + resource.TestCheckResourceAttr(resourceName, "relationship_status", "Created"), + ), + }, + // Invite member + { + Config: testAccGuardDutyMemberConfig_invite(accountID, email, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsGuardDutyMemberExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "invite", "true"), + resource.TestCheckResourceAttr(resourceName, "relationship_status", "Invited"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "disable_email_notification", + }, + }, + }, + }) +} + +func testAccAwsGuardDutyMember_invitationMessage(t *testing.T) { resourceName := "aws_guardduty_member.test" accountID, email := testAccAWSGuardDutyMemberFromEnv(t) invitationMessage := "inviting" @@ -49,7 +126,7 @@ func testAccAwsGuardDutyMember_invite(t *testing.T) { CheckDestroy: testAccCheckAwsGuardDutyMemberDestroy, Steps: []resource.TestStep{ { - Config: testAccGuardDutyMemberConfig_invite(accountID, email, invitationMessage), + Config: testAccGuardDutyMemberConfig_invitationMessage(accountID, email, invitationMessage), Check: resource.ComposeTestCheckFunc( testAccCheckAwsGuardDutyMemberExists(resourceName), resource.TestCheckResourceAttr(resourceName, "account_id", accountID), @@ -58,6 +135,7 @@ func testAccAwsGuardDutyMember_invite(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "email", email), resource.TestCheckResourceAttr(resourceName, "invite", "true"), resource.TestCheckResourceAttr(resourceName, "invitation_message", invitationMessage), + resource.TestCheckResourceAttr(resourceName, "relationship_status", "Invited"), ), }, { @@ -152,7 +230,21 @@ resource "aws_guardduty_member" "test" { `, testAccGuardDutyDetectorConfig_basic1, accountID, email) } -func testAccGuardDutyMemberConfig_invite(accountID, email, invitationMessage string) string { +func testAccGuardDutyMemberConfig_invite(accountID, email string, invite bool) string { + return fmt.Sprintf(` +%[1]s + +resource "aws_guardduty_member" "test" { + account_id = "%[2]s" + detector_id = "${aws_guardduty_detector.test.id}" + disable_email_notification = true + email = "%[3]s" + invite = %[4]t +} +`, testAccGuardDutyDetectorConfig_basic1, accountID, email, invite) +} + +func testAccGuardDutyMemberConfig_invitationMessage(accountID, email, invitationMessage string) string { return fmt.Sprintf(` %[1]s diff --git a/aws/resource_aws_guardduty_test.go b/aws/resource_aws_guardduty_test.go index 5c2ecb087fb3..93a4fbfbe031 100644 --- a/aws/resource_aws_guardduty_test.go +++ b/aws/resource_aws_guardduty_test.go @@ -20,8 +20,10 @@ func TestAccAWSGuardDuty(t *testing.T) { "import": testAccAwsGuardDutyThreatintelset_import, }, "Member": { - "basic": testAccAwsGuardDutyMember_basic, - "invite": testAccAwsGuardDutyMember_invite, + "basic": testAccAwsGuardDutyMember_basic, + "inviteOnUpdate": testAccAwsGuardDutyMember_invite_onUpdate, + "inviteDisassociate": testAccAwsGuardDutyMember_invite_disassociate, + "invitationMessage": testAccAwsGuardDutyMember_invitationMessage, }, } diff --git a/website/docs/r/guardduty_member.html.markdown b/website/docs/r/guardduty_member.html.markdown index 492c99f7a1f9..f14d63534e34 100644 --- a/website/docs/r/guardduty_member.html.markdown +++ b/website/docs/r/guardduty_member.html.markdown @@ -51,6 +51,7 @@ The following arguments are supported: configuration options: - `create` - (Default `60s`) How long to wait for a verification to be done against inviting GuardDuty member account. +- `update` - (Default `60s`) How long to wait for a verification to be done against inviting GuardDuty member account. ## Attributes Reference