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 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 +```