diff --git a/.changelog/18276.txt b/.changelog/18276.txt new file mode 100644 index 00000000000..b94e07e663c --- /dev/null +++ b/.changelog/18276.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_iam_policy: Add tagging support +``` + +```release-note:enhancement +resource/aws_iam_policy: Add `policy_id` attribute +``` + +```release-note:enhancement +data-source/aws_iam_policy: Add `policy_id` and `tags` attributes +``` diff --git a/aws/data_source_aws_iam_policy.go b/aws/data_source_aws_iam_policy.go index 8e2d3fdd161..3a2233aac96 100644 --- a/aws/data_source_aws_iam_policy.go +++ b/aws/data_source_aws_iam_policy.go @@ -30,6 +30,11 @@ func dataSourceAwsIAMPolicy() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "policy_id": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchemaComputed(), }, } } diff --git a/aws/data_source_aws_iam_policy_test.go b/aws/data_source_aws_iam_policy_test.go index 42bf6de1142..e3246cac11e 100644 --- a/aws/data_source_aws_iam_policy_test.go +++ b/aws/data_source_aws_iam_policy_test.go @@ -10,7 +10,8 @@ import ( ) func TestAccAWSDataSourceIAMPolicy_basic(t *testing.T) { - resourceName := "data.aws_iam_policy.test" + datasourceName := "data.aws_iam_policy.test" + resourceName := "aws_iam_policy.test" policyName := fmt.Sprintf("test-policy-%s", acctest.RandString(10)) resource.ParallelTest(t, resource.TestCase{ @@ -21,11 +22,13 @@ func TestAccAWSDataSourceIAMPolicy_basic(t *testing.T) { { Config: testAccAwsDataSourceIamPolicyConfig(policyName), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "name", policyName), - resource.TestCheckResourceAttr(resourceName, "description", "My test policy"), - resource.TestCheckResourceAttr(resourceName, "path", "/"), - resource.TestCheckResourceAttrSet(resourceName, "policy"), - resource.TestCheckResourceAttrPair(resourceName, "arn", "aws_iam_policy.test_policy", "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(datasourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(datasourceName, "path", resourceName, "path"), + resource.TestCheckResourceAttrPair(datasourceName, "policy", resourceName, "policy"), + resource.TestCheckResourceAttrPair(datasourceName, "policy_id", resourceName, "policy_id"), + resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "tags", resourceName, "tags"), ), }, }, @@ -35,7 +38,7 @@ func TestAccAWSDataSourceIAMPolicy_basic(t *testing.T) { func testAccAwsDataSourceIamPolicyConfig(policyName string) string { return fmt.Sprintf(` -resource "aws_iam_policy" "test_policy" { +resource "aws_iam_policy" "test" { name = "%s" path = "/" description = "My test policy" @@ -57,7 +60,7 @@ EOF } data "aws_iam_policy" "test" { - arn = aws_iam_policy.test_policy.arn + arn = aws_iam_policy.test.arn } `, policyName) } diff --git a/aws/internal/keyvaluetags/iam_tags.go b/aws/internal/keyvaluetags/iam_tags.go index a7af6a8c6e5..f67281a2f33 100644 --- a/aws/internal/keyvaluetags/iam_tags.go +++ b/aws/internal/keyvaluetags/iam_tags.go @@ -151,6 +151,41 @@ func IamOpenIDConnectProviderUpdateTags(conn *iam.IAM, identifier string, oldTag return nil } +// IamPolicyUpdateTags updates IAM Policy tags. +// The identifier is the Policy ARN. +func IamPolicyUpdateTags(conn *iam.IAM, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := New(oldTagsMap) + newTags := New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &iam.UntagPolicyInput{ + PolicyArn: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.Keys()), + } + + _, err := conn.UntagPolicy(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &iam.TagPolicyInput{ + PolicyArn: aws.String(identifier), + Tags: updatedTags.IgnoreAws().IamTags(), + } + + _, err := conn.TagPolicy(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + // IamSAMLProviderUpdateTags updates IAM SAML Provider tags. // The identifier is the SAML Provider ARN. func IamSAMLProviderUpdateTags(conn *iam.IAM, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { diff --git a/aws/resource_aws_iam_policy.go b/aws/resource_aws_iam_policy.go index 6feee9a8cbe..ef7c52a25ff 100644 --- a/aws/resource_aws_iam_policy.go +++ b/aws/resource_aws_iam_policy.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" ) @@ -67,12 +68,17 @@ func resourceAwsIamPolicy() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "policy_id": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchema(), }, } } func resourceAwsIamPolicyCreate(d *schema.ResourceData, meta interface{}) error { - iamconn := meta.(*AWSClient).iamconn + conn := meta.(*AWSClient).iamconn var name string if v, ok := d.GetOk("name"); ok { @@ -88,11 +94,12 @@ func resourceAwsIamPolicyCreate(d *schema.ResourceData, meta interface{}) error Path: aws.String(d.Get("path").(string)), PolicyDocument: aws.String(d.Get("policy").(string)), PolicyName: aws.String(name), + Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().IamTags(), } - response, err := iamconn.CreatePolicy(request) + response, err := conn.CreatePolicy(request) if err != nil { - return fmt.Errorf("Error creating IAM policy %s: %s", name, err) + return fmt.Errorf("Error creating IAM policy %s: %w", name, err) } d.SetId(aws.StringValue(response.Policy.Arn)) @@ -101,7 +108,8 @@ func resourceAwsIamPolicyCreate(d *schema.ResourceData, meta interface{}) error } func resourceAwsIamPolicyRead(d *schema.ResourceData, meta interface{}) error { - iamconn := meta.(*AWSClient).iamconn + conn := meta.(*AWSClient).iamconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig getPolicyRequest := &iam.GetPolicyInput{ PolicyArn: aws.String(d.Id()), @@ -112,7 +120,7 @@ func resourceAwsIamPolicyRead(d *schema.ResourceData, meta interface{}) error { var getPolicyResponse *iam.GetPolicyOutput err := resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError { var err error - getPolicyResponse, err = iamconn.GetPolicy(getPolicyRequest) + getPolicyResponse, err = conn.GetPolicy(getPolicyRequest) if d.IsNewResource() && isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") { return resource.RetryableError(err) @@ -125,7 +133,7 @@ func resourceAwsIamPolicyRead(d *schema.ResourceData, meta interface{}) error { return nil }) if isResourceTimeoutError(err) { - getPolicyResponse, err = iamconn.GetPolicy(getPolicyRequest) + getPolicyResponse, err = conn.GetPolicy(getPolicyRequest) } if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") { log.Printf("[WARN] IAM Policy (%s) not found, removing from state", d.Id()) @@ -134,7 +142,7 @@ func resourceAwsIamPolicyRead(d *schema.ResourceData, meta interface{}) error { } if err != nil { - return fmt.Errorf("Error reading IAM policy %s: %s", d.Id(), err) + return fmt.Errorf("Error reading IAM policy %s: %w", d.Id(), err) } if getPolicyResponse == nil || getPolicyResponse.Policy == nil { @@ -143,16 +151,22 @@ func resourceAwsIamPolicyRead(d *schema.ResourceData, meta interface{}) error { return nil } - d.Set("arn", getPolicyResponse.Policy.Arn) - d.Set("description", getPolicyResponse.Policy.Description) - d.Set("name", getPolicyResponse.Policy.PolicyName) - d.Set("path", getPolicyResponse.Policy.Path) + policyRes := getPolicyResponse.Policy + d.Set("arn", policyRes.Arn) + d.Set("description", policyRes.Description) + d.Set("name", policyRes.PolicyName) + d.Set("path", policyRes.Path) + d.Set("policy_id", policyRes.PolicyId) + + if err := d.Set("tags", keyvaluetags.IamKeyValueTags(policyRes.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } // Retrieve policy getPolicyVersionRequest := &iam.GetPolicyVersionInput{ PolicyArn: aws.String(d.Id()), - VersionId: getPolicyResponse.Policy.DefaultVersionId, + VersionId: policyRes.DefaultVersionId, } log.Printf("[DEBUG] Getting IAM Policy Version: %s", getPolicyVersionRequest) @@ -160,7 +174,7 @@ func resourceAwsIamPolicyRead(d *schema.ResourceData, meta interface{}) error { var getPolicyVersionResponse *iam.GetPolicyVersionOutput err = resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError { var err error - getPolicyVersionResponse, err = iamconn.GetPolicyVersion(getPolicyVersionRequest) + getPolicyVersionResponse, err = conn.GetPolicyVersion(getPolicyVersionRequest) if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") { return resource.RetryableError(err) @@ -173,7 +187,7 @@ func resourceAwsIamPolicyRead(d *schema.ResourceData, meta interface{}) error { return nil }) if isResourceTimeoutError(err) { - getPolicyVersionResponse, err = iamconn.GetPolicyVersion(getPolicyVersionRequest) + getPolicyVersionResponse, err = conn.GetPolicyVersion(getPolicyVersionRequest) } if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") { log.Printf("[WARN] IAM Policy (%s) not found, removing from state", d.Id()) @@ -182,7 +196,7 @@ func resourceAwsIamPolicyRead(d *schema.ResourceData, meta interface{}) error { } if err != nil { - return fmt.Errorf("Error reading IAM policy version %s: %s", d.Id(), err) + return fmt.Errorf("Error reading IAM policy version %s: %w", d.Id(), err) } policy := "" @@ -190,7 +204,7 @@ func resourceAwsIamPolicyRead(d *schema.ResourceData, meta interface{}) error { var err error policy, err = url.QueryUnescape(aws.StringValue(getPolicyVersionResponse.PolicyVersion.Document)) if err != nil { - return fmt.Errorf("error parsing policy: %s", err) + return fmt.Errorf("error parsing policy: %w", err) } } @@ -200,29 +214,40 @@ func resourceAwsIamPolicyRead(d *schema.ResourceData, meta interface{}) error { } func resourceAwsIamPolicyUpdate(d *schema.ResourceData, meta interface{}) error { - iamconn := meta.(*AWSClient).iamconn + conn := meta.(*AWSClient).iamconn - if err := iamPolicyPruneVersions(d.Id(), iamconn); err != nil { - return err - } + if d.HasChangeExcept("tags") { - request := &iam.CreatePolicyVersionInput{ - PolicyArn: aws.String(d.Id()), - PolicyDocument: aws.String(d.Get("policy").(string)), - SetAsDefault: aws.Bool(true), + if err := iamPolicyPruneVersions(d.Id(), conn); err != nil { + return err + } + + request := &iam.CreatePolicyVersionInput{ + PolicyArn: aws.String(d.Id()), + PolicyDocument: aws.String(d.Get("policy").(string)), + SetAsDefault: aws.Bool(true), + } + + if _, err := conn.CreatePolicyVersion(request); err != nil { + return fmt.Errorf("Error updating IAM policy %s: %w", d.Id(), err) + } } - if _, err := iamconn.CreatePolicyVersion(request); err != nil { - return fmt.Errorf("Error updating IAM policy %s: %s", d.Id(), err) + if d.HasChange("tags") { + o, n := d.GetChange("tags") + + if err := keyvaluetags.IamPolicyUpdateTags(conn, d.Id(), o, n); err != nil { + return fmt.Errorf("error updating tags for IAM Policy (%s): %w", d.Id(), err) + } } return resourceAwsIamPolicyRead(d, meta) } func resourceAwsIamPolicyDelete(d *schema.ResourceData, meta interface{}) error { - iamconn := meta.(*AWSClient).iamconn + conn := meta.(*AWSClient).iamconn - if err := iamPolicyDeleteNondefaultVersions(d.Id(), iamconn); err != nil { + if err := iamPolicyDeleteNondefaultVersions(d.Id(), conn); err != nil { return err } @@ -230,11 +255,11 @@ func resourceAwsIamPolicyDelete(d *schema.ResourceData, meta interface{}) error PolicyArn: aws.String(d.Id()), } - if _, err := iamconn.DeletePolicy(request); err != nil { + if _, err := conn.DeletePolicy(request); err != nil { if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") { return nil } - return fmt.Errorf("Error deleting IAM policy %s: %s", d.Id(), err) + return fmt.Errorf("Error deleting IAM policy %s: %w", d.Id(), err) } return nil @@ -247,8 +272,8 @@ func resourceAwsIamPolicyDelete(d *schema.ResourceData, meta interface{}) error // // The default version is never deleted. -func iamPolicyPruneVersions(arn string, iamconn *iam.IAM) error { - versions, err := iamPolicyListVersions(arn, iamconn) +func iamPolicyPruneVersions(arn string, conn *iam.IAM) error { + versions, err := iamPolicyListVersions(arn, conn) if err != nil { return err } @@ -268,12 +293,12 @@ func iamPolicyPruneVersions(arn string, iamconn *iam.IAM) error { } } - err1 := iamPolicyDeleteVersion(arn, *oldestVersion.VersionId, iamconn) + err1 := iamPolicyDeleteVersion(arn, aws.StringValue(oldestVersion.VersionId), conn) return err1 } -func iamPolicyDeleteNondefaultVersions(arn string, iamconn *iam.IAM) error { - versions, err := iamPolicyListVersions(arn, iamconn) +func iamPolicyDeleteNondefaultVersions(arn string, conn *iam.IAM) error { + versions, err := iamPolicyListVersions(arn, conn) if err != nil { return err } @@ -282,7 +307,7 @@ func iamPolicyDeleteNondefaultVersions(arn string, iamconn *iam.IAM) error { if *version.IsDefaultVersion { continue } - if err := iamPolicyDeleteVersion(arn, *version.VersionId, iamconn); err != nil { + if err := iamPolicyDeleteVersion(arn, aws.StringValue(version.VersionId), conn); err != nil { return err } } @@ -290,27 +315,27 @@ func iamPolicyDeleteNondefaultVersions(arn string, iamconn *iam.IAM) error { return nil } -func iamPolicyDeleteVersion(arn, versionID string, iamconn *iam.IAM) error { +func iamPolicyDeleteVersion(arn, versionID string, conn *iam.IAM) error { request := &iam.DeletePolicyVersionInput{ PolicyArn: aws.String(arn), VersionId: aws.String(versionID), } - _, err := iamconn.DeletePolicyVersion(request) + _, err := conn.DeletePolicyVersion(request) if err != nil { - return fmt.Errorf("Error deleting version %s from IAM policy %s: %s", versionID, arn, err) + return fmt.Errorf("Error deleting version %s from IAM policy %s: %w", versionID, arn, err) } return nil } -func iamPolicyListVersions(arn string, iamconn *iam.IAM) ([]*iam.PolicyVersion, error) { +func iamPolicyListVersions(arn string, conn *iam.IAM) ([]*iam.PolicyVersion, error) { request := &iam.ListPolicyVersionsInput{ PolicyArn: aws.String(arn), } - response, err := iamconn.ListPolicyVersions(request) + response, err := conn.ListPolicyVersions(request) if err != nil { - return nil, fmt.Errorf("Error listing versions for IAM policy %s: %s", arn, err) + return nil, fmt.Errorf("Error listing versions for IAM policy %s: %w", arn, err) } return response.Versions, nil } diff --git a/aws/resource_aws_iam_policy_test.go b/aws/resource_aws_iam_policy_test.go index 8335b961744..584c617ee84 100644 --- a/aws/resource_aws_iam_policy_test.go +++ b/aws/resource_aws_iam_policy_test.go @@ -126,6 +126,8 @@ func TestAccAWSIAMPolicy_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttr(resourceName, "path", "/"), resource.TestCheckResourceAttr(resourceName, "policy", expectedPolicyText), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttrSet(resourceName, "policy_id"), ), }, { @@ -164,6 +166,51 @@ func TestAccAWSIAMPolicy_description(t *testing.T) { }) } +func TestAccAWSIAMPolicy_tags(t *testing.T) { + var out iam.GetPolicyOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_iam_policy.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSIAMPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSIAMPolicyConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSIAMPolicyExists(resourceName, &out), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSIAMPolicyConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSIAMPolicyExists(resourceName, &out), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAWSIAMPolicyConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSIAMPolicyExists(resourceName, &out), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + func TestAccAWSIAMPolicy_disappears(t *testing.T) { var out iam.GetPolicyOutput rName := acctest.RandomWithPrefix("tf-acc-test") @@ -179,7 +226,7 @@ func TestAccAWSIAMPolicy_disappears(t *testing.T) { Config: testAccAWSIAMPolicyConfigName(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSIAMPolicyExists(resourceName, &out), - testAccCheckAWSIAMPolicyDisappears(&out), + testAccCheckResourceDisappears(testAccProvider, resourceAwsIamPolicy(), resourceName), ), ExpectNonEmptyPlan: true, }, @@ -334,19 +381,6 @@ func testAccCheckAWSIAMPolicyDestroy(s *terraform.State) error { return nil } -func testAccCheckAWSIAMPolicyDisappears(out *iam.GetPolicyOutput) resource.TestCheckFunc { - return func(s *terraform.State) error { - iamconn := testAccProvider.Meta().(*AWSClient).iamconn - - params := &iam.DeletePolicyInput{ - PolicyArn: out.Policy.Arn, - } - - _, err := iamconn.DeletePolicy(params) - return err - } -} - func testAccAWSIAMPolicyConfigDescription(rName, description string) string { return fmt.Sprintf(` resource "aws_iam_policy" "test" { @@ -449,3 +483,58 @@ resource "aws_iam_policy" "test" { } `, rName, policy) } + +func testAccAWSIAMPolicyConfigTags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_iam_policy" "test" { + name = %q + + policy = <