From 9c4cfa4792421f0ac9ebec411e6dca1bbf8bddf8 Mon Sep 17 00:00:00 2001 From: Angie Pinilla Date: Sun, 7 Feb 2021 21:58:30 -0500 Subject: [PATCH 1/2] new resource: securityhub_organization_admin_account --- .../service/securityhub/finder/finder.go | 32 +++++ .../service/securityhub/waiter/status.go | 33 +++++ .../service/securityhub/waiter/waiter.go | 52 +++++++ aws/provider.go | 1 + aws/resource_aws_securityhub_account.go | 28 +++- ...urce_aws_securityhub_action_target_test.go | 6 + ..._securityhub_organization_admin_account.go | 112 +++++++++++++++ ...rityhub_organization_admin_account_test.go | 136 ++++++++++++++++++ aws/resource_aws_securityhub_test.go | 4 + ...b_organization_admin_account.html.markdown | 48 +++++++ 10 files changed, 450 insertions(+), 2 deletions(-) create mode 100644 aws/internal/service/securityhub/finder/finder.go create mode 100644 aws/internal/service/securityhub/waiter/status.go create mode 100644 aws/internal/service/securityhub/waiter/waiter.go create mode 100644 aws/resource_aws_securityhub_organization_admin_account.go create mode 100644 aws/resource_aws_securityhub_organization_admin_account_test.go create mode 100644 website/docs/r/securityhub_organization_admin_account.html.markdown diff --git a/aws/internal/service/securityhub/finder/finder.go b/aws/internal/service/securityhub/finder/finder.go new file mode 100644 index 00000000000..e13cd6a0708 --- /dev/null +++ b/aws/internal/service/securityhub/finder/finder.go @@ -0,0 +1,32 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/securityhub" +) + +func AdminAccount(conn *securityhub.SecurityHub, adminAccountID string) (*securityhub.AdminAccount, error) { + input := &securityhub.ListOrganizationAdminAccountsInput{} + var result *securityhub.AdminAccount + + err := conn.ListOrganizationAdminAccountsPages(input, func(page *securityhub.ListOrganizationAdminAccountsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, adminAccount := range page.AdminAccounts { + if adminAccount == nil { + continue + } + + if aws.StringValue(adminAccount.AccountId) == adminAccountID { + result = adminAccount + return false + } + } + + return !lastPage + }) + + return result, err +} diff --git a/aws/internal/service/securityhub/waiter/status.go b/aws/internal/service/securityhub/waiter/status.go new file mode 100644 index 00000000000..6ab9c336004 --- /dev/null +++ b/aws/internal/service/securityhub/waiter/status.go @@ -0,0 +1,33 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/securityhub/finder" +) + +const ( + // AdminStatus NotFound + AdminStatusNotFound = "NotFound" + + // AdminStatus Unknown + AdminStatusUnknown = "Unknown" +) + +// AdminAccountAdminStatus fetches the AdminAccount and its AdminStatus +func AdminAccountAdminStatus(conn *securityhub.SecurityHub, adminAccountID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + adminAccount, err := finder.AdminAccount(conn, adminAccountID) + + if err != nil { + return nil, AdminStatusUnknown, err + } + + if adminAccount == nil { + return adminAccount, AdminStatusNotFound, nil + } + + return adminAccount, aws.StringValue(adminAccount.Status), nil + } +} diff --git a/aws/internal/service/securityhub/waiter/waiter.go b/aws/internal/service/securityhub/waiter/waiter.go new file mode 100644 index 00000000000..deac42a9091 --- /dev/null +++ b/aws/internal/service/securityhub/waiter/waiter.go @@ -0,0 +1,52 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + // Maximum amount of time to wait for an AdminAccount to return Enabled + AdminAccountEnabledTimeout = 5 * time.Minute + + // Maximum amount of time to wait for an AdminAccount to return NotFound + AdminAccountNotFoundTimeout = 5 * time.Minute +) + +// AdminAccountEnabled waits for an AdminAccount to return Enabled +func AdminAccountEnabled(conn *securityhub.SecurityHub, adminAccountID string) (*securityhub.AdminAccount, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{AdminStatusNotFound}, + Target: []string{securityhub.AdminStatusEnabled}, + Refresh: AdminAccountAdminStatus(conn, adminAccountID), + Timeout: AdminAccountEnabledTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*securityhub.AdminAccount); ok { + return output, err + } + + return nil, err +} + +// AdminAccountNotFound waits for an AdminAccount to return NotFound +func AdminAccountNotFound(conn *securityhub.SecurityHub, adminAccountID string) (*securityhub.AdminAccount, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{securityhub.AdminStatusDisableInProgress}, + Target: []string{AdminStatusNotFound}, + Refresh: AdminAccountAdminStatus(conn, adminAccountID), + Timeout: AdminAccountNotFoundTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*securityhub.AdminAccount); ok { + return output, err + } + + return nil, err +} diff --git a/aws/provider.go b/aws/provider.go index be2f297a3ad..c350c16fdd4 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -936,6 +936,7 @@ func Provider() *schema.Provider { "aws_securityhub_account": resourceAwsSecurityHubAccount(), "aws_securityhub_action_target": resourceAwsSecurityHubActionTarget(), "aws_securityhub_member": resourceAwsSecurityHubMember(), + "aws_securityhub_organization_admin_account": resourceAwsSecurityHubOrganizationAdminAccount(), "aws_securityhub_product_subscription": resourceAwsSecurityHubProductSubscription(), "aws_securityhub_standards_subscription": resourceAwsSecurityHubStandardsSubscription(), "aws_servicecatalog_portfolio": resourceAwsServiceCatalogPortfolio(), diff --git a/aws/resource_aws_securityhub_account.go b/aws/resource_aws_securityhub_account.go index 49bb9fc4a4c..356eb361273 100644 --- a/aws/resource_aws_securityhub_account.go +++ b/aws/resource_aws_securityhub_account.go @@ -5,7 +5,11 @@ import ( "log" "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/securityhub/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func resourceAwsSecurityHubAccount() *schema.Resource { @@ -58,10 +62,30 @@ func resourceAwsSecurityHubAccountDelete(d *schema.ResourceData, meta interface{ conn := meta.(*AWSClient).securityhubconn log.Print("[DEBUG] Disabling Security Hub for account") - _, err := conn.DisableSecurityHub(&securityhub.DisableSecurityHubInput{}) + err := resource.Retry(waiter.AdminAccountNotFoundTimeout, func() *resource.RetryError { + _, err := conn.DisableSecurityHub(&securityhub.DisableSecurityHubInput{}) + + if tfawserr.ErrMessageContains(err, securityhub.ErrCodeInvalidInputException, "Cannot disable Security Hub on the Security Hub administrator") { + return resource.RetryableError(err) + } + + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + + if tfresource.TimedOut(err) { + _, err = conn.DisableSecurityHub(&securityhub.DisableSecurityHubInput{}) + } + + if tfawserr.ErrCodeEquals(err, securityhub.ErrCodeResourceNotFoundException) { + return nil + } if err != nil { - return fmt.Errorf("Error disabling Security Hub for account: %s", err) + return fmt.Errorf("Error disabling Security Hub for account: %w", err) } return nil diff --git a/aws/resource_aws_securityhub_action_target_test.go b/aws/resource_aws_securityhub_action_target_test.go index 7648ab476e7..bf6b21c0cb9 100644 --- a/aws/resource_aws_securityhub_action_target_test.go +++ b/aws/resource_aws_securityhub_action_target_test.go @@ -4,6 +4,8 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) @@ -154,6 +156,10 @@ func testAccCheckAwsSecurityHubActionTargetDestroy(s *terraform.State) error { action, err := resourceAwsSecurityHubActionTargetCheckExists(conn, rs.Primary.ID) + if tfawserr.ErrMessageContains(err, securityhub.ErrCodeInvalidAccessException, "not subscribed to AWS Security Hub") { + continue + } + if err != nil { return err } diff --git a/aws/resource_aws_securityhub_organization_admin_account.go b/aws/resource_aws_securityhub_organization_admin_account.go new file mode 100644 index 00000000000..0f1846c37f5 --- /dev/null +++ b/aws/resource_aws_securityhub_organization_admin_account.go @@ -0,0 +1,112 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/securityhub/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/securityhub/waiter" +) + +func resourceAwsSecurityHubOrganizationAdminAccount() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSecurityHubOrganizationAdminAccountCreate, + Read: resourceAwsSecurityHubOrganizationAdminAccountRead, + Delete: resourceAwsSecurityHubOrganizationAdminAccountDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "admin_account_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateAwsAccountId, + }, + }, + } +} + +func resourceAwsSecurityHubOrganizationAdminAccountCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + + adminAccountID := d.Get("admin_account_id").(string) + + input := &securityhub.EnableOrganizationAdminAccountInput{ + AdminAccountId: aws.String(adminAccountID), + } + + _, err := conn.EnableOrganizationAdminAccount(input) + + if err != nil { + return fmt.Errorf("error enabling Security Hub Organization Admin Account (%s): %w", adminAccountID, err) + } + + d.SetId(adminAccountID) + + if _, err := waiter.AdminAccountEnabled(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for Security Hub Organization Admin Account (%s) to enable: %w", d.Id(), err) + } + + return resourceAwsSecurityHubOrganizationAdminAccountRead(d, meta) +} + +func resourceAwsSecurityHubOrganizationAdminAccountRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + + adminAccount, err := finder.AdminAccount(conn, d.Id()) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, securityhub.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] Security Hub Organization Admin Account (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading Security Hub Organization Admin Account (%s): %w", d.Id(), err) + } + + if adminAccount == nil { + if d.IsNewResource() { + return fmt.Errorf("error reading Security Hub Organization Admin Account (%s): %w", d.Id(), err) + } + + log.Printf("[WARN] Security Hub Organization Admin Account (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + d.Set("admin_account_id", adminAccount.AccountId) + + return nil +} + +func resourceAwsSecurityHubOrganizationAdminAccountDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).securityhubconn + + input := &securityhub.DisableOrganizationAdminAccountInput{ + AdminAccountId: aws.String(d.Id()), + } + + _, err := conn.DisableOrganizationAdminAccount(input) + + if tfawserr.ErrCodeEquals(err, securityhub.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error disabling Security Hub Organization Admin Account (%s): %w", d.Id(), err) + } + + if _, err := waiter.AdminAccountNotFound(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for Security Hub Organization Admin Account (%s) to disable: %w", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_securityhub_organization_admin_account_test.go b/aws/resource_aws_securityhub_organization_admin_account_test.go new file mode 100644 index 00000000000..165d0e0cc14 --- /dev/null +++ b/aws/resource_aws_securityhub_organization_admin_account_test.go @@ -0,0 +1,136 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/securityhub/finder" +) + +func testAccAwsSecurityHubOrganizationAdminAccount_basic(t *testing.T) { + resourceName := "aws_securityhub_organization_admin_account.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccOrganizationsAccountPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsSecurityHubOrganizationAdminAccountDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSecurityHubOrganizationAdminAccountConfigSelf(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsSecurityHubOrganizationAdminAccountExists(resourceName), + testAccCheckResourceAttrAccountID(resourceName, "admin_account_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAwsSecurityHubOrganizationAdminAccount_disappears(t *testing.T) { + resourceName := "aws_securityhub_organization_admin_account.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccOrganizationsAccountPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsSecurityHubOrganizationAdminAccountDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSecurityHubOrganizationAdminAccountConfigSelf(), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsSecurityHubOrganizationAdminAccountExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsSecurityHubOrganizationAdminAccount(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckAwsSecurityHubOrganizationAdminAccountDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).securityhubconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_securityhub_organization_admin_account" { + continue + } + + adminAccount, err := finder.AdminAccount(conn, rs.Primary.ID) + + // Because of this resource's dependency, the Organizations organization + // will be deleted first, resulting in the following valid error + if tfawserr.ErrMessageContains(err, securityhub.ErrCodeAccessDeniedException, "account is not a member of an organization") { + continue + } + + if err != nil { + return err + } + + if adminAccount == nil { + continue + } + + return fmt.Errorf("expected Security Hub Organization Admin Account (%s) to be removed", rs.Primary.ID) + } + + return nil +} + +func testAccCheckAwsSecurityHubOrganizationAdminAccountExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).securityhubconn + + adminAccount, err := finder.AdminAccount(conn, rs.Primary.ID) + + if err != nil { + return err + } + + if adminAccount == nil { + return fmt.Errorf("Security Hub Organization Admin Account (%s) not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccSecurityHubOrganizationAdminAccountConfigSelf() string { + return ` +data "aws_caller_identity" "current" {} + +data "aws_partition" "current" {} + +resource "aws_organizations_organization" "test" { + aws_service_access_principals = ["securityhub.${data.aws_partition.current.dns_suffix}"] + feature_set = "ALL" +} + +resource "aws_securityhub_account" "test" {} + +resource "aws_securityhub_organization_admin_account" "test" { + depends_on = [aws_organizations_organization.test] + + admin_account_id = data.aws_caller_identity.current.account_id +} +` +} diff --git a/aws/resource_aws_securityhub_test.go b/aws/resource_aws_securityhub_test.go index cdc4edeabbf..d08c0b1e647 100644 --- a/aws/resource_aws_securityhub_test.go +++ b/aws/resource_aws_securityhub_test.go @@ -19,6 +19,10 @@ func TestAccAWSSecurityHub_serial(t *testing.T) { "Description": testAccAwsSecurityHubActionTarget_Description, "Name": testAccAwsSecurityHubActionTarget_Name, }, + "OrganizationAdminAccount": { + "basic": testAccAwsSecurityHubOrganizationAdminAccount_basic, + "disappears": testAccAwsSecurityHubOrganizationAdminAccount_disappears, + }, "ProductSubscription": { "basic": testAccAWSSecurityHubProductSubscription_basic, }, diff --git a/website/docs/r/securityhub_organization_admin_account.html.markdown b/website/docs/r/securityhub_organization_admin_account.html.markdown new file mode 100644 index 00000000000..54760557bf3 --- /dev/null +++ b/website/docs/r/securityhub_organization_admin_account.html.markdown @@ -0,0 +1,48 @@ +--- +subcategory: "Security Hub" +layout: "aws" +page_title: "AWS: aws_securityhub_organization_admin_account" +description: |- + Manages a Security Hub administrator account for an organization. +--- + +# Resource: aws_securityhub_organization_admin_account + +Manages a Security Hub administrator account for an organization. The AWS account utilizing this resource must be an Organizations primary account. More information about Organizations support in Security Hub can be found in the [Security Hub User Guide](https://docs.aws.amazon.com/securityhub/latest/userguide/designate-orgs-admin-account.html). + +## Example Usage + +```hcl +resource "aws_organizations_organization" "example" { + aws_service_access_principals = ["securityhub.amazonaws.com"] + feature_set = "ALL" +} + +resource "aws_securityhub_account" "example" {} + +resource "aws_securityhub_organization_admin_account" "example" { + depends_on = [aws_organizations_organization.example] + + admin_account_id = "123456789012" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `admin_account_id` - (Required) The AWS account identifier of the account to designate as the Security Hub administrator account. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - AWS account identifier. + +## Import + +Security Hub Organization Admin Accounts can be imported using the AWS account ID, e.g. + +``` +$ terraform import aws_securityhub_organization_admin_account.example 123456789012 +``` From 49da3cc1525075b733fd1c002b90f95872b2254b Mon Sep 17 00:00:00 2001 From: Angie Pinilla Date: Mon, 8 Feb 2021 00:56:27 -0500 Subject: [PATCH 2/2] Update CHANGELOG for #17501 --- .changelog/17501.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/17501.txt diff --git a/.changelog/17501.txt b/.changelog/17501.txt new file mode 100644 index 00000000000..a6b764a31b2 --- /dev/null +++ b/.changelog/17501.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_securityhub_organization_admin_account +``` \ No newline at end of file