diff --git a/.changelog/39554.txt b/.changelog/39554.txt new file mode 100644 index 00000000000..c01cbca5072 --- /dev/null +++ b/.changelog/39554.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_iam_group_policies_exclusive +``` diff --git a/internal/service/iam/exports_test.go b/internal/service/iam/exports_test.go index d803c877629..907c0d04f9e 100644 --- a/internal/service/iam/exports_test.go +++ b/internal/service/iam/exports_test.go @@ -41,6 +41,7 @@ var ( FindAttachedUserPolicyByTwoPartKey = findAttachedUserPolicyByTwoPartKey FindEntitiesForPolicyByARN = findEntitiesForPolicyByARN FindGroupByName = findGroupByName + FindGroupPoliciesByName = findGroupPoliciesByName FindInstanceProfileByName = findInstanceProfileByName FindOpenIDConnectProviderByARN = findOpenIDConnectProviderByARN FindPolicyByARN = findPolicyByARN diff --git a/internal/service/iam/group_policies_exclusive.go b/internal/service/iam/group_policies_exclusive.go new file mode 100644 index 00000000000..0db5cd25250 --- /dev/null +++ b/internal/service/iam/group_policies_exclusive.go @@ -0,0 +1,217 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package iam + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + awstypes "github.com/aws/aws-sdk-go-v2/service/iam/types" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + intflex "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource("aws_iam_group_policies_exclusive", name="Group Policies Exclusive") +func newResourceGroupPoliciesExclusive(_ context.Context) (resource.ResourceWithConfigure, error) { + return &resourceGroupPoliciesExclusive{}, nil +} + +const ( + ResNameGroupPoliciesExclusive = "Group Policies Exclusive" +) + +type resourceGroupPoliciesExclusive struct { + framework.ResourceWithConfigure + framework.WithNoOpDelete +} + +func (r *resourceGroupPoliciesExclusive) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "aws_iam_group_policies_exclusive" +} + +func (r *resourceGroupPoliciesExclusive) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrGroupName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "policy_names": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + }, + }, + } +} + +func (r *resourceGroupPoliciesExclusive) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan resourceGroupPoliciesExclusiveData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var policyNames []string + resp.Diagnostics.Append(plan.PolicyNames.ElementsAs(ctx, &policyNames, false)...) + if resp.Diagnostics.HasError() { + return + } + + err := r.syncAttachments(ctx, plan.GroupName.ValueString(), policyNames) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.IAM, create.ErrActionCreating, ResNameGroupPoliciesExclusive, plan.GroupName.String(), err), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceGroupPoliciesExclusive) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().IAMClient(ctx) + + var state resourceGroupPoliciesExclusiveData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := findGroupPoliciesByName(ctx, conn, state.GroupName.ValueString()) + if tfresource.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.IAM, create.ErrActionReading, ResNameGroupPoliciesExclusive, state.GroupName.String(), err), + err.Error(), + ) + return + } + + state.PolicyNames = flex.FlattenFrameworkStringValueSetLegacy(ctx, out) + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceGroupPoliciesExclusive) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state resourceGroupPoliciesExclusiveData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if !plan.PolicyNames.Equal(state.PolicyNames) { + var policyNames []string + resp.Diagnostics.Append(plan.PolicyNames.ElementsAs(ctx, &policyNames, false)...) + if resp.Diagnostics.HasError() { + return + } + + err := r.syncAttachments(ctx, plan.GroupName.ValueString(), policyNames) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.IAM, create.ErrActionUpdating, ResNameGroupPoliciesExclusive, plan.GroupName.String(), err), + err.Error(), + ) + return + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +// syncAttachments handles keeping the configured inline policy attachments +// in sync with the remote resource. +// +// Inline policies defined on this resource but not attached to the group will +// be added. Policies attached to the group but not configured on this resource +// will be removed. +func (r *resourceGroupPoliciesExclusive) syncAttachments(ctx context.Context, groupName string, want []string) error { + conn := r.Meta().IAMClient(ctx) + + have, err := findGroupPoliciesByName(ctx, conn, groupName) + if err != nil { + return err + } + + create, remove, _ := intflex.DiffSlices(have, want, func(s1, s2 string) bool { return s1 == s2 }) + + for _, name := range create { + in := &iam.PutGroupPolicyInput{ + GroupName: aws.String(groupName), + PolicyName: aws.String(name), + } + + _, err := conn.PutGroupPolicy(ctx, in) + if err != nil { + return err + } + } + + for _, name := range remove { + in := &iam.DeleteGroupPolicyInput{ + GroupName: aws.String(groupName), + PolicyName: aws.String(name), + } + + _, err := conn.DeleteGroupPolicy(ctx, in) + if err != nil { + return err + } + } + + return nil +} + +func (r *resourceGroupPoliciesExclusive) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root(names.AttrGroupName), req, resp) +} + +func findGroupPoliciesByName(ctx context.Context, conn *iam.Client, groupName string) ([]string, error) { + in := &iam.ListGroupPoliciesInput{ + GroupName: aws.String(groupName), + } + + var policyNames []string + paginator := iam.NewListGroupPoliciesPaginator(conn, in) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + if errs.IsA[*awstypes.NoSuchEntityException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + return policyNames, err + } + + policyNames = append(policyNames, page.PolicyNames...) + } + + return policyNames, nil +} + +type resourceGroupPoliciesExclusiveData struct { + GroupName types.String `tfsdk:"group_name"` + PolicyNames types.Set `tfsdk:"policy_names"` +} diff --git a/internal/service/iam/group_policies_exclusive_test.go b/internal/service/iam/group_policies_exclusive_test.go new file mode 100644 index 00000000000..e2e322139ee --- /dev/null +++ b/internal/service/iam/group_policies_exclusive_test.go @@ -0,0 +1,438 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package iam_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/iam/types" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + tfiam "github.com/hashicorp/terraform-provider-aws/internal/service/iam" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccIAMGroupPoliciesExclusive_basic(t *testing.T) { + ctx := acctest.Context(t) + + var group types.Group + var groupPolicy string + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_iam_group_policies_exclusive.test" + groupResourceName := "aws_iam_group.test" + groupPolicyResourceName := "aws_iam_group_policy.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckGroupPoliciesExclusiveDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccGroupPoliciesExclusiveConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGroupExists(ctx, groupResourceName, &group), + testAccCheckGroupPolicyExists(ctx, groupPolicyResourceName, &groupPolicy), + testAccCheckGroupPoliciesExclusiveExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, names.AttrGroupName, groupResourceName, names.AttrName), + resource.TestCheckTypeSetElemAttrPair(resourceName, "policy_names.*", groupPolicyResourceName, names.AttrName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccGroupPoliciesExclusiveImportStateIdFunc(resourceName), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrGroupName, + }, + }, + }) +} + +func TestAccIAMGroupPoliciesExclusive_disappears_Group(t *testing.T) { + ctx := acctest.Context(t) + + var group types.Group + var groupPolicy string + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_iam_group_policies_exclusive.test" + groupResourceName := "aws_iam_group.test" + groupPolicyResourceName := "aws_iam_group_policy.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckGroupPoliciesExclusiveDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccGroupPoliciesExclusiveConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGroupExists(ctx, groupResourceName, &group), + testAccCheckGroupPolicyExists(ctx, groupPolicyResourceName, &groupPolicy), + testAccCheckGroupPoliciesExclusiveExists(ctx, resourceName), + // Inline policy must be deleted before the group can be + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfiam.ResourceGroupPolicy(), groupPolicyResourceName), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfiam.ResourceGroup(), groupResourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccIAMGroupPoliciesExclusive_multiple(t *testing.T) { + ctx := acctest.Context(t) + + var group types.Group + var groupPolicy string + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_iam_group_policies_exclusive.test" + groupResourceName := "aws_iam_group.test" + groupPolicyResourceName := "aws_iam_group_policy.test" + groupPolicyResourceName2 := "aws_iam_group_policy.test2" + groupPolicyResourceName3 := "aws_iam_group_policy.test3" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckGroupPoliciesExclusiveDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccGroupPoliciesExclusiveConfig_multiple(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGroupExists(ctx, groupResourceName, &group), + testAccCheckGroupPolicyExists(ctx, groupPolicyResourceName, &groupPolicy), + testAccCheckGroupPoliciesExclusiveExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, names.AttrGroupName, groupResourceName, names.AttrName), + resource.TestCheckTypeSetElemAttrPair(resourceName, "policy_names.*", groupPolicyResourceName, names.AttrName), + resource.TestCheckTypeSetElemAttrPair(resourceName, "policy_names.*", groupPolicyResourceName2, names.AttrName), + resource.TestCheckTypeSetElemAttrPair(resourceName, "policy_names.*", groupPolicyResourceName3, names.AttrName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccGroupPoliciesExclusiveImportStateIdFunc(resourceName), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrGroupName, + }, + { + Config: testAccGroupPoliciesExclusiveConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGroupExists(ctx, groupResourceName, &group), + testAccCheckGroupPolicyExists(ctx, groupPolicyResourceName, &groupPolicy), + testAccCheckGroupPoliciesExclusiveExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, names.AttrGroupName, groupResourceName, names.AttrName), + resource.TestCheckTypeSetElemAttrPair(resourceName, "policy_names.*", groupPolicyResourceName, names.AttrName), + ), + }, + }, + }) +} + +func TestAccIAMGroupPoliciesExclusive_empty(t *testing.T) { + ctx := acctest.Context(t) + + var group types.Group + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_iam_group_policies_exclusive.test" + groupResourceName := "aws_iam_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckGroupPoliciesExclusiveDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccGroupPoliciesExclusiveConfig_empty(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGroupExists(ctx, groupResourceName, &group), + testAccCheckGroupPoliciesExclusiveExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, names.AttrGroupName, groupResourceName, names.AttrName), + resource.TestCheckResourceAttr(resourceName, "policy_names.#", acctest.Ct0), + ), + // The empty `policy_names` argument in the exclusive lock will remove the + // inline policy defined in this configuration, so a diff is expected + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +// An inline policy removed out of band should be recreated +func TestAccIAMGroupPoliciesExclusive_outOfBandRemoval(t *testing.T) { + ctx := acctest.Context(t) + + var group types.Group + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_iam_group_policies_exclusive.test" + groupResourceName := "aws_iam_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckGroupDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccGroupPoliciesExclusiveConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGroupExists(ctx, groupResourceName, &group), + testAccCheckGroupPoliciesExclusiveExists(ctx, resourceName), + testAccCheckGroupPolicyRemoveInlinePolicy(ctx, &group, rName), + ), + ExpectNonEmptyPlan: true, + }, + { + Config: testAccGroupPoliciesExclusiveConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGroupExists(ctx, groupResourceName, &group), + testAccCheckGroupPoliciesExclusiveExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, names.AttrGroupName, groupResourceName, names.AttrName), + resource.TestCheckResourceAttr(resourceName, "policy_names.#", acctest.Ct1), + ), + }, + }, + }) +} + +// An inline policy added out of band should be removed +func TestAccIAMGroupPoliciesExclusive_outOfBandAddition(t *testing.T) { + ctx := acctest.Context(t) + + var group types.Group + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + policyName := rName + "-out-of-band" + resourceName := "aws_iam_group_policies_exclusive.test" + groupResourceName := "aws_iam_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckGroupDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccGroupPoliciesExclusiveConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGroupExists(ctx, groupResourceName, &group), + testAccCheckGroupPoliciesExclusiveExists(ctx, resourceName), + testAccCheckGroupPolicyAddInlinePolicy(ctx, &group, policyName), + ), + ExpectNonEmptyPlan: true, + }, + { + Config: testAccGroupPoliciesExclusiveConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGroupExists(ctx, groupResourceName, &group), + testAccCheckGroupPoliciesExclusiveExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, names.AttrGroupName, groupResourceName, names.AttrName), + resource.TestCheckResourceAttr(resourceName, "policy_names.#", acctest.Ct1), + ), + }, + }, + }) +} + +func testAccCheckGroupPoliciesExclusiveDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).IAMClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_iam_group_policies_exclusive" { + continue + } + + groupName := rs.Primary.Attributes[names.AttrGroupName] + _, err := tfiam.FindGroupPoliciesByName(ctx, conn, groupName) + if errs.IsA[*types.NoSuchEntityException](err) { + return nil + } + if err != nil { + return create.Error(names.IAM, create.ErrActionCheckingDestroyed, tfiam.ResNameGroupPoliciesExclusive, rs.Primary.ID, err) + } + + return create.Error(names.IAM, create.ErrActionCheckingDestroyed, tfiam.ResNameGroupPoliciesExclusive, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckGroupPoliciesExclusiveExists(ctx context.Context, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.IAM, create.ErrActionCheckingExistence, tfiam.ResNameGroupPoliciesExclusive, name, errors.New("not found")) + } + + groupName := rs.Primary.Attributes[names.AttrGroupName] + if groupName == "" { + return create.Error(names.IAM, create.ErrActionCheckingExistence, tfiam.ResNameGroupPoliciesExclusive, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).IAMClient(ctx) + out, err := tfiam.FindGroupPoliciesByName(ctx, conn, groupName) + if err != nil { + return create.Error(names.IAM, create.ErrActionCheckingExistence, tfiam.ResNameGroupPoliciesExclusive, groupName, err) + } + + policyCount := rs.Primary.Attributes["policy_names.#"] + if policyCount != fmt.Sprint(len(out)) { + return create.Error(names.IAM, create.ErrActionCheckingExistence, tfiam.ResNameGroupPoliciesExclusive, groupName, errors.New("unexpected policy_names count")) + } + + return nil + } +} + +func testAccGroupPoliciesExclusiveImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return rs.Primary.Attributes[names.AttrGroupName], nil + } +} + +func testAccCheckGroupPolicyAddInlinePolicy(ctx context.Context, group *types.Group, inlinePolicy string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).IAMClient(ctx) + + _, err := conn.PutGroupPolicy(ctx, &iam.PutGroupPolicyInput{ + PolicyDocument: aws.String(testAccGroupPolicyExtraInlineConfig()), + PolicyName: aws.String(inlinePolicy), + GroupName: group.GroupName, + }) + + return err + } +} + +func testAccCheckGroupPolicyRemoveInlinePolicy(ctx context.Context, group *types.Group, inlinePolicy string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).IAMClient(ctx) + + _, err := conn.DeleteGroupPolicy(ctx, &iam.DeleteGroupPolicyInput{ + PolicyName: aws.String(inlinePolicy), + GroupName: group.GroupName, + }) + + return err + } +} + +func testAccGroupPolicyExtraInlineConfig() string { + return `{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "ec2:Describe*" + ], + "Effect": "Allow", + "Resource": "*" + } + ] +}` +} + +func testAccGroupPoliciesExclusiveConfigBase(rName string) string { + return fmt.Sprintf(` +data "aws_iam_policy_document" "inline" { + statement { + actions = ["s3:ListBucket"] + resources = ["*"] + } +} + +resource "aws_iam_group" "test" { + name = %[1]q +} + +resource "aws_iam_group_policy" "test" { + name = %[1]q + group = aws_iam_group.test.name + policy = data.aws_iam_policy_document.inline.json +} +`, rName) +} + +func testAccGroupPoliciesExclusiveConfig_basic(rName string) string { + return acctest.ConfigCompose( + testAccGroupPoliciesExclusiveConfigBase(rName), + ` +resource "aws_iam_group_policies_exclusive" "test" { + group_name = aws_iam_group.test.name + policy_names = [aws_iam_group_policy.test.name] +} +`) +} + +func testAccGroupPoliciesExclusiveConfig_multiple(rName string) string { + return acctest.ConfigCompose( + testAccGroupPoliciesExclusiveConfigBase(rName), + fmt.Sprintf(` +resource "aws_iam_group_policy" "test2" { + name = "%[1]s-2" + group = aws_iam_group.test.name + policy = data.aws_iam_policy_document.inline.json +} + +resource "aws_iam_group_policy" "test3" { + name = "%[1]s-3" + group = aws_iam_group.test.name + policy = data.aws_iam_policy_document.inline.json +} + +resource "aws_iam_group_policies_exclusive" "test" { + group_name = aws_iam_group.test.name + policy_names = [ + aws_iam_group_policy.test.name, + aws_iam_group_policy.test2.name, + aws_iam_group_policy.test3.name, + ] +} +`, rName)) +} + +func testAccGroupPoliciesExclusiveConfig_empty(rName string) string { + return acctest.ConfigCompose( + testAccGroupPoliciesExclusiveConfigBase(rName), + ` +resource "aws_iam_group_policies_exclusive" "test" { + # Wait until the inline policy is created, then provision + # the exclusive lock which will remove it. This creates a diff on + # on the next plan (to re-create aws_iam_group_policy.test) + # which the test can check for. + depends_on = [aws_iam_group_policy.test] + + group_name = aws_iam_group.test.name + policy_names = [] +} +`) +} diff --git a/internal/service/iam/service_package_gen.go b/internal/service/iam/service_package_gen.go index b1bf861b7b8..dd774262e85 100644 --- a/internal/service/iam/service_package_gen.go +++ b/internal/service/iam/service_package_gen.go @@ -20,6 +20,10 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.Serv func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.ServicePackageFrameworkResource { return []*types.ServicePackageFrameworkResource{ + { + Factory: newResourceGroupPoliciesExclusive, + Name: "Group Policies Exclusive", + }, { Factory: newResourceRolePoliciesExclusive, Name: "Role Policies Exclusive", diff --git a/website/docs/r/iam_group_policies_exclusive.html.markdown b/website/docs/r/iam_group_policies_exclusive.html.markdown new file mode 100644 index 00000000000..a853101423a --- /dev/null +++ b/website/docs/r/iam_group_policies_exclusive.html.markdown @@ -0,0 +1,64 @@ +--- +subcategory: "IAM (Identity & Access Management)" +layout: "aws" +page_title: "AWS: aws_iam_group_policies_exclusive" +description: |- + Terraform resource for maintaining exclusive management of inline policies assigned to an AWS IAM (Identity & Access Management) group. +--- +# Resource: aws_iam_group_policies_exclusive + +Terraform resource for maintaining exclusive management of inline policies assigned to an AWS IAM (Identity & Access Management) group. + +!> This resource takes exclusive ownership over inline policies assigned to a group. This includes removal of inline policies which are not explicitly configured. To prevent persistent drift, ensure any `aws_iam_group_policy` resources managed alongside this resource are included in the `policy_names` argument. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_iam_group_policies_exclusive" "example" { + group_name = aws_iam_group.example.name + policy_names = [aws_iam_group_policy.example.name] +} +``` + +### Disallow Inline Policies + +To automatically remove any configured inline policies, set the `policy_names` argument to an empty list. + +~> This will not __prevent__ inline policies from being assigned to a group via Terraform (or any other interface). This resource enables bringing inline policy assignments into a configured state, however, this reconciliation happens only when `apply` is proactively run. + +```terraform +resource "aws_iam_group_policies_exclusive" "example" { + group_name = aws_iam_group.example.name + policy_names = [] +} +``` + +## Argument Reference + +The following arguments are required: + +* `group_name` - (Required) IAM group name. +* `policy_names` - (Required) A list of inline policy names to be assigned to the group. Policies attached to this group but not configured in this argument will be removed. + +## Attribute Reference + +This resource exports no additional attributes. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to exclusively manage inline policy assignments using the `group_name`. For example: + +```terraform +import { + to = aws_iam_group_policies_exclusive.example + id = "MyGroup" +} +``` + +Using `terraform import`, import exclusive management of inline policy assignments using the `group_name`. For example: + +```console +% terraform import aws_iam_group_policies_exclusive.example MyGroup +```