diff --git a/.changelog/6084.txt b/.changelog/6084.txt new file mode 100644 index 00000000000..d9dc3388815 --- /dev/null +++ b/.changelog/6084.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +data-source/aws_iam_policy: Add support for lookup by `arn`, `name`, and/or `path_prefix` +``` diff --git a/aws/data_source_aws_iam_policy.go b/aws/data_source_aws_iam_policy.go index 938af05e46f..b4177d16c91 100644 --- a/aws/data_source_aws_iam_policy.go +++ b/aws/data_source_aws_iam_policy.go @@ -3,6 +3,7 @@ package aws import ( "fmt" "net/url" + "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/iam" @@ -10,6 +11,7 @@ import ( "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/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/finder" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) @@ -20,23 +22,29 @@ func dataSourceAwsIAMPolicy() *schema.Resource { Schema: map[string]*schema.Schema{ "arn": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validateArn, }, - "name": { + "description": { Type: schema.TypeString, Computed: true, }, - "policy": { + "name": { Type: schema.TypeString, + Optional: true, Computed: true, }, "path": { Type: schema.TypeString, Computed: true, }, - - "description": { + "path_prefix": { + Type: schema.TypeString, + Optional: true, + }, + "policy": { Type: schema.TypeString, Computed: true, }, @@ -54,16 +62,15 @@ func dataSourceAwsIAMPolicyRead(d *schema.ResourceData, meta interface{}) error ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig arn := d.Get("arn").(string) + name := d.Get("name").(string) + pathPrefix := d.Get("path_prefix").(string) - input := &iam.GetPolicyInput{ - PolicyArn: aws.String(arn), - } + var results []*iam.Policy // Handle IAM eventual consistency - var output *iam.GetPolicyOutput err := resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError { var err error - output, err = conn.GetPolicy(input) + results, err = finder.Policies(conn, arn, name, pathPrefix) if tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) { return resource.RetryableError(err) @@ -77,23 +84,27 @@ func dataSourceAwsIAMPolicyRead(d *schema.ResourceData, meta interface{}) error }) if tfresource.TimedOut(err) { - output, err = conn.GetPolicy(input) + results, err = finder.Policies(conn, arn, name, pathPrefix) } if err != nil { - return fmt.Errorf("error reading IAM policy %s: %w", arn, err) + return fmt.Errorf("error reading IAM policy (%s): %w", PolicySearchDetails(arn, name, pathPrefix), err) } - if output == nil || output.Policy == nil { - return fmt.Errorf("error reading IAM policy %s: empty output", arn) + if len(results) == 0 { + return fmt.Errorf("no IAM policy found matching criteria (%s); try different search", PolicySearchDetails(arn, name, pathPrefix)) } - policy := output.Policy + if len(results) > 1 { + return fmt.Errorf("multiple IAM policies found matching criteria (%s); try different search", PolicySearchDetails(arn, name, pathPrefix)) + } - d.SetId(aws.StringValue(policy.Arn)) + policy := results[0] + policyArn := aws.StringValue(policy.Arn) - d.Set("arn", policy.Arn) - d.Set("description", policy.Description) + d.SetId(policyArn) + + d.Set("arn", policyArn) d.Set("name", policy.PolicyName) d.Set("path", policy.Path) d.Set("policy_id", policy.PolicyId) @@ -102,10 +113,26 @@ func dataSourceAwsIAMPolicyRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("error setting tags: %w", err) } - // Retrieve policy + // Retrieve policy description + policyInput := &iam.GetPolicyInput{ + PolicyArn: policy.Arn, + } + policyOutput, err := conn.GetPolicy(policyInput) + + if err != nil { + return fmt.Errorf("error reading IAM policy (%s): %w", policyArn, err) + } + + if policyOutput == nil || policyOutput.Policy == nil { + return fmt.Errorf("error reading IAM policy (%s): empty output", policyArn) + } + + d.Set("description", policyOutput.Policy.Description) + + // Retrieve policy policyVersionInput := &iam.GetPolicyVersionInput{ - PolicyArn: aws.String(arn), + PolicyArn: policy.Arn, VersionId: policy.DefaultVersionId, } @@ -131,11 +158,11 @@ func dataSourceAwsIAMPolicyRead(d *schema.ResourceData, meta interface{}) error } if err != nil { - return fmt.Errorf("error reading IAM Policy (%s) version: %w", arn, err) + return fmt.Errorf("error reading IAM Policy (%s) version: %w", policyArn, err) } if policyVersionOutput == nil || policyVersionOutput.PolicyVersion == nil { - return fmt.Errorf("error reading IAM Policy (%s) version: empty output", arn) + return fmt.Errorf("error reading IAM Policy (%s) version: empty output", policyArn) } policyVersion := policyVersionOutput.PolicyVersion @@ -144,7 +171,7 @@ func dataSourceAwsIAMPolicyRead(d *schema.ResourceData, meta interface{}) error if policyVersion != nil { policyDocument, err = url.QueryUnescape(aws.StringValue(policyVersion.Document)) if err != nil { - return fmt.Errorf("error parsing IAM Policy (%s) document: %w", arn, err) + return fmt.Errorf("error parsing IAM Policy (%s) document: %w", policyArn, err) } } @@ -152,3 +179,19 @@ func dataSourceAwsIAMPolicyRead(d *schema.ResourceData, meta interface{}) error return nil } + +// PolicySearchDetails returns the configured search criteria as a printable string +func PolicySearchDetails(arn, name, pathPrefix string) string { + var policyDetails []string + if arn != "" { + policyDetails = append(policyDetails, fmt.Sprintf("ARN: %s", arn)) + } + if name != "" { + policyDetails = append(policyDetails, fmt.Sprintf("Name: %s", name)) + } + if pathPrefix != "" { + policyDetails = append(policyDetails, fmt.Sprintf("PathPrefix: %s", pathPrefix)) + } + + return strings.Join(policyDetails, ", ") +} diff --git a/aws/data_source_aws_iam_policy_test.go b/aws/data_source_aws_iam_policy_test.go index e3246cac11e..f14a6b469eb 100644 --- a/aws/data_source_aws_iam_policy_test.go +++ b/aws/data_source_aws_iam_policy_test.go @@ -2,6 +2,7 @@ package aws import ( "fmt" + "regexp" "testing" "github.com/aws/aws-sdk-go/service/iam" @@ -9,7 +10,75 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -func TestAccAWSDataSourceIAMPolicy_basic(t *testing.T) { +func TestPolicySearchDetails(t *testing.T) { + testCases := []struct { + Arn string + Name string + PathPrefix string + Expected string + }{ + { + Arn: "", + Name: "", + PathPrefix: "", + Expected: "", + }, + { + Arn: "arn:aws:iam::aws:policy/TestPolicy", //lintignore:AWSAT005 + Name: "", + PathPrefix: "", + Expected: "ARN: arn:aws:iam::aws:policy/TestPolicy", //lintignore:AWSAT005 + }, + { + Arn: "", + Name: "tf-acc-test-policy", + PathPrefix: "", + Expected: "Name: tf-acc-test-policy", + }, + { + Arn: "", + Name: "", + PathPrefix: "/test-prefix/", + Expected: "PathPrefix: /test-prefix/", + }, + { + Arn: "arn:aws:iam::aws:policy/TestPolicy", //lintignore:AWSAT005 + Name: "tf-acc-test-policy", + PathPrefix: "", + Expected: "ARN: arn:aws:iam::aws:policy/TestPolicy, Name: tf-acc-test-policy", //lintignore:AWSAT005 + }, + { + Arn: "arn:aws:iam::aws:policy/TestPolicy", //lintignore:AWSAT005 + Name: "", + PathPrefix: "/test-prefix/", + Expected: "ARN: arn:aws:iam::aws:policy/TestPolicy, PathPrefix: /test-prefix/", //lintignore:AWSAT005 + }, + { + Arn: "", + Name: "tf-acc-test-policy", + PathPrefix: "/test-prefix/", + Expected: "Name: tf-acc-test-policy, PathPrefix: /test-prefix/", + }, + { + Arn: "arn:aws:iam::aws:policy/TestPolicy", //lintignore:AWSAT005 + Name: "tf-acc-test-policy", + PathPrefix: "/test-prefix/", + Expected: "ARN: arn:aws:iam::aws:policy/TestPolicy, Name: tf-acc-test-policy, PathPrefix: /test-prefix/", //lintignore:AWSAT005 + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + got := PolicySearchDetails(testCase.Arn, testCase.Name, testCase.PathPrefix) + + if got != testCase.Expected { + t.Errorf("got %s, expected %s", got, testCase.Expected) + } + }) + } +} + +func TestAccAWSDataSourceIAMPolicy_Arn(t *testing.T) { datasourceName := "data.aws_iam_policy.test" resourceName := "aws_iam_policy.test" policyName := fmt.Sprintf("test-policy-%s", acctest.RandString(10)) @@ -20,7 +89,7 @@ func TestAccAWSDataSourceIAMPolicy_basic(t *testing.T) { Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccAwsDataSourceIamPolicyConfig(policyName), + Config: testAccAwsDataSourceIamPolicyConfig_Arn(policyName, "/"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), resource.TestCheckResourceAttrPair(datasourceName, "description", resourceName, "description"), @@ -33,14 +102,84 @@ func TestAccAWSDataSourceIAMPolicy_basic(t *testing.T) { }, }, }) +} + +func TestAccAWSDataSourceIAMPolicy_Name(t *testing.T) { + 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{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAwsDataSourceIamPolicyConfig_Name(policyName, "/"), + Check: resource.ComposeTestCheckFunc( + 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"), + ), + }, + }, + }) } -func testAccAwsDataSourceIamPolicyConfig(policyName string) string { +func TestAccAWSDataSourceIAMPolicy_NameAndPathPrefix(t *testing.T) { + datasourceName := "data.aws_iam_policy.test" + resourceName := "aws_iam_policy.test" + + policyName := fmt.Sprintf("test-policy-%s", acctest.RandString(10)) + policyPath := "/test-path/" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAwsDataSourceIamPolicyConfig_PathPrefix(policyName, policyPath), + Check: resource.ComposeTestCheckFunc( + 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"), + ), + }, + }, + }) +} + +func TestAccAWSDataSourceIAMPolicy_NonExistent(t *testing.T) { + policyName := fmt.Sprintf("test-policy-%s", acctest.RandString(10)) + policyPath := "/test-path/" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, iam.EndpointsID), + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAwsDataSourceIamPolicyConfig_NonExistent(policyName, policyPath), + ExpectError: regexp.MustCompile(`no IAM policy found matching criteria`), + }, + }, + }) +} + +func testAccAwsDataSourceIamPolicyBaseConfig(policyName, policyPath string) string { return fmt.Sprintf(` resource "aws_iam_policy" "test" { - name = "%s" - path = "/" + name = %q + path = %q description = "My test policy" policy = <