diff --git a/docs/rules/README.md b/docs/rules/README.md index af6892dd..124f70eb 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -68,6 +68,7 @@ These rules enforce best practices and naming conventions: |[aws_elasticache_replication_group_previous_type](aws_elasticache_replication_group_previous_type.md)|Disallow using previous node types|✔| |[aws_elasticache_replication_group_default_parameter_group](aws_elasticache_replication_group_default_parameter_group.md)|Disallow using default parameter group|✔| |[aws_instance_previous_type](aws_instance_previous_type.md)|Disallow using previous generation instance types|✔| +|[aws_iam_policy_attachment_exclusive_attachment](aws_iam_policy_attachment_exclusive_attachment.md)|Consider alternative resources to `aws_iam_policy_attachment`|| |[aws_iam_policy_document_gov_friendly_arns](aws_iam_policy_document_gov_friendly_arns.md)|Ensure `iam_policy_document` data sources do not contain `arn:aws:` ARN's|| |[aws_iam_policy_gov_friendly_arns](aws_iam_policy_gov_friendly_arns.md)|Ensure `iam_policy` resources do not contain `arn:aws:` ARN's|| |[aws_iam_role_policy_gov_friendly_arns](aws_iam_role_policy_gov_friendly_arns.md)|Ensure `iam_role_policy` resources do not contain `arn:aws:` ARN's|| diff --git a/docs/rules/README.md.tmpl b/docs/rules/README.md.tmpl index f197f6fe..22dab0ff 100644 --- a/docs/rules/README.md.tmpl +++ b/docs/rules/README.md.tmpl @@ -68,6 +68,7 @@ These rules enforce best practices and naming conventions: |[aws_elasticache_replication_group_previous_type](aws_elasticache_replication_group_previous_type.md)|Disallow using previous node types|✔| |[aws_elasticache_replication_group_default_parameter_group](aws_elasticache_replication_group_default_parameter_group.md)|Disallow using default parameter group|✔| |[aws_instance_previous_type](aws_instance_previous_type.md)|Disallow using previous generation instance types|✔| +|[aws_iam_policy_attachment_exclusive_attachment](aws_iam_policy_attachment_exclusive_attachment.md)|Consider alternative resources to `aws_iam_policy_attachment`|| |[aws_iam_policy_document_gov_friendly_arns](aws_iam_policy_document_gov_friendly_arns.md)|Ensure `iam_policy_document` data sources do not contain `arn:aws:` ARN's|| |[aws_iam_policy_gov_friendly_arns](aws_iam_policy_gov_friendly_arns.md)|Ensure `iam_policy` resources do not contain `arn:aws:` ARN's|| |[aws_iam_role_policy_gov_friendly_arns](aws_iam_role_policy_gov_friendly_arns.md)|Ensure `iam_role_policy` resources do not contain `arn:aws:` ARN's|| diff --git a/docs/rules/aws_iam_policy_attachment_exclusive_attachment.md b/docs/rules/aws_iam_policy_attachment_exclusive_attachment.md new file mode 100644 index 00000000..a0bfa8aa --- /dev/null +++ b/docs/rules/aws_iam_policy_attachment_exclusive_attachment.md @@ -0,0 +1,37 @@ +# aws_iam_policy_attachment_exclusive_attachment + +This rule checks whether the `aws_iam_policy_attachment` resource is used. + +The `aws_iam_policy_attachment` resource creates exclusive attachments for IAM policies. Within the entire AWS account, all users, roles, and groups that a single policy is attached to must be specified by a single aws_iam_policy_attachment resource. + +## Configuration + +```hcl +rule "aws_iam_policy_attachment_exclusive_attachment" { + enabled = true +} +``` + +## Example + +```hcl +resource "aws_iam_policy_attachment" "attachment" { + name = "test_attachment" +} +``` + +```shell +$ tflint +1 issue(s) found: +Warning: Within the entire AWS account, all users, roles, and groups that a single policy is attached to must be specified by a single aws_iam_policy_attachment resource. Consider aws_iam_role_policy_attachment, aws_iam_user_policy_attachment, or aws_iam_group_policy_attachment instead. (aws_iam_policy_attachment_has_alternatives) + on template.tf line 2: + 2: resource "aws_iam_policy_attachment" "attachment" { +``` + +## Why + +The [`aws_iam_policy_attachment`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy_attachment) resource creates exclusive attachments of IAM policies. Across the entire AWS account, all the users/roles/groups to which a single policy is attached must be declared by a single `aws_iam_policy_attachment` resource. This means that even any users/roles/groups that have the attached policy via any other mechanism (including other Terraform resources) will have that attached policy revoked by this resource. + +## How To Fix + +Consider using `aws_iam_role_policy_attachment`, `aws_iam_user_policy_attachment`, or `aws_iam_group_policy_attachment` instead. These resources do not enforce exclusive attachment of an IAM policy. diff --git a/rules/aws_iam_policy_attachment_exclusive_attachment.go b/rules/aws_iam_policy_attachment_exclusive_attachment.go new file mode 100644 index 00000000..068e573e --- /dev/null +++ b/rules/aws_iam_policy_attachment_exclusive_attachment.go @@ -0,0 +1,65 @@ +package rules + +import ( + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" + "github.com/terraform-linters/tflint-ruleset-aws/project" +) + +// AwsIAMPolicyAttachmentExclusiveAttachmentRule warns that the resource has alternatives recommended +type AwsIAMPolicyAttachmentExclusiveAttachmentRule struct { + tflint.DefaultRule + + resourceType string + attributeName string +} + +// AwsIAMPolicyAttachmentExclusiveAttachmentRule returns new rule with default attributes +func NewAwsIAMPolicyAttachmentExclusiveAttachmentRule() *AwsIAMPolicyAttachmentExclusiveAttachmentRule { + return &AwsIAMPolicyAttachmentExclusiveAttachmentRule{ + resourceType: "aws_iam_policy_attachment", + attributeName: "name", + } +} + +// Name returns the rule name +func (r *AwsIAMPolicyAttachmentExclusiveAttachmentRule) Name() string { + return "aws_iam_policy_attachment_exclusive_attachment" +} + +// Enabled returns whether the rule is enabled by default +func (r *AwsIAMPolicyAttachmentExclusiveAttachmentRule) Enabled() bool { + return false +} + +// Severity returns the rule severity +func (r *AwsIAMPolicyAttachmentExclusiveAttachmentRule) Severity() tflint.Severity { + return tflint.WARNING +} + +// Link returns the rule reference link +func (r *AwsIAMPolicyAttachmentExclusiveAttachmentRule) Link() string { + return project.ReferenceLink(r.Name()) +} + +// Check that the resource is not used +func (r *AwsIAMPolicyAttachmentExclusiveAttachmentRule) Check(runner tflint.Runner) error { + resources, err := runner.GetResourceContent(r.resourceType, &hclext.BodySchema{}, nil) + if err != nil { + return err + } + + for _, resource := range resources.Blocks { + runner.EmitIssue( + r, + "Within the entire AWS account, all users, roles, and groups that a single policy is attached to must be specified by a single aws_iam_policy_attachment resource. Consider aws_iam_role_policy_attachment, aws_iam_user_policy_attachment, or aws_iam_group_policy_attachment instead.", + resource.DefRange, + ) + + if err != nil { + return err + } + } + + return nil +} diff --git a/rules/aws_iam_policy_attachment_exclusive_attachment_test.go b/rules/aws_iam_policy_attachment_exclusive_attachment_test.go new file mode 100644 index 00000000..c3cdaa1a --- /dev/null +++ b/rules/aws_iam_policy_attachment_exclusive_attachment_test.go @@ -0,0 +1,61 @@ +package rules + +import ( + "math/rand" + "testing" + "time" + + hcl "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint-plugin-sdk/helper" +) + +func Test_AwsIAMPolicyAttachmentExclusiveAttachmentRule(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + + cases := []struct { + Name string + Content string + Expected helper.Issues + }{ + { + Name: "resource has alternatives", + Content: ` +resource "aws_iam_policy_attachment" "attachment" { + name = "test_attachment" +} +`, + Expected: helper.Issues{ + { + Rule: NewAwsIAMPolicyAttachmentExclusiveAttachmentRule(), + Message: "Within the entire AWS account, all users, roles, and groups that a single policy is attached to must be specified by a single aws_iam_policy_attachment resource. Consider aws_iam_role_policy_attachment, aws_iam_user_policy_attachment, or aws_iam_group_policy_attachment instead.", + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 50}, + }, + }, + }, + }, + { + Name: "no issues with resource", + Content: ` +resource "aws_iam_role_policy_attachment" "attachment" { + role = "test_role" +} +`, + Expected: helper.Issues{}, + }, + } + + rule := NewAwsIAMPolicyAttachmentExclusiveAttachmentRule() + + for _, tc := range cases { + runner := helper.TestRunner(t, map[string]string{"resource.tf": tc.Content}) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + helper.AssertIssues(t, tc.Expected, runner.Issues) + } +} diff --git a/rules/provider.go b/rules/provider.go index 4d450b64..e1a52830 100644 --- a/rules/provider.go +++ b/rules/provider.go @@ -31,6 +31,7 @@ var manualRules = []tflint.Rule{ NewAwsElastiCacheReplicationGroupDefaultParameterGroupRule(), NewAwsElastiCacheReplicationGroupInvalidTypeRule(), NewAwsElastiCacheReplicationGroupPreviousTypeRule(), + NewAwsIAMPolicyAttachmentExclusiveAttachmentRule(), NewAwsIAMPolicySidInvalidCharactersRule(), NewAwsIAMPolicyTooLongPolicyRule(), NewAwsLambdaFunctionDeprecatedRuntimeRule(),