From 00310384b26cc1f9acbddd3c3c93d5ec61fd2ea6 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 24 Jun 2022 12:18:23 -0700 Subject: [PATCH 1/6] service/iam: iam_principal_policy_simulation data source This data source wraps the IAM policy simulation API. This was previously a data source with little utility in Terraform, but with the introduction of preconditions and postconditions in Terraform v1.2.3 it can be potentially useful as a way for a configuration to either pre-verify that it seems to be running with credentials that confer sufficient access or to self-check a policy it declares itself to get earlier warning if the policy seems insufficient for its intended purpose. Unfortunately the IAM policy simulator is pretty low-level and requires the caller to figure out all of the relevant details of how a real AWS service would make requests to IAM at runtime in order to construct a fully-realistic simulation, but thankfully in practice it seems like authors could make do with relatively-simple "naive" simulations unless they know they are using more complex IAM policy features, such as custom conditions or interpolations. --- .changelog/25569.txt | 3 + internal/provider/provider.go | 31 +- ...principal_policy_simulation_data_source.go | 335 ++++++++++++++++++ ...ipal_policy_simulation_data_source_test.go | 209 +++++++++++ ..._principal_policy_simulation.html.markdown | 220 ++++++++++++ 5 files changed, 783 insertions(+), 15 deletions(-) create mode 100644 .changelog/25569.txt create mode 100644 internal/service/iam/principal_policy_simulation_data_source.go create mode 100644 internal/service/iam/principal_policy_simulation_data_source_test.go create mode 100644 website/docs/d/iam_principal_policy_simulation.html.markdown diff --git a/.changelog/25569.txt b/.changelog/25569.txt new file mode 100644 index 00000000000..7df0ee2e471 --- /dev/null +++ b/.changelog/25569.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_iam_principal_policy_simulation: Run simulated requests against a user's, group's, or role's IAM policies. +``` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index cccc08412c9..12fc9cc4562 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -665,21 +665,22 @@ func Provider() *schema.Provider { "aws_guardduty_detector": guardduty.DataSourceDetector(), - "aws_iam_account_alias": iam.DataSourceAccountAlias(), - "aws_iam_group": iam.DataSourceGroup(), - "aws_iam_instance_profile": iam.DataSourceInstanceProfile(), - "aws_iam_instance_profiles": iam.DataSourceInstanceProfiles(), - "aws_iam_openid_connect_provider": iam.DataSourceOpenIDConnectProvider(), - "aws_iam_policy": iam.DataSourcePolicy(), - "aws_iam_policy_document": iam.DataSourcePolicyDocument(), - "aws_iam_role": iam.DataSourceRole(), - "aws_iam_roles": iam.DataSourceRoles(), - "aws_iam_saml_provider": iam.DataSourceSAMLProvider(), - "aws_iam_server_certificate": iam.DataSourceServerCertificate(), - "aws_iam_session_context": iam.DataSourceSessionContext(), - "aws_iam_user": iam.DataSourceUser(), - "aws_iam_user_ssh_key": iam.DataSourceUserSSHKey(), - "aws_iam_users": iam.DataSourceUsers(), + "aws_iam_account_alias": iam.DataSourceAccountAlias(), + "aws_iam_group": iam.DataSourceGroup(), + "aws_iam_instance_profile": iam.DataSourceInstanceProfile(), + "aws_iam_instance_profiles": iam.DataSourceInstanceProfiles(), + "aws_iam_openid_connect_provider": iam.DataSourceOpenIDConnectProvider(), + "aws_iam_policy": iam.DataSourcePolicy(), + "aws_iam_policy_document": iam.DataSourcePolicyDocument(), + "aws_iam_principal_policy_simulation": iam.DataSourcePrincipalPolicySimulation(), + "aws_iam_role": iam.DataSourceRole(), + "aws_iam_roles": iam.DataSourceRoles(), + "aws_iam_saml_provider": iam.DataSourceSAMLProvider(), + "aws_iam_server_certificate": iam.DataSourceServerCertificate(), + "aws_iam_session_context": iam.DataSourceSessionContext(), + "aws_iam_user": iam.DataSourceUser(), + "aws_iam_user_ssh_key": iam.DataSourceUserSSHKey(), + "aws_iam_users": iam.DataSourceUsers(), "aws_identitystore_group": identitystore.DataSourceGroup(), "aws_identitystore_user": identitystore.DataSourceUser(), diff --git a/internal/service/iam/principal_policy_simulation_data_source.go b/internal/service/iam/principal_policy_simulation_data_source.go new file mode 100644 index 00000000000..fae1d4c9722 --- /dev/null +++ b/internal/service/iam/principal_policy_simulation_data_source.go @@ -0,0 +1,335 @@ +package iam + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +func DataSourcePrincipalPolicySimulation() *schema.Resource { + return &schema.Resource{ + Read: dataSourcePrincipalPolicySimulationRead, + + Schema: map[string]*schema.Schema{ + // Arguments + "action_names": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: `One or more names of actions, like "iam:CreateUser", that should be included in the simulation.`, + }, + "caller_arn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: verify.ValidARN, + Description: `ARN of a user to use as the caller of the simulated requests. If not specified, defaults to the principal specified in policy_source_arn, if it is a user ARN.`, + }, + "context": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + Description: `The key name of the context entry, such as "aws:CurrentTime".`, + }, + "type": { + Type: schema.TypeString, + Required: true, + Description: `The type that the simulator should use to interpret the strings given in argument "values".`, + }, + "values": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: `One or more values to assign to the context key, given as a string in a syntax appropriate for the selected value type.`, + }, + }, + }, + Description: `Each block specifies one item of additional context entry to include in the simulated requests. These are the additional properties used in the 'Condition' element of an IAM policy, and in dynamic value interpolations.`, + }, + "permissions_boundary_policies_json": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringIsJSON, + }, + Description: `Additional permission boundary policies to use in the simulation.`, + }, + "additional_policies_json": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringIsJSON, + }, + Description: `Additional principal-based policies to use in the simulation.`, + }, + "policy_source_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: verify.ValidARN, + Description: `ARN of the principal (e.g. user, role) whose existing configured access policies will be used as the basis for the simulation. If you specify a role ARN here, you can also set caller_arn to simulate a particular user acting with the given role.`, + }, + "resource_arns": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: verify.ValidARN, + }, + Description: `ARNs of specific resources to use as the targets of the specified actions during simulation. If not specified, the simulator assumes "*" which represents general access across all resources.`, + }, + "resource_handling_option": { + Type: schema.TypeString, + Optional: true, + Description: `Specifies the type of simulation to run. Some API operations need a particular resource handling option in order to produce a correct reesult.`, + }, + "resource_owner_account_id": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: verify.ValidAccountID, + Description: `An AWS account ID to use as the simulated owner for any resource whose ARN does not include a specific owner account ID. Defaults to the account given as part of caller_arn.`, + }, + "resource_policy_json": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringIsJSON, + Description: `A resource policy to associate with all of the target resources for simulation purposes. The policy simulator does not automatically retrieve resource-level policies, so if a resource policy is crucial to your test then you must specify here the same policy document associated with your target resource(s).`, + }, + + // Result Attributes + "all_allowed": { + Type: schema.TypeBool, + Computed: true, + Description: `A summary of the results attribute which is true if all of the results have decision "allowed", and false otherwise.`, + }, + "results": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "action_name": { + Type: schema.TypeString, + Computed: true, + Description: `The name of the action whose simulation this result is describing.`, + }, + "decision": { + Type: schema.TypeString, + Computed: true, + Description: `The exact decision keyword returned by the policy simulator: "allowed", "explicitDeny", or "implicitDeny".`, + }, + "allowed": { + Type: schema.TypeBool, + Computed: true, + Description: `A summary of attribute "decision" which is true only if the decision is "allowed".`, + }, + "decision_details": { + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: `A mapping of various additional details that are relevant to the decision, exactly as returned by the policy simulator.`, + }, + "resource_arn": { + Type: schema.TypeString, + Computed: true, + Description: `ARN of the resource that the action was tested against.`, + }, + "matched_statements": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "source_policy_id": { + Type: schema.TypeString, + Computed: true, + Description: `Identifier of one of the policies used as input to the simulation.`, + }, + "source_policy_type": { + Type: schema.TypeString, + Computed: true, + Description: `The type of the policy identified in source_policy_id.`, + }, + // NOTE: start position and end position + // omitted right now because they would + // ideally be singleton objects with + // column/line attributes, but this SDK + // can't support that. Maybe we later adopt + // the new framework and add support for + // those. + }, + }, + Description: `Detail about which specific policies contributed to this result.`, + }, + "missing_context_keys": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: `Set of context entry keys that were needed for one or more of the relevant policies but not included in the request. You must specify suitable values for all context keys used in all of the relevant policies in order to obtain a correct simulation result.`, + }, + // NOTE: organizations decision detail, permissions + // boundary decision detail, and resource-specific + // results omitted for now because it isn't clear + // that they will be useful and they would make the + // results of this data source considerably larger + // and more complicated. + }, + }, + }, + "id": { + Type: schema.TypeString, + Computed: true, + Description: `Do not use`, + }, + }, + } +} + +func dataSourcePrincipalPolicySimulationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).IAMConn + + setAsAWSStringSlice := func(raw interface{}) []*string { + listOfInterface := raw.(*schema.Set).List() + if len(listOfInterface) == 0 { + return nil + } + ret := make([]*string, len(listOfInterface)) + for i, iface := range listOfInterface { + str := iface.(string) + ret[i] = &str + } + return ret + } + + input := &iam.SimulatePrincipalPolicyInput{ + ActionNames: setAsAWSStringSlice(d.Get("action_names")), + PermissionsBoundaryPolicyInputList: setAsAWSStringSlice(d.Get("permissions_boundary_policies_json")), + PolicyInputList: setAsAWSStringSlice(d.Get("additional_policies_json")), + PolicySourceArn: aws.String(d.Get("policy_source_arn").(string)), + ResourceArns: setAsAWSStringSlice(d.Get("resource_arns")), + } + + for _, entryRaw := range d.Get("context").(*schema.Set).List() { + entryRaw := entryRaw.(map[string]interface{}) + entry := &iam.ContextEntry{ + ContextKeyName: aws.String(entryRaw["key"].(string)), + ContextKeyType: aws.String(entryRaw["type"].(string)), + ContextKeyValues: setAsAWSStringSlice(entryRaw["values"]), + } + input.ContextEntries = append(input.ContextEntries, entry) + } + + if v := d.Get("caller_arn").(string); v != "" { + input.CallerArn = aws.String(v) + } + if v := d.Get("resource_handling_option").(string); v != "" { + input.ResourceHandlingOption = aws.String(v) + } + if v := d.Get("resource_owner_account_id").(string); v != "" { + input.ResourceOwner = aws.String(v) + } + if v := d.Get("resource_policy_json").(string); v != "" { + input.ResourcePolicy = aws.String(v) + } + + // We are going to keep fetching through potentially multiple pages of + // results in order to return a complete result, so we'll ask the API + // to return as much as possible in each request to minimize the + // round-trips. + input.MaxItems = aws.Int64(1000) + + var results []*iam.EvaluationResult + + for { // Terminates below, once we see a result that does not set IsTruncated. + resp, err := conn.SimulatePrincipalPolicy(input) + if err != nil { + return fmt.Errorf("policy simulation error (%s): %w", d.Id(), err) + } + + results = append(results, resp.EvaluationResults...) + + if !aws.BoolValue(resp.IsTruncated) { + break // All done! + } + + // If we're making another request then we need to specify the marker + // to get the next page of results. + input.Marker = resp.Marker + } + + // While we build the result we'll also tally up the number of allowed + // vs. denied decisions to use for our top-level "all_allowed" summary + // result. + allowedCount := 0 + deniedCount := 0 + + rawResults := make([]interface{}, len(results)) + for i, result := range results { + rawResult := map[string]interface{}{} + rawResult["action_name"] = aws.StringValue(result.EvalActionName) + rawResult["decision"] = aws.StringValue(result.EvalDecision) + allowed := aws.StringValue(result.EvalDecision) == "allowed" + rawResult["allowed"] = allowed + if allowed { + allowedCount++ + } else { + deniedCount++ + } + if result.EvalResourceName != nil { + rawResult["resource_arn"] = aws.StringValue(result.EvalResourceName) + } + + var missingContextKeys []string + for _, mkk := range result.MissingContextValues { + if mkk != nil { + missingContextKeys = append(missingContextKeys, *mkk) + } + } + rawResult["missing_context_keys"] = missingContextKeys + + decisionDetails := make(map[string]string, len(result.EvalDecisionDetails)) + for k, pv := range result.EvalDecisionDetails { + if pv != nil { + decisionDetails[k] = aws.StringValue(pv) + } + } + rawResult["decision_details"] = decisionDetails + + rawMatchedStmts := make([]interface{}, len(result.MatchedStatements)) + for i, stmt := range result.MatchedStatements { + rawStmt := map[string]interface{}{ + "source_policy_id": stmt.SourcePolicyId, + "source_policy_type": stmt.SourcePolicyType, + } + rawMatchedStmts[i] = rawStmt + } + rawResult["matched_statements"] = rawMatchedStmts + + rawResults[i] = rawResult + } + d.Set("results", rawResults) + + // "all" are allowed only if there is at least one result and no other + // results were denied. We require at least one allowed here just as + // a safety-net against a confusing result from a degenerate request. + d.Set("all_allowed", allowedCount > 0 && deniedCount == 0) + + d.SetId("-") + + return nil +} diff --git a/internal/service/iam/principal_policy_simulation_data_source_test.go b/internal/service/iam/principal_policy_simulation_data_source_test.go new file mode 100644 index 00000000000..f80c87954c6 --- /dev/null +++ b/internal/service/iam/principal_policy_simulation_data_source_test.go @@ -0,0 +1,209 @@ +package iam_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/iam" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" +) + +func TestAccIAMPrincipalPolicySimulationDataSource(t *testing.T) { + + uniqueName := fmt.Sprintf("policy-simulation-test-%d", sdkacctest.RandInt()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, iam.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccPrincipalPolicySimulationDataSourceConfig_main(uniqueName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_simple", "all_allowed", "true"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_simple", "results.#", "1"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_simple", "results.0.action_name", "ec2:AssociateVpcCidrBlock"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_simple", "results.0.allowed", "true"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_simple", "results.0.decision", "allowed"), + + // IAM seems to generate the SourcePolicyId by concatenating + // together the username, the policy name, and some other + // hard-coded bits. Not sure if this is constractual, so + // if this turns out to change in future it may be better + // to test this in a different way. + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_simple", "results.0.matched_statements.0.source_policy_id", fmt.Sprintf("user_%s_%s", uniqueName, uniqueName)), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_simple", "results.0.matched_statements.0.source_policy_type", "IAM Policy"), + + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.deny_explicit", "all_allowed", "false"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.deny_explicit", "results.#", "1"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.deny_explicit", "results.0.action_name", "ec2:AttachClassicLinkVpc"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.deny_explicit", "results.0.allowed", "false"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.deny_explicit", "results.0.decision", "explicitDeny"), + + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.deny_implicit", "all_allowed", "false"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.deny_implicit", "results.#", "1"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.deny_implicit", "results.0.action_name", "ec2:AttachVpnGateway"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.deny_implicit", "results.0.allowed", "false"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.deny_implicit", "results.0.decision", "implicitDeny"), + + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_with_context", "all_allowed", "true"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_with_context", "results.#", "1"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_with_context", "results.0.action_name", "ec2:AttachInternetGateway"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_with_context", "results.0.allowed", "true"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_with_context", "results.0.decision", "allowed"), + + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_with_wrong_context", "all_allowed", "false"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_with_wrong_context", "results.#", "1"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_with_wrong_context", "results.0.action_name", "ec2:AttachInternetGateway"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_with_wrong_context", "results.0.allowed", "false"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_with_wrong_context", "results.0.decision", "implicitDeny"), + + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.multiple_mixed", "all_allowed", "false"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.multiple_mixed", "results.#", "2"), + + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.multiple_allow", "all_allowed", "true"), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.multiple_allow", "results.#", "2"), + + func(state *terraform.State) error { + vpcARN := state.RootModule().Outputs["vpc_arn"].Value.(string) + return resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_simple", "results.0.resource_arn", vpcARN)(state) + }, + ), + }, + }, + }) +} + +func testAccPrincipalPolicySimulationDataSourceConfig_main(name string) string { + return fmt.Sprintf(` +resource "aws_iam_user" "test" { + name = "%s" +} + +resource "aws_vpc" "test" { + cidr_block = "192.168.0.0/16" +} + +resource "aws_iam_user_policy" "test" { + name = "%s" + user = aws_iam_user.test.name + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "ec2:AssociateVpcCidrBlock" + Effect = "Allow" + Resource = aws_vpc.test.arn + }, + { + Action = "ec2:AttachClassicLinkVpc" + Effect = "Deny" + Resource = aws_vpc.test.arn + }, + { + Action = "ec2:AttachInternetGateway" + Effect = "Allow" + Resource = aws_vpc.test.arn + Condition = { + StringEquals = { + "ec2:ResourceTag/Foo" = "bar" + } + } + }, + ] + }) +} + +data "aws_iam_principal_policy_simulation" "allow_simple" { + action_names = ["ec2:AssociateVpcCidrBlock"] + resource_arns = [aws_vpc.test.arn] + policy_source_arn = aws_iam_user.test.arn + + depends_on = [aws_iam_user_policy.test] +} + +data "aws_iam_principal_policy_simulation" "deny_explicit" { + action_names = ["ec2:AttachClassicLinkVpc"] + resource_arns = [aws_vpc.test.arn] + policy_source_arn = aws_iam_user.test.arn + + depends_on = [aws_iam_user_policy.test] +} + +data "aws_iam_principal_policy_simulation" "deny_implicit" { + # This one is implicit deny because our policy + # doesn't mention ec2:AttachVpnGateway at all. + action_names = ["ec2:AttachVpnGateway"] + resource_arns = [aws_vpc.test.arn] + policy_source_arn = aws_iam_user.test.arn + + depends_on = [aws_iam_user_policy.test] +} + +data "aws_iam_principal_policy_simulation" "allow_with_context" { + action_names = ["ec2:AttachInternetGateway"] + resource_arns = [aws_vpc.test.arn] + policy_source_arn = aws_iam_user.test.arn + + context { + key = "ec2:ResourceTag/Foo" + type = "string" + values = ["bar"] + } + + depends_on = [aws_iam_user_policy.test] +} + +data "aws_iam_principal_policy_simulation" "allow_with_wrong_context" { + action_names = ["ec2:AttachInternetGateway"] + resource_arns = [aws_vpc.test.arn] + policy_source_arn = aws_iam_user.test.arn + + context { + key = "ec2:ResourceTag/Foo" + type = "string" + values = ["baz"] + } + + depends_on = [aws_iam_user_policy.test] +} + +data "aws_iam_principal_policy_simulation" "multiple_mixed" { + action_names = [ + "ec2:AssociateVpcCidrBlock", + "ec2:AttachClassicLinkVpc", + ] + resource_arns = [aws_vpc.test.arn] + policy_source_arn = aws_iam_user.test.arn + + depends_on = [aws_iam_user_policy.test] +} + +data "aws_iam_principal_policy_simulation" "multiple_allow" { + action_names = [ + "ec2:AssociateVpcCidrBlock", + "ec2:AttachInternetGateway", + ] + resource_arns = [aws_vpc.test.arn] + policy_source_arn = aws_iam_user.test.arn + + context { + key = "ec2:ResourceTag/Foo" + type = "string" + values = ["bar"] + } + + depends_on = [aws_iam_user_policy.test] +} + +output "vpc_arn" { + value = aws_vpc.test.arn +} +`, + name, name, + ) +} diff --git a/website/docs/d/iam_principal_policy_simulation.html.markdown b/website/docs/d/iam_principal_policy_simulation.html.markdown new file mode 100644 index 00000000000..3d877227165 --- /dev/null +++ b/website/docs/d/iam_principal_policy_simulation.html.markdown @@ -0,0 +1,220 @@ +--- +subcategory: "IAM (Identity & Access Management)" +layout: "aws" +page_title: "AWS: aws_iam_principal_policy_simulation" +description: |- + Runs a simulation of the IAM policies of a particular princial against a given hypothetical request. +--- + +# Data Source: aws_iam_principal_policy_simulation + +Runs a simulation of the IAM policies of a particular princial against a given hypothetical request. + +You can use this data source in conjunction with +[Preconditions and Postconditions](https://www.terraform.io/language/expressions/custom-conditions#preconditions-and-postconditions) so that your configuration can test either whether it should have sufficient access to do its own work, or whether policies your configuration declares itself are sufficient for their intended use elsewhere. + +-> **Note:** Correctly using this data source requires familiarity with various details of AWS Identity and Access Management, and how various AWS services integrate with it. For general information on the AWS IAM policy simulator, see [Testing IAM policies with the IAM policy simulator](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_testing-policies.html). This data source wraps the `iam:SimulatePrincipalPolicy` API action described on that page. + +## Example Usage + +### Self Access-checking Example + +The following example raises an error if the credentials passed to the AWS provider do not have access to perform the three actions `s3:GetObject`, `s3:PutObject`, and `s3:DeleteObject` on the S3 bucket with the given ARN. It combines `aws_iam_principal_policy_simulation` with the core Terraform postconditions feature. + +```terraform +data "aws_caller_identity" "current" {} + +data "aws_iam_principal_policy_simulation" "s3_object_access" { + action_names = [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", + ] + policy_source_arn = data.aws_caller_identity.current.arn + resource_arns = ["arn:aws:s3:::my-test-bucket"] + + # The "lifecycle" and "postcondition" block types are part of + # the main Terraform language, not part of this data source. + lifecycle { + postcondition { + condition = self.all_allowed + error_message = < Date: Mon, 5 Jun 2023 14:56:47 -0400 Subject: [PATCH 2/6] d/aws_iam_principal_policy_simulation: Use 'WithoutTimeout' CRUD handler variants. --- ...principal_policy_simulation_data_source.go | 29 +++++++++---------- ...ipal_policy_simulation_data_source_test.go | 20 +++++++------ 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/internal/service/iam/principal_policy_simulation_data_source.go b/internal/service/iam/principal_policy_simulation_data_source.go index 27935595694..4b4a0ead790 100644 --- a/internal/service/iam/principal_policy_simulation_data_source.go +++ b/internal/service/iam/principal_policy_simulation_data_source.go @@ -1,20 +1,23 @@ package iam import ( - "fmt" + "context" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/iam" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + "github.com/hashicorp/terraform-provider-aws/internal/flex" "github.com/hashicorp/terraform-provider-aws/internal/verify" ) // @SDKDataSource("aws_iam_principal_policy_simulation") func DataSourcePrincipalPolicySimulation() *schema.Resource { return &schema.Resource{ - Read: dataSourcePrincipalPolicySimulationRead, + ReadWithoutTimeout: dataSourcePrincipalPolicySimulationRead, Schema: map[string]*schema.Schema{ // Arguments @@ -201,7 +204,8 @@ func DataSourcePrincipalPolicySimulation() *schema.Resource { } } -func dataSourcePrincipalPolicySimulationRead(d *schema.ResourceData, meta interface{}) error { +func dataSourcePrincipalPolicySimulationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics conn := meta.(*conns.AWSClient).IAMConn() setAsAWSStringSlice := func(raw interface{}) []*string { @@ -209,12 +213,7 @@ func dataSourcePrincipalPolicySimulationRead(d *schema.ResourceData, meta interf if len(listOfInterface) == 0 { return nil } - ret := make([]*string, len(listOfInterface)) - for i, iface := range listOfInterface { - str := iface.(string) - ret[i] = &str - } - return ret + return flex.ExpandStringList(listOfInterface) } input := &iam.SimulatePrincipalPolicyInput{ @@ -257,20 +256,20 @@ func dataSourcePrincipalPolicySimulationRead(d *schema.ResourceData, meta interf var results []*iam.EvaluationResult for { // Terminates below, once we see a result that does not set IsTruncated. - resp, err := conn.SimulatePrincipalPolicy(input) + output, err := conn.SimulatePrincipalPolicyWithContext(ctx, input) if err != nil { - return fmt.Errorf("policy simulation error (%s): %w", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "simulating IAM Principal Policy: %s", err) } - results = append(results, resp.EvaluationResults...) + results = append(results, output.EvaluationResults...) - if !aws.BoolValue(resp.IsTruncated) { + if !aws.BoolValue(output.IsTruncated) { break // All done! } // If we're making another request then we need to specify the marker // to get the next page of results. - input.Marker = resp.Marker + input.Marker = output.Marker } // While we build the result we'll also tally up the number of allowed @@ -332,5 +331,5 @@ func dataSourcePrincipalPolicySimulationRead(d *schema.ResourceData, meta interf d.SetId("-") - return nil + return diags } diff --git a/internal/service/iam/principal_policy_simulation_data_source_test.go b/internal/service/iam/principal_policy_simulation_data_source_test.go index d4b3b2c8d05..104cfd25d3a 100644 --- a/internal/service/iam/principal_policy_simulation_data_source_test.go +++ b/internal/service/iam/principal_policy_simulation_data_source_test.go @@ -13,7 +13,7 @@ import ( func TestAccIAMPrincipalPolicySimulationDataSource(t *testing.T) { ctx := acctest.Context(t) - uniqueName := fmt.Sprintf("policy-simulation-test-%d", sdkacctest.RandInt()) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, @@ -21,7 +21,7 @@ func TestAccIAMPrincipalPolicySimulationDataSource(t *testing.T) { ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccPrincipalPolicySimulationDataSourceConfig_main(uniqueName), + Config: testAccPrincipalPolicySimulationDataSourceConfig_main(rName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_simple", "all_allowed", "true"), resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_simple", "results.#", "1"), @@ -34,7 +34,7 @@ func TestAccIAMPrincipalPolicySimulationDataSource(t *testing.T) { // hard-coded bits. Not sure if this is constractual, so // if this turns out to change in future it may be better // to test this in a different way. - resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_simple", "results.0.matched_statements.0.source_policy_id", fmt.Sprintf("user_%s_%s", uniqueName, uniqueName)), + resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_simple", "results.0.matched_statements.0.source_policy_id", fmt.Sprintf("user_%s_%s", rName, rName)), resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.allow_simple", "results.0.matched_statements.0.source_policy_type", "IAM Policy"), resource.TestCheckResourceAttr("data.aws_iam_principal_policy_simulation.deny_explicit", "all_allowed", "false"), @@ -77,18 +77,22 @@ func TestAccIAMPrincipalPolicySimulationDataSource(t *testing.T) { }) } -func testAccPrincipalPolicySimulationDataSourceConfig_main(name string) string { +func testAccPrincipalPolicySimulationDataSourceConfig_main(rName string) string { return fmt.Sprintf(` resource "aws_iam_user" "test" { - name = "%s" + name = %[1]q } resource "aws_vpc" "test" { cidr_block = "192.168.0.0/16" + + tags = { + Name = %[1]q + } } resource "aws_iam_user_policy" "test" { - name = "%s" + name = %[1]q user = aws_iam_user.test.name policy = jsonencode({ @@ -203,7 +207,5 @@ data "aws_iam_principal_policy_simulation" "multiple_allow" { output "vpc_arn" { value = aws_vpc.test.arn } -`, - name, name, - ) +`, rName) } From 945e3346d698718d9e1a5c40ab749b0ba3e07115 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 5 Jun 2023 14:59:01 -0400 Subject: [PATCH 3/6] Tweak CHANGELOG entry. --- .changelog/25569.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changelog/25569.txt b/.changelog/25569.txt index 7df0ee2e471..da95c6e2c87 100644 --- a/.changelog/25569.txt +++ b/.changelog/25569.txt @@ -1,3 +1,3 @@ ```release-note:new-data-source -aws_iam_principal_policy_simulation: Run simulated requests against a user's, group's, or role's IAM policies. +aws_iam_principal_policy_simulation ``` From 7c987287568431e66a9bae404943b7e597b2e22a Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 5 Jun 2023 15:00:52 -0400 Subject: [PATCH 4/6] Fix spelling mistake in documentation. --- website/docs/d/iam_principal_policy_simulation.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/d/iam_principal_policy_simulation.html.markdown b/website/docs/d/iam_principal_policy_simulation.html.markdown index 3d877227165..b5a9e41b735 100644 --- a/website/docs/d/iam_principal_policy_simulation.html.markdown +++ b/website/docs/d/iam_principal_policy_simulation.html.markdown @@ -3,12 +3,12 @@ subcategory: "IAM (Identity & Access Management)" layout: "aws" page_title: "AWS: aws_iam_principal_policy_simulation" description: |- - Runs a simulation of the IAM policies of a particular princial against a given hypothetical request. + Runs a simulation of the IAM policies of a particular principal against a given hypothetical request. --- # Data Source: aws_iam_principal_policy_simulation -Runs a simulation of the IAM policies of a particular princial against a given hypothetical request. +Runs a simulation of the IAM policies of a particular principal against a given hypothetical request. You can use this data source in conjunction with [Preconditions and Postconditions](https://www.terraform.io/language/expressions/custom-conditions#preconditions-and-postconditions) so that your configuration can test either whether it should have sufficient access to do its own work, or whether policies your configuration declares itself are sufficient for their intended use elsewhere. From 19adb867252e01832433567536e3008e44f51b64 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 5 Jun 2023 15:13:53 -0400 Subject: [PATCH 5/6] 'TestAccIAMPrincipalPolicySimulationDataSource' -> 'TestAccIAMPrincipalPolicySimulationDataSource_basic'. --- .../service/iam/principal_policy_simulation_data_source_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/iam/principal_policy_simulation_data_source_test.go b/internal/service/iam/principal_policy_simulation_data_source_test.go index 104cfd25d3a..10ecddf7e2f 100644 --- a/internal/service/iam/principal_policy_simulation_data_source_test.go +++ b/internal/service/iam/principal_policy_simulation_data_source_test.go @@ -11,7 +11,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/acctest" ) -func TestAccIAMPrincipalPolicySimulationDataSource(t *testing.T) { +func TestAccIAMPrincipalPolicySimulationDataSource_basic(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) From 4f2f304a8602ad34aa2459b818f0022efb7c8c37 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 5 Jun 2023 15:16:43 -0400 Subject: [PATCH 6/6] Fix semgrep 'ci.helper-schema-Set-extraneous-expandStringList-with-List'. --- .../service/iam/principal_policy_simulation_data_source.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/service/iam/principal_policy_simulation_data_source.go b/internal/service/iam/principal_policy_simulation_data_source.go index 4b4a0ead790..4787ab610c6 100644 --- a/internal/service/iam/principal_policy_simulation_data_source.go +++ b/internal/service/iam/principal_policy_simulation_data_source.go @@ -209,11 +209,10 @@ func dataSourcePrincipalPolicySimulationRead(ctx context.Context, d *schema.Reso conn := meta.(*conns.AWSClient).IAMConn() setAsAWSStringSlice := func(raw interface{}) []*string { - listOfInterface := raw.(*schema.Set).List() - if len(listOfInterface) == 0 { + if raw.(*schema.Set).Len() == 0 { return nil } - return flex.ExpandStringList(listOfInterface) + return flex.ExpandStringSet(raw.(*schema.Set)) } input := &iam.SimulatePrincipalPolicyInput{