diff --git a/README.md b/README.md index 5566235..3e71f00 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ This repository contains reusable infrastructure modules designed for enterprise | `networking/` | `vpc` | AWS VPC with adaptive public and private subnets | v1.0.0 | | `security/` | `acm_certificate` | AWS ACM public certificates with ordered domains, DNS validation, optional Route53, and optional wait | v1.0.0 | | `security/` | `iam` | AWS IAM roles and policies | v1.0.0 | +| `security/` | `iam_policy` | Reusable customer-managed AWS IAM policies | v1.0.0 | | `security/` | `kms` | AWS KMS keys (symmetric or asymmetric: signing, encryption, MAC, key agreement) | v1.0.0 | | `security/` | `secrets-manager` | AWS Secrets Manager secrets | Planned | | `stack/` | `terraform` | Ravion Terraform/OpenTofu stack workflows with git triggers and managed state (includes `rvn-stack` module definition) | v1.2.3 | diff --git a/security/iam_policy/.terraform.lock.hcl b/security/iam_policy/.terraform.lock.hcl new file mode 100644 index 0000000..defc1a0 --- /dev/null +++ b/security/iam_policy/.terraform.lock.hcl @@ -0,0 +1,26 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/aws" { + version = "6.56.0" + constraints = ">= 6.0.0" + hashes = [ + "h1:34wj37Q0GYVYOpPZzJmWe7Cn0oLRLs0ayufm/D8sJow=", + "h1:OzyJEpEhKzbd6MrlOuXl0044PaHtN1yZt8pwmvvd36o=", + "zh:0df287675010e67011cd830a69fb8c81eb81b8cc82ec21f806bf5b70ffacb94c", + "zh:1b0a431e4a51be43b77dc5f790906567a0a6a9e683b442be0cd2758f1e1d5637", + "zh:1c58a0335198d558cb2ea83a42ef043b89f817c5e67a54a3746d3a47344ba602", + "zh:1ce5a623d2a2d9c006e0009fa67a9d486984abd3084648fcdd7997b7fc022233", + "zh:2380bc878d1127d75155df23f8fad4b64fe21db4856070dc084fcde80b856ca9", + "zh:51d77a6f1847fbf97874c976cf134b105abce8fce0beba38fc169ab5cfb7de66", + "zh:6364cac70a860c107b4a019aeb8c8afd9cb7e70bcd5a25ae62b74e27eaadeac2", + "zh:68e8d389804f6fc40fe5071093c52d41b82a18436be01ff75eba719d5c43286a", + "zh:82d4b24e9eb2e01568e0c0bc43c85686093d3c6f1a97461374c3009333f0522c", + "zh:b5845bd95ba758f6d411683bf14b5ee9208f31872d416f1aa82975e2ac31d696", + "zh:c228a7e9fab6319af525d78052f542504a042356ba1e71cd3c5f218c8b0fd7c4", + "zh:c5dd55276e518c6ab215e298091577ceb618ca405603885bb5e153b95d2e7d8c", + "zh:c6ceeb277f1897a0f813b913bf270db409f4fc3ec95d24238e5f5e71709edf7e", + "zh:f1e42b4c42faebd28f6040201760bfdd1e3391d858abd7586c7e513433e8a73d", + "zh:fff049bcd38e4cd82d526670e81aceb5f30d211c8b494221773737cade6e27c2", + ] +} diff --git a/security/iam_policy/README.md b/security/iam_policy/README.md new file mode 100644 index 0000000..27b7232 --- /dev/null +++ b/security/iam_policy/README.md @@ -0,0 +1,99 @@ +# AWS IAM Policy Module + +Creates one reusable customer-managed AWS IAM policy from structured statements or a complete JSON policy document. + +## Usage + +```hcl +module "application_policy" { + source = "git::https://github.com/flightcontrolhq/modules.git//security/iam_policy?ref=v1.0.0" + + name = "application-s3-read" + description = "Allows the application to read objects from its bucket." + + policy_statements = [{ + sid = "ReadApplicationObjects" + effect = "Allow" + actions = ["s3:GetObject"] + resources = ["arn:aws:s3:::application-bucket/*"] + conditions = [{ + test = "StringEquals" + variable = "aws:ResourceAccount" + values = ["123456789012"] + }] + }] + + tags = { + Environment = "production" + } +} +``` + +Use `policy_json` when a complete policy document is more practical. When set, it overrides `policy_statements`. + +```hcl +module "custom_policy" { + source = "git::https://github.com/flightcontrolhq/modules.git//security/iam_policy?ref=v1.0.0" + + name = "custom-policy" + policy_json = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = ["logs:CreateLogStream", "logs:PutLogEvents"] + Resource = "arn:aws:logs:us-east-1:123456789012:log-group:/aws/example:*" + }] + }) +} +``` + +## Structured Statements + +Each structured statement must set exactly one of `actions` or `not_actions` and exactly one of `resources` or `not_resources`. `NotAction` and `NotResource` can create broad matches, especially with an `Allow` effect, so prefer positive action and resource lists unless an inverse statement is necessary. + +```hcl +policy_statements = [{ + sid = "DenyOutsideApprovedResources" + effect = "Deny" + actions = ["s3:*"] + not_resources = [ + "arn:aws:s3:::approved-bucket", + "arn:aws:s3:::approved-bucket/*", + ] +}] +``` + +## Requirements + +| Name | Version | +|------|---------| +| opentofu/terraform | >= 1.10.0 | +| aws | >= 6.0 | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|----------| +| name | Customer-managed IAM policy name. | `string` | n/a | yes | +| description | Policy description. AWS does not allow updates after creation. | `string` | `null` | no | +| path | IAM path for the policy. | `string` | `"/"` | no | +| policy_statements | Structured IAM statements. Ignored when `policy_json` is set. | `list(object)` | `[]` | no | +| policy_json | Complete IAM policy JSON document. | `string` | `null` | no | +| region | AWS provider region. IAM resources are global. | `string` | `null` | no | +| tags | Tags to assign to the policy. | `map(string)` | `{}` | no | + +At least one structured statement or a raw JSON policy document is required. + +## Outputs + +| Name | Description | +|------|-------------| +| policy_arn | ARN of the customer-managed IAM policy. | +| policy_id | ID of the customer-managed IAM policy. | +| policy_name | Name of the customer-managed IAM policy. | +| policy_path | Path of the customer-managed IAM policy. | +| attachment_count | Number of entities attached to the policy. | +| aws_account_id | AWS account ID where the policy is created. | +| region | AWS provider region used by the module. | + +Attach `policy_arn` to roles through the `security/iam` module's `managed_policy_arns` input, or use it as that module's `permission_boundary_arn`. diff --git a/security/iam_policy/data.tf b/security/iam_policy/data.tf new file mode 100644 index 0000000..e86a802 --- /dev/null +++ b/security/iam_policy/data.tf @@ -0,0 +1,30 @@ +data "aws_caller_identity" "current" {} + +data "aws_region" "current" {} + +data "aws_iam_policy_document" "structured" { + count = var.policy_json == null && length(var.policy_statements) > 0 ? 1 : 0 + + dynamic "statement" { + for_each = var.policy_statements + + content { + sid = statement.value.sid + effect = statement.value.effect + actions = statement.value.actions + not_actions = statement.value.not_actions + resources = statement.value.resources + not_resources = statement.value.not_resources + + dynamic "condition" { + for_each = statement.value.conditions + + content { + test = condition.value.test + variable = condition.value.variable + values = condition.value.values + } + } + } + } +} diff --git a/security/iam_policy/iam_policy.tf b/security/iam_policy/iam_policy.tf new file mode 100644 index 0000000..dadb32e --- /dev/null +++ b/security/iam_policy/iam_policy.tf @@ -0,0 +1,14 @@ +resource "aws_iam_policy" "this" { + name = var.name + description = var.description + path = var.path + policy = local.policy_document + tags = local.tags + + lifecycle { + precondition { + condition = var.policy_json != null || length(var.policy_statements) > 0 + error_message = "Provide at least one policy_statement or a policy_json document." + } + } +} diff --git a/security/iam_policy/locals.tf b/security/iam_policy/locals.tf new file mode 100644 index 0000000..2a93c40 --- /dev/null +++ b/security/iam_policy/locals.tf @@ -0,0 +1,12 @@ +locals { + region = coalesce(var.region, data.aws_region.current.region) + + default_tags = { + ManagedBy = "terraform" + Module = "security/iam_policy" + } + + tags = merge(local.default_tags, var.tags) + + policy_document = var.policy_json != null ? var.policy_json : one(data.aws_iam_policy_document.structured[*].json) +} diff --git a/security/iam_policy/outputs.tf b/security/iam_policy/outputs.tf new file mode 100644 index 0000000..00a745f --- /dev/null +++ b/security/iam_policy/outputs.tf @@ -0,0 +1,34 @@ +output "policy_arn" { + description = "The ARN of the customer-managed IAM policy." + value = aws_iam_policy.this.arn +} + +output "policy_id" { + description = "The ID of the customer-managed IAM policy." + value = aws_iam_policy.this.id +} + +output "policy_name" { + description = "The name of the customer-managed IAM policy." + value = aws_iam_policy.this.name +} + +output "policy_path" { + description = "The path of the customer-managed IAM policy." + value = aws_iam_policy.this.path +} + +output "attachment_count" { + description = "The number of entities attached to the customer-managed IAM policy." + value = aws_iam_policy.this.attachment_count +} + +output "aws_account_id" { + description = "The AWS account ID where the IAM policy is created." + value = data.aws_caller_identity.current.account_id +} + +output "region" { + description = "The AWS region used to configure the provider." + value = local.region +} diff --git a/security/iam_policy/rvn-aws-iam-policy-definition.yml b/security/iam_policy/rvn-aws-iam-policy-definition.yml new file mode 100644 index 0000000..1d6081c --- /dev/null +++ b/security/iam_policy/rvn-aws-iam-policy-definition.yml @@ -0,0 +1,279 @@ +definition: + type: rvn-aws-iam-policy + name: AWS IAM Policy + description: Creates one reusable customer-managed AWS IAM policy from structured statements or a complete JSON policy document. +release: + version: 0.1.0 + description: Initial module definition with immutable policy identity fields and structured statement support. +module: + inputs: + - $include: ../../partials/inputs/aws-account.yml + - $include: ../../partials/inputs/aws-region.yml + - id: section_policy + label: IAM policy + type: section + - default: <> + description: Name of the customer-managed IAM policy. Policy names must be unique within their path in the AWS account. + id: name + immutable: true + label: Policy name + patterns: + - message: "1-128 letters, numbers, plus, equals, comma, period, at, underscore, or hyphen characters." + pattern: ^[A-Za-z0-9+=,.@_-]{1,128}$ + required: true + type: string + - description: Optional description stored on the IAM policy. AWS does not allow this value to be changed after creation. + id: description + immutable: true + label: Description + patterns: + - message: 1000 characters or fewer. + pattern: ^.{0,1000}$ + type: string + - collapsible: true + default: / + description: IAM path for organizing the policy. Most policies should use the root path. + id: path + immutable: true + label: Path + patterns: + - message: Start and end with a slash, for example / or /application/. + pattern: ^/$|^/.*/$ + - message: Asterisks are not allowed in IAM paths. + pattern: ^[^*]*$ + - message: 512 characters or fewer. + pattern: ^.{1,512}$ + required: true + type: string + - id: section_document + label: Policy document + type: section + - default: structured + description: Build the policy with structured statements or provide a complete JSON document. + id: policy_document_mode + label: Policy document format + required: true + type: string + values: + - label: Structured statements + value: structured + - label: JSON document + value: json + - default: [] + description: Define permissions this policy grants or denies. Each statement matches Action or NotAction values against Resource or NotResource ARNs and can add conditions to narrow access. Prefer explicit actions and resource ARNs over wildcards. + id: policy_statements + item_inputs: + - description: Optional IAM statement ID. + id: sid + label: SID + required: false + type: string + - default: Allow + description: Whether this statement allows or denies the listed actions. + id: effect + label: Effect + required: true + type: string + values: + - label: Allow + value: Allow + - label: Deny + value: Deny + - default: Action + description: Match the listed actions or every applicable action except those listed. + id: action_match_mode + label: Action matching + required: true + type: string + values: + - label: Listed actions + value: Action + - label: All except listed actions + value: NotAction + - add_button_label: Add action + description: IAM actions granted or denied by this statement. + id: actions + label: Actions + placeholder: s3:GetObject + required: true + show_when: + action_match_mode: Action + type: string_array + - add_button_label: Add excluded action + description: IAM actions excluded from this statement's match. NotAction can create broad permissions and should be used carefully. + id: not_actions + label: Excluded actions (NotAction) + placeholder: iam:* + required: true + show_when: + action_match_mode: NotAction + type: string_array + - default: Resource + description: Match the listed resources or every applicable resource except those listed. + id: resource_match_mode + label: Resource matching + required: true + type: string + values: + - label: Listed resources + value: Resource + - label: All except listed resources + value: NotResource + - add_button_label: Add resource + description: Resource ARNs covered by this statement. Use wildcards only when the AWS service requires them. + id: resources + label: Resources + placeholder: arn:aws:s3:::my-bucket/* + required: true + show_when: + resource_match_mode: Resource + type: string_array + - add_button_label: Add excluded resource + description: Resource ARNs excluded from this statement's match. NotResource can create broad permissions and should be used carefully. + id: not_resources + label: Excluded resources (NotResource) + placeholder: arn:aws:s3:::my-bucket/protected/* + required: true + show_when: + resource_match_mode: NotResource + type: string_array + - default: [] + description: Optional IAM conditions that further restrict this statement. + id: conditions + item_inputs: + - $template: ../../partials/templates/iam-condition-operator-input.yml + with: + default: StringEquals + - description: IAM context key evaluated by the condition. + id: variable + label: Condition key + placeholder: aws:ResourceAccount + required: true + type: string + - add_button_label: Add condition value + description: Allowed or matched values for the condition key. + id: values + label: Condition values + placeholder: "123456789012" + required: true + type: string_array + item_label: Condition + label: Conditions + required: false + type: object_array + item_label: Policy statement + item_title: + template: "{effect}: {action_match_mode} on {resource_match_mode}" + fallback: Policy statement + label: Policy statements + required: true + show_when: + policy_document_mode: structured + type: object_array + - description: Complete IAM policy JSON document. This is used instead of structured statements. + id: policy_json + label: Policy JSON + patterns: + - message: Include a JSON object with Version and Statement properties. + pattern: ^\s*\{[\s\S]*"Version"[\s\S]*"Statement"[\s\S]*\}\s*$ + placeholder: |- + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["s3:GetObject"], + "Resource": ["arn:aws:s3:::my-bucket/*"] + } + ] + } + required: true + show_when: + policy_document_mode: json + type: text + - $include: ../../partials/inputs/misc-section.yml + - $include: ../../partials/inputs/tags.yml + - $include: ../../partials/inputs/terraform-settings.yml + - $merge: + - ../../partials/inputs/execution-environment.yml + description: Override the execution environment for Terraform runners. Must use the same AWS account as selected above. + stack: + $template: ../../partials/templates/opentofu-stack.yml + with: + base_path: security/iam_policy + terraform_variables: + ...overrides: << module.input.advanced_terraform_variables >> + description: << module.input.description >> + name: << module.input.name >> + path: << module.input.path >> + policy_json: << module.input.policy_json >> + policy_statements: << module.input.policy_statements || [] >> + region: << module.input.aws_region >> + tags: + $include: ../../partials/stack/ravion-tags.yml + readme: | + Creates one reusable customer-managed AWS IAM policy from structured statements or a complete JSON policy document. + + ## Overview + + Use this module when permissions need to be managed independently from an IAM role. The policy ARN can be attached to one or more roles, reused across workloads, or supplied as a permissions boundary. + + Customer-managed policies are versioned by AWS when their document changes. Keep statements narrowly scoped to the actions and resource ARNs each workload needs. + + ## Use cases + + | Scenario | Benefit | + | --- | --- | + | Shared workload permissions | Reuse one policy across several IAM roles without duplicating inline policy documents. | + | Permissions boundary | Set the maximum permissions that another IAM role can receive. | + | Independently managed access | Update permissions without combining policy ownership with trust relationships or instance profiles. | + | Complex policy document | Use complete JSON when a policy cannot be represented by the structured statement form. | + + ## Policy documents + + Structured statements are the default. Each statement requires an effect, either Action or NotAction values, and either Resource or NotResource values. Optional conditions can restrict access by IAM context keys such as account, source ARN, principal tags, or resource tags. + + NotAction and NotResource invert what a statement matches. They can simplify explicit deny policies, but combining either with Allow can grant broader access than expected. Prefer listed actions and resources unless an inverse statement is necessary. + + JSON document accepts a complete IAM policy document. Use this mode for uncommon IAM elements or documents produced by another trusted policy-authoring workflow. The JSON document replaces structured statements rather than merging with them. + + IAM policies can have broad security impact. Avoid wildcard actions and resources unless the AWS service requires them, and add conditions where they reduce unintended access. + + ## Using the policy + + This module creates the policy but does not attach it. Attach the Policy ARN output through the AWS IAM Role module's Managed policy ARNs setting, or use it as that module's Permission boundary ARN. + + Keeping policy creation and role creation separate allows a policy to be shared, audited, and updated without duplicating role trust configuration. + + ## Configuration + + | Setting | Required | Default | Notes | + | --- | --- | --- | --- | + | AWS account | Yes | None | AWS account where the customer-managed policy is created. | + | Region | Yes | None | Runner region for the stack. IAM resources are global, but Terraform still runs with an AWS region. | + | Policy name | Yes | Module given ID | Must be unique within the selected path in the AWS account. | + | Description | No | None | AWS does not allow a managed policy description to be edited after creation. | + | Path | No | / | IAM path used to organize the policy. | + | Policy document format | Yes | Structured statements | Chooses between the statement editor and complete JSON. | + | Policy statements | In structured mode | Empty list | At least one statement is required when structured mode is selected. | + | Policy JSON | In JSON mode | None | Complete policy document used instead of structured statements. | + | Tags | No | Standard Ravion tags | Additional tags merged with Ravion ownership and identity tags. | + | Terraform execution environment | No | None | Optional runner environment override. | + | Advanced Terraform variables | No | Empty object | Escape hatch for one-off Terraform variable overrides. | + + ## Design decisions + + The module manages one policy and no attachments. IAM roles, users, and groups have separate lifecycles, while a customer-managed policy may be shared by several consumers. + + Structured statements require an explicit Action or NotAction choice and an explicit Resource or NotResource choice. The module does not add wildcard permissions automatically. + + Policy name, description, and path are immutable in Ravion because AWS replaces the managed policy when any of these values change. Replacement would sever attachments managed outside this module. + + Ravion adds standard tags for ownership and traceability. User-provided Tags are merged on top for team, cost, or environment metadata. + + ## Learn more + + - [IAM managed policies](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html) + - [IAM JSON policy elements](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html) + - [IAM permissions boundaries](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) + - [Source module](https://github.com/flightcontrolhq/modules/tree/$local.module_tag/security/iam_policy) diff --git a/security/iam_policy/tests/basic.tftest.hcl b/security/iam_policy/tests/basic.tftest.hcl new file mode 100644 index 0000000..10ed10c --- /dev/null +++ b/security/iam_policy/tests/basic.tftest.hcl @@ -0,0 +1,223 @@ +mock_provider "aws" { + override_resource { + target = aws_iam_policy.this + values = { + arn = "arn:aws:iam::123456789012:policy/test-policy" + attachment_count = 0 + } + } + + override_data { + target = data.aws_caller_identity.current + values = { + account_id = "123456789012" + } + } + + override_data { + target = data.aws_region.current + values = { + region = "us-east-1" + } + } + + override_data { + target = data.aws_iam_policy_document.structured + values = { + json = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"ReadObjects\",\"Effect\":\"Allow\",\"Action\":\"s3:GetObject\",\"Resource\":\"arn:aws:s3:::example-bucket/*\"}]}" + } + } +} + +variables { + name = "test-policy" + policy_statements = [{ + sid = "ReadObjects" + effect = "Allow" + actions = ["s3:GetObject"] + resources = ["arn:aws:s3:::example-bucket/*"] + conditions = [{ + test = "StringEquals" + variable = "aws:ResourceAccount" + values = ["123456789012"] + }] + }] +} + +run "structured_policy_defaults" { + command = plan + + assert { + condition = aws_iam_policy.this.name == "test-policy" + error_message = "The policy name should use the provided name." + } + + assert { + condition = aws_iam_policy.this.path == "/" + error_message = "The policy path should default to root." + } + + assert { + condition = aws_iam_policy.this.description == null + error_message = "The policy description should default to null." + } + + assert { + condition = aws_iam_policy.this.tags["ManagedBy"] == "terraform" + error_message = "The ManagedBy tag should be present." + } + + assert { + condition = aws_iam_policy.this.tags["Module"] == "security/iam_policy" + error_message = "The Module tag should identify security/iam_policy." + } +} + +run "raw_json_overrides_structured_statements" { + command = plan + + variables { + policy_json = <<-JSON + { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Deny", + "Action": "s3:DeleteObject", + "Resource": "arn:aws:s3:::example-bucket/*" + }] + } + JSON + } + + assert { + condition = jsondecode(aws_iam_policy.this.policy).Statement[0].Effect == "Deny" + error_message = "Raw policy JSON should override structured statements." + } +} + +run "user_tags_override_defaults" { + command = plan + + variables { + tags = { + ManagedBy = "ravion" + Environment = "production" + } + } + + assert { + condition = aws_iam_policy.this.tags["ManagedBy"] == "ravion" + error_message = "User tags should override default tag values." + } + + assert { + condition = aws_iam_policy.this.tags["Environment"] == "production" + error_message = "User tags should be merged with default tags." + } +} + +run "missing_policy_document_rejected" { + command = plan + + variables { + policy_statements = [] + } + + expect_failures = [ + aws_iam_policy.this, + ] +} + +run "invalid_name_rejected" { + command = plan + + variables { + name = "invalid policy name" + } + + expect_failures = [ + var.name, + ] +} + +run "invalid_effect_rejected" { + command = plan + + variables { + policy_statements = [{ + effect = "Permit" + actions = ["s3:GetObject"] + resources = ["arn:aws:s3:::example-bucket/*"] + }] + } + + expect_failures = [ + var.policy_statements, + ] +} + +run "not_action_and_not_resource_supported" { + command = plan + + variables { + policy_statements = [{ + effect = "Deny" + not_actions = ["iam:GetUser"] + not_resources = ["arn:aws:iam::123456789012:user/break-glass"] + }] + } + + assert { + condition = var.policy_statements[0].not_actions == ["iam:GetUser"] + error_message = "Structured statements should accept NotAction values." + } + + assert { + condition = var.policy_statements[0].not_resources == ["arn:aws:iam::123456789012:user/break-glass"] + error_message = "Structured statements should accept NotResource values." + } +} + +run "action_and_not_action_rejected" { + command = plan + + variables { + policy_statements = [{ + actions = ["s3:GetObject"] + not_actions = ["s3:DeleteObject"] + resources = ["arn:aws:s3:::example-bucket/*"] + }] + } + + expect_failures = [ + var.policy_statements, + ] +} + +run "resource_and_not_resource_rejected" { + command = plan + + variables { + policy_statements = [{ + actions = ["s3:GetObject"] + resources = ["arn:aws:s3:::example-bucket/*"] + not_resources = ["arn:aws:s3:::example-bucket/private/*"] + }] + } + + expect_failures = [ + var.policy_statements, + ] +} + +run "wildcard_path_rejected" { + command = plan + + variables { + path = "/application*/" + } + + expect_failures = [ + var.path, + ] +} diff --git a/security/iam_policy/variables.tf b/security/iam_policy/variables.tf new file mode 100644 index 0000000..bb57ed5 --- /dev/null +++ b/security/iam_policy/variables.tf @@ -0,0 +1,92 @@ +################################################################################ +# General +################################################################################ + +variable "name" { + type = string + description = "The name of the customer-managed IAM policy." + + validation { + condition = can(regex("^[A-Za-z0-9+=,.@_-]{1,128}$", var.name)) + error_message = "The name must contain 1-128 letters, numbers, plus, equals, comma, period, at, underscore, or hyphen characters." + } +} + +variable "description" { + type = string + description = "The description of the customer-managed IAM policy. AWS does not allow this description to be changed after creation." + default = null + + validation { + condition = var.description == null || length(var.description) <= 1000 + error_message = "The description must be 1000 characters or fewer." + } +} + +variable "path" { + type = string + description = "The path for the customer-managed IAM policy." + default = "/" + + validation { + condition = length(var.path) <= 512 && !strcontains(var.path, "*") && (var.path == "/" || can(regex("^/.*/$", var.path))) + error_message = "The path must be 512 characters or fewer, start and end with '/', and not contain '*'." + } +} + +variable "tags" { + type = map(string) + description = "A map of tags to assign to the IAM policy." + default = {} +} + +################################################################################ +# Policy Document +################################################################################ + +variable "policy_statements" { + type = list(object({ + sid = optional(string) + effect = optional(string, "Allow") + actions = optional(list(string), []) + not_actions = optional(list(string), []) + resources = optional(list(string), []) + not_resources = optional(list(string), []) + conditions = optional(list(object({ + test = string + variable = string + values = list(string) + })), []) + })) + description = "Structured IAM policy statements. Ignored when policy_json is provided." + default = [] + + validation { + condition = alltrue([ + for statement in var.policy_statements : ( + contains(["Allow", "Deny"], statement.effect) && + ((length(statement.actions) > 0) != (length(statement.not_actions) > 0)) && + ((length(statement.resources) > 0) != (length(statement.not_resources) > 0)) && + alltrue([for condition in statement.conditions : length(condition.values) > 0]) + ) + ]) + error_message = "Each policy statement must use Allow or Deny, set exactly one of actions or not_actions, set exactly one of resources or not_resources, and include at least one value per condition." + } +} + +variable "policy_json" { + type = string + description = "A complete IAM policy JSON document. When provided, this overrides policy_statements." + default = null + + validation { + condition = var.policy_json == null || (can(jsondecode(var.policy_json)) && can(jsondecode(var.policy_json).Statement)) + error_message = "The policy_json must be valid JSON with a Statement property." + } +} + +variable "region" { + type = string + description = "AWS region used to configure the provider. IAM resources are global. When null, the provider's configured region is used." + default = null +} diff --git a/security/iam_policy/versions.tf b/security/iam_policy/versions.tf new file mode 100644 index 0000000..f2ea0cd --- /dev/null +++ b/security/iam_policy/versions.tf @@ -0,0 +1,12 @@ +terraform { + required_version = ">= 1.10.0" + + cloud {} + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.0" + } + } +}