From f532aa6059bfef1bfbda928f854462609879cd1b Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Fri, 11 Sep 2026 11:22:28 -0400 Subject: [PATCH] feat(terraform): complete dev environment adoption (SH-300) --- scripts/check-terraform-import-plan.py | 129 +---------- scripts/terraform_import_plan_resources.py | 1 - scripts/test-terraform-import-plan-check.py | 215 +++++------------- terraform/README.md | 43 ++-- terraform/live/dev/main.tf | 5 +- .../live/modules/environment-owned/main.tf | 92 +++----- .../modules/environment-owned/variables.tf | 2 +- 7 files changed, 110 insertions(+), 377 deletions(-) diff --git a/scripts/check-terraform-import-plan.py b/scripts/check-terraform-import-plan.py index 28bd0dce..1e65685f 100644 --- a/scripts/check-terraform-import-plan.py +++ b/scripts/check-terraform-import-plan.py @@ -18,17 +18,11 @@ from terraform_import_plan_resources import ( BUCKET_POLICY_ADDRESS = "module.environment_owned.aws_s3_bucket_policy.site" BUCKET_ADDRESS = "module.environment_owned.aws_s3_bucket.site" -DEPLOY_POLICY_ADDRESS = ( - "module.environment_owned.aws_iam_role_policy.github_deploy" -) DISTRIBUTION_ADDRESS = ( "module.environment_owned.aws_cloudfront_distribution.site" ) ROLE_ADDRESS = "module.environment_owned.aws_iam_role.github_deploy" -TAG_UPDATE_ADDRESSES = CONTROLLED_UPDATE_ADDRESSES - { - BUCKET_POLICY_ADDRESS, - DEPLOY_POLICY_ADDRESS, -} +TAG_UPDATE_ADDRESSES = CONTROLLED_UPDATE_ADDRESSES - {BUCKET_POLICY_ADDRESS} OWNERSHIP_TAGS = { "Environment": None, "ManagedBy": "terraform", @@ -255,113 +249,6 @@ def _expected_bucket_policy(environment: str, distribution_id: str) -> dict[str, ) -def _expected_pre_adoption_deploy_policy( - environment: str, - distribution_id: str, -) -> dict[str, Any]: - config = ENVIRONMENT_CONFIG[environment] - bucket_arn = f"arn:aws:s3:::{config['bucket_name']}" - distribution_arn = ( - f"arn:aws:cloudfront::396287094661:distribution/{distribution_id}" - ) - statements: list[dict[str, Any]] = [] - if environment == "dev": - statements.append( - { - "Sid": "AssumeCdkBootstrapRoles", - "Effect": "Allow", - "Action": "sts:AssumeRole", - "Resource": "arn:aws:iam::396287094661:role/cdk-hnb659fds-*", - } - ) - statements.extend( - [ - { - "Sid": "DescribeStack", - "Effect": "Allow", - "Action": "cloudformation:DescribeStacks", - "Resource": ( - "arn:aws:cloudformation:us-east-1:396287094661:stack/" - f"{config['cloudformation_stack_name']}/*" - ), - }, - { - "Effect": "Allow", - "Action": [ - "s3:Abort*", - "s3:DeleteObject*", - "s3:GetBucket*", - "s3:GetObject*", - "s3:List*", - "s3:PutObject", - "s3:PutObjectLegalHold", - "s3:PutObjectRetention", - "s3:PutObjectTagging", - "s3:PutObjectVersionTagging", - ], - "Resource": [bucket_arn, f"{bucket_arn}/*"], - }, - { - "Sid": "InvalidateDistribution", - "Effect": "Allow", - "Action": [ - "cloudfront:CreateInvalidation", - "cloudfront:GetInvalidation", - ], - "Resource": distribution_arn, - }, - ] - ) - return _canonical({"Version": "2012-10-17", "Statement": statements}) - - -def _expected_deploy_policy(environment: str, distribution_id: str) -> dict[str, Any]: - bucket = ENVIRONMENT_CONFIG[environment]["bucket_name"] - bucket_arn = f"arn:aws:s3:::{bucket}" - distribution_arn = ( - f"arn:aws:cloudfront::396287094661:distribution/{distribution_id}" - ) - return _canonical( - { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "ReadDeploymentBucket", - "Effect": "Allow", - "Action": [ - "s3:GetBucketLocation", - "s3:GetBucketVersioning", - "s3:ListBucket", - "s3:ListBucketVersions", - ], - "Resource": bucket_arn, - }, - { - "Sid": "PublishAndRollbackSiteObjects", - "Effect": "Allow", - "Action": [ - "s3:DeleteObject", - "s3:DeleteObjectVersion", - "s3:GetObject", - "s3:GetObjectVersion", - "s3:PutObject", - ], - "Resource": f"{bucket_arn}/*", - }, - { - "Sid": "InvalidateDistribution", - "Effect": "Allow", - "Action": [ - "cloudfront:CreateInvalidation", - "cloudfront:GetInvalidation", - ], - "Resource": distribution_arn, - }, - ], - } - ) - - def _validate_tag_update( address: str, before: dict[str, Any], @@ -432,16 +319,10 @@ def _validate_policy_update( f"{address}: cannot verify policy without the pinned distribution ID" ) return violations - expected_before = ( - _expected_pre_adoption_bucket_policy(environment, distribution_id) - if address == BUCKET_POLICY_ADDRESS - else _expected_pre_adoption_deploy_policy(environment, distribution_id) - ) - expected_after = ( - _expected_bucket_policy(environment, distribution_id) - if address == BUCKET_POLICY_ADDRESS - else _expected_deploy_policy(environment, distribution_id) + expected_before = _expected_pre_adoption_bucket_policy( + environment, distribution_id ) + expected_after = _expected_bucket_policy(environment, distribution_id) if before_policy is not None and before_policy != expected_before: violations.append(f"{address}: pre-adoption policy semantics are not exact") if after_policy is not None and after_policy != expected_after: @@ -467,7 +348,7 @@ def _validate_controlled_update( return [*violations, f"{address}: controlled update requires before/after objects"] if address in TAG_UPDATE_ADDRESSES: violations.extend(_validate_tag_update(address, before, after, environment)) - elif address in {BUCKET_POLICY_ADDRESS, DEPLOY_POLICY_ADDRESS}: + elif address == BUCKET_POLICY_ADDRESS: violations.extend( _validate_policy_update( address, diff --git a/scripts/terraform_import_plan_resources.py b/scripts/terraform_import_plan_resources.py index 5d558b02..a5b6db08 100644 --- a/scripts/terraform_import_plan_resources.py +++ b/scripts/terraform_import_plan_resources.py @@ -45,7 +45,6 @@ CONTROLLED_UPDATE_ADDRESSES = frozenset( "module.environment_owned.aws_cloudfront_distribution.site", "module.environment_owned.aws_cloudfront_function.spa_rewrite", "module.environment_owned.aws_iam_role.github_deploy", - "module.environment_owned.aws_iam_role_policy.github_deploy", } ) diff --git a/scripts/test-terraform-import-plan-check.py b/scripts/test-terraform-import-plan-check.py index 5046ccad..c122ae19 100644 --- a/scripts/test-terraform-import-plan-check.py +++ b/scripts/test-terraform-import-plan-check.py @@ -27,7 +27,7 @@ BUCKET = "module.environment_owned.aws_s3_bucket.site" DEPLOY_POLICY = "module.environment_owned.aws_iam_role_policy.github_deploy" ROLE = "module.environment_owned.aws_iam_role.github_deploy" DISTRIBUTION = "module.environment_owned.aws_cloudfront_distribution.site" -TAG_ADDRESSES = CONTROLLED_UPDATE_ADDRESSES - {BUCKET_POLICY, DEPLOY_POLICY} +TAG_ADDRESSES = CONTROLLED_UPDATE_ADDRESSES - {BUCKET_POLICY} def import_id(environment: str, address: str) -> str: @@ -111,110 +111,6 @@ def bucket_policy(environment: str) -> dict[str, Any]: } -def pre_adoption_deploy_policy(environment: str) -> dict[str, Any]: - config = ENVIRONMENT_CONFIG[environment] - bucket_arn = f"arn:aws:s3:::{config['bucket_name']}" - distribution_arn = ( - "arn:aws:cloudfront::396287094661:distribution/" - f"{distribution_id(environment)}" - ) - statements: list[dict[str, Any]] = [] - if environment == "dev": - statements.append( - { - "Sid": "AssumeCdkBootstrapRoles", - "Effect": "Allow", - "Action": "sts:AssumeRole", - "Resource": "arn:aws:iam::396287094661:role/cdk-hnb659fds-*", - } - ) - statements.extend( - [ - { - "Sid": "DescribeStack", - "Effect": "Allow", - "Action": "cloudformation:DescribeStacks", - "Resource": ( - "arn:aws:cloudformation:us-east-1:396287094661:stack/" - f"{config['cloudformation_stack_name']}/*" - ), - }, - { - "Effect": "Allow", - "Action": [ - "s3:Abort*", - "s3:DeleteObject*", - "s3:GetBucket*", - "s3:GetObject*", - "s3:List*", - "s3:PutObject", - "s3:PutObjectLegalHold", - "s3:PutObjectRetention", - "s3:PutObjectTagging", - "s3:PutObjectVersionTagging", - ], - "Resource": [bucket_arn, f"{bucket_arn}/*"], - }, - { - "Sid": "InvalidateDistribution", - "Effect": "Allow", - "Action": [ - "cloudfront:CreateInvalidation", - "cloudfront:GetInvalidation", - ], - "Resource": distribution_arn, - }, - ] - ) - return {"Version": "2012-10-17", "Statement": statements} - - -def deploy_policy(environment: str) -> dict[str, Any]: - bucket = ENVIRONMENT_CONFIG[environment]["bucket_name"] - bucket_arn = f"arn:aws:s3:::{bucket}" - distribution_arn = ( - "arn:aws:cloudfront::396287094661:distribution/" - f"{distribution_id(environment)}" - ) - return { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "ReadDeploymentBucket", - "Effect": "Allow", - "Action": [ - "s3:GetBucketLocation", - "s3:GetBucketVersioning", - "s3:ListBucket", - "s3:ListBucketVersions", - ], - "Resource": bucket_arn, - }, - { - "Sid": "PublishAndRollbackSiteObjects", - "Effect": "Allow", - "Action": [ - "s3:DeleteObject", - "s3:DeleteObjectVersion", - "s3:GetObject", - "s3:GetObjectVersion", - "s3:PutObject", - ], - "Resource": f"{bucket_arn}/*", - }, - { - "Sid": "InvalidateDistribution", - "Effect": "Allow", - "Action": [ - "cloudfront:CreateInvalidation", - "cloudfront:GetInvalidation", - ], - "Resource": distribution_arn, - }, - ], - } - - def tag_change(environment: str, address: str) -> dict[str, Any]: manager = { "HcpTerraformWorkspace": ENVIRONMENT_CONFIG[environment]["workspace_name"] @@ -250,20 +146,12 @@ def tag_change(environment: str, address: str) -> dict[str, Any]: def policy_change(environment: str, address: str) -> dict[str, Any]: - before_policy = ( - pre_adoption_bucket_policy(environment) - if address == BUCKET_POLICY - else pre_adoption_deploy_policy(environment) - ) - after_policy = ( - bucket_policy(environment) - if address == BUCKET_POLICY - else deploy_policy(environment) - ) + if address != BUCKET_POLICY: + raise AssertionError(f"{address} is not a reviewed policy update") return { "actions": ["update"], - "before": {"policy": json.dumps(before_policy)}, - "after": {"policy": json.dumps(after_policy)}, + "before": {"policy": json.dumps(pre_adoption_bucket_policy(environment))}, + "after": {"policy": json.dumps(bucket_policy(environment))}, } @@ -395,9 +283,9 @@ class ImportPlanCheckerTests(unittest.TestCase): ) self.assertEqual(["dev"], live_roots) - def test_dev_root_pins_import_phase_in_code(self) -> None: + def test_dev_root_pins_adoption_complete_in_code(self) -> None: source = (REPOSITORY / "terraform/live/dev/main.tf").read_text(encoding="utf-8") - self.assertRegex(source, r"\n\s+adoption_complete\s+= false\n") + self.assertRegex(source, r"\n\s+adoption_complete\s+= true\n") self.assertRegex(source, r"adoption_complete\s+= local\.adoption_complete") self.assertNotIn('variable "adoption_complete"', source) for root_file in ("main.tf", "imports.tf", "outputs.tf", "providers.tf", "versions.tf"): @@ -564,54 +452,53 @@ class ImportPlanCheckerTests(unittest.TestCase): with self.subTest(mutation=mutation): self.assert_fails(plan, "dev", BUCKET_POLICY) - def test_deploy_policy_rejects_resource_action_and_extra_statement(self) -> None: - for mutation in ("resource", "action", "extra"): - plan = make_plan( - "staging", - mode="controlled", - controlled_updates={DEPLOY_POLICY}, - ) - policy = copy.deepcopy(deploy_policy("staging")) - if mutation == "resource": - policy["Statement"][0]["Resource"] = "*" - elif mutation == "action": - policy["Statement"][0]["Action"].append("iam:PassRole") - else: - policy["Statement"].append( - { - "Sid": "Extra", - "Effect": "Allow", - "Action": "s3:*", - "Resource": "*", - } - ) - resource(plan, DEPLOY_POLICY)["change"]["after"]["policy"] = json.dumps( - policy - ) - with self.subTest(mutation=mutation): - self.assert_fails(plan, "staging", DEPLOY_POLICY) + def test_github_deploy_policy_stays_byte_identical(self) -> None: + source = ( + REPOSITORY / "terraform/live/modules/environment-owned/main.tf" + ).read_text(encoding="utf-8") + document = source.split('data "aws_iam_policy_document" "github_deploy" {', 1)[1] + document = document.split("resource ", 1)[0] + self.assertNotIn("var.adoption_complete", document) + self.assertIn("AssumeCdkBootstrapRoles", document) + self.assertIn("DescribeStack", document) + self.assertNotIn("ReadDeploymentBucket", document) + self.assertNotIn("PublishAndRollbackSiteObjects", document) + self.assertNotIn( + "module.environment_owned.aws_iam_role_policy.github_deploy", + CONTROLLED_UPDATE_ADDRESSES, + ) + + def test_deploy_policy_is_not_eligible_for_controlled_update(self) -> None: + plan = make_plan("dev", mode="controlled", controlled_updates=set()) + self.assert_fails(plan, "dev", DEPLOY_POLICY) + plan = make_plan("dev", mode="controlled", controlled_updates=set()) + resource(plan, DEPLOY_POLICY)["change"] = { + "actions": ["update"], + "before": {"policy": "{}"}, + "after": {"policy": '{"Version":"2012-10-17"}'}, + } + self.assert_fails(plan, "dev", DEPLOY_POLICY) def test_policy_updates_require_exact_pre_adoption_state(self) -> None: for environment in REQUIRED_RESOURCES: - for address in (BUCKET_POLICY, DEPLOY_POLICY): - plan = make_plan( - environment, - mode="controlled", - controlled_updates={address}, - ) - change = resource(plan, address)["change"] - before = json.loads(change["before"]["policy"]) - before["Statement"].append( - { - "Sid": "UnexpectedDrift", - "Effect": "Deny", - "Action": "*", - "Resource": "*", - } - ) - change["before"]["policy"] = json.dumps(before) - with self.subTest(environment=environment, address=address): - self.assert_fails(plan, environment, address) + plan = make_plan( + environment, + mode="controlled", + controlled_updates={BUCKET_POLICY}, + ) + change = resource(plan, BUCKET_POLICY)["change"] + before = json.loads(change["before"]["policy"]) + before["Statement"].append( + { + "Sid": "UnexpectedDrift", + "Effect": "Deny", + "Action": "*", + "Resource": "*", + } + ) + change["before"]["policy"] = json.dumps(before) + with self.subTest(environment=environment): + self.assert_fails(plan, environment, BUCKET_POLICY) def test_controlled_update_rejects_unknown_and_replace_paths(self) -> None: for field, value in ( diff --git a/terraform/README.md b/terraform/README.md index e0b2b4ab..190dd53b 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -4,11 +4,11 @@ This tree adopts the existing Sea Haven SHOC frontend dev hosting resources into HCP Terraform without recreating them. It mirrors the backend adoption (`shoc-backend` #94, #98, #99, #102) and lands in three PRs: -| PR | Branch | Change | -| --- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -| A | `feature/frontend-terraform-adoption` | This PR. Dev root with `adoption_complete = false`, import guard, CDK retain mode, push-to-`dev` deploy off. | -| B | `feature/terraform-dev-adoption` | `adoption_complete = true`: ownership tags, bucket policy drops the auto-delete grant, CloudFormation detaches. | -| C | `feature/terraform-dev-content-cd` | Content CD through Terraform: release prefixes, pointer object, origin group, invalidation action, rollback. | +| PR | Branch | Change | +| --- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| A | `feature/frontend-terraform-adoption` | Merged. Dev root with `adoption_complete = false`, import guard, CDK retain mode, push-to-`dev` deploy off. | +| B | `feature/terraform-dev-adoption` | This PR. `adoption_complete = true`: ownership tags, bucket policy drops the auto-delete grant. | +| C | `feature/terraform-dev-content-cd` | Content CD through Terraform: release prefixes, pointer object, origin group, invalidation action, rollback. | Creating these files, formatting them, initializing with `-backend=false`, and validating them does not authorize an AWS, HCP Terraform, GitHub, @@ -45,9 +45,8 @@ before the first release after any Terraform merge: `>= 1.9.0, < 2.0.0`; CI validates with `1.16.0`). - Dynamic AWS credentials only: environment variables `TFC_AWS_PROVIDER_AUTH=true`, `TFC_AWS_PLAN_ROLE_ARN`, and - `TFC_AWS_APPLY_ROLE_ARN` pointing at the `seahaven-org-baseline` roles - `hcptf-shoc-frontend-new-dev-plan` and `hcptf-shoc-frontend-new-dev`. No - access keys. + `TFC_AWS_APPLY_ROLE_ARN` pointing at `hcptf-shoc-frontend-new-dev-plan` + and `hcptf-shoc-frontend-new-dev`. No access keys. - **No** `adoption_complete` workspace variable. The dev root pins it in code (`local.adoption_complete`) so the value under review is the value that applies. `scripts/test-terraform-import-plan-check.py` fails if a `variable` @@ -86,7 +85,6 @@ The following remain outside state: - `CDKToolkit` resources and CDK metadata - the S3 auto-delete custom resource, its provider Lambda and role - the HCP plan/apply roles and the deploy-role permissions boundary - (`seahaven-org-baseline` owns them) ## Exact live inventory (dev) @@ -130,7 +128,7 @@ If read-back after that deploy differs from the root in any other way, update the root to the observed value and prove a zero-change import plan. Do not approve drift through the controlled-update checker. -## Phase 1: import-first adoption (this PR) +## Phase 1: import-first adoption (merged) Each step is gated. State the impact, get the go, act, read back, record. @@ -177,7 +175,7 @@ Each step is gated. State the impact, get the go, act, read back, record. After Phase 1 CloudFormation still owns every resource. Terraform holds state for them and nothing else. -## Phase 2: controlled ownership transfer (PR B) +## Phase 2: controlled ownership transfer (this PR) PR B pins `adoption_complete = true`. The controlled apply may update only: @@ -189,15 +187,19 @@ PR B pins `adoption_complete = true`. The controlled apply may update only: - `module.environment_owned.aws_iam_role.github_deploy` (tags) The OAC, both Route 53 records, and the deploy inline policy must be no-op. -PR B keeps the post-adoption inline policy byte-identical to live so the -policy address does not appear in the plan. Run the checker with one -`--allow-update-address` per updating address; it rejects unused allowlist -entries, unknown values, and replacements. +PR B keeps the GitHub deploy inline policy byte-identical to live so +`aws_iam_role_policy.github_deploy` does not appear in the plan. Run the +checker with one `--allow-update-address` per updating address; it rejects +unused allowlist entries, unknown values, and replacements: -Dependency: `hcptf-shoc-frontend-new-dev` currently lacks -`cloudfront:UpdateDistribution` and `cloudfront:UpdateFunction`. Codify the -expansion in `seahaven-org-baseline` (cross-family plus security review) and -deploy it before the controlled apply. +```bash +python3 scripts/check-terraform-import-plan.py plan.json --environment dev \ + --allow-update-address module.environment_owned.aws_s3_bucket.site \ + --allow-update-address module.environment_owned.aws_s3_bucket_policy.site \ + --allow-update-address module.environment_owned.aws_cloudfront_distribution.site \ + --allow-update-address module.environment_owned.aws_cloudfront_function.spa_rewrite \ + --allow-update-address module.environment_owned.aws_iam_role.github_deploy +``` After the apply and a no-op plan, deploy the same reviewed CDK SHA with `--parameters ManageSiteInfrastructure=false`. Expect `DELETE_SKIPPED` on the @@ -205,6 +207,9 @@ After the apply and a no-op plan, deploy the same reviewed CDK SHA with `ManageSiteInfrastructure=true` again after that. See [`infra/cdk/README.md`](../infra/cdk/README.md). +Confirm `dev.seahaven.com` still serves and that a manual `workflow_dispatch` +of `deploy.yml` can still upload with the unchanged GitHub content policy. + ## Phase 3: content CD through Terraform (PR C) Summary only; PR C carries the full design. GitHub builds and uploads to an diff --git a/terraform/live/dev/main.tf b/terraform/live/dev/main.tf index 37ed5472..f11f75d4 100644 --- a/terraform/live/dev/main.tf +++ b/terraform/live/dev/main.tf @@ -1,7 +1,6 @@ locals { - # Import-first phase. Pinned in code, never a workspace variable: the - # controlled ownership transfer flips this to true in its own reviewed PR. - adoption_complete = false + # Controlled ownership transfer. Pinned in code, never a workspace variable. + adoption_complete = true environment = "dev" workspace_name = "shoc-frontend-new-dev" diff --git a/terraform/live/modules/environment-owned/main.tf b/terraform/live/modules/environment-owned/main.tf index 3ddb5fdb..6a0e2a61 100644 --- a/terraform/live/modules/environment-owned/main.tf +++ b/terraform/live/modules/environment-owned/main.tf @@ -109,8 +109,11 @@ data "aws_iam_policy_document" "github_deploy_assume" { } data "aws_iam_policy_document" "github_deploy" { + # Byte-identical to the live GitHub content policy through Phase 2 so + # aws_iam_role_policy.github_deploy stays no-op. Phase 3 replaces this + # with the release-prefix policy. dynamic "statement" { - for_each = !var.adoption_complete && var.environment == "dev" ? [1] : [] + for_each = var.environment == "dev" ? [1] : [] content { sid = "AssumeCdkBootstrapRoles" @@ -120,72 +123,31 @@ data "aws_iam_policy_document" "github_deploy" { } } - dynamic "statement" { - for_each = var.adoption_complete ? [] : [1] - - content { - sid = "DescribeStack" - effect = "Allow" - actions = ["cloudformation:DescribeStacks"] - resources = ["arn:aws:cloudformation:${var.aws_region}:${var.aws_account_id}:stack/${var.cloudformation_stack_name}/*"] - } + statement { + sid = "DescribeStack" + effect = "Allow" + actions = ["cloudformation:DescribeStacks"] + resources = ["arn:aws:cloudformation:${var.aws_region}:${var.aws_account_id}:stack/${var.cloudformation_stack_name}/*"] } - dynamic "statement" { - for_each = var.adoption_complete ? [] : [1] - - content { - effect = "Allow" - actions = [ - "s3:Abort*", - "s3:DeleteObject*", - "s3:GetBucket*", - "s3:GetObject*", - "s3:List*", - "s3:PutObject", - "s3:PutObjectLegalHold", - "s3:PutObjectRetention", - "s3:PutObjectTagging", - "s3:PutObjectVersionTagging", - ] - resources = [ - local.bucket_arn, - "${local.bucket_arn}/*", - ] - } - } - - dynamic "statement" { - for_each = var.adoption_complete ? [1] : [] - - content { - sid = "ReadDeploymentBucket" - effect = "Allow" - actions = [ - "s3:GetBucketLocation", - "s3:GetBucketVersioning", - "s3:ListBucket", - "s3:ListBucketVersions", - ] - resources = [local.bucket_arn] - } - } - - dynamic "statement" { - for_each = var.adoption_complete ? [1] : [] - - content { - sid = "PublishAndRollbackSiteObjects" - effect = "Allow" - actions = [ - "s3:DeleteObject", - "s3:DeleteObjectVersion", - "s3:GetObject", - "s3:GetObjectVersion", - "s3:PutObject", - ] - resources = ["${local.bucket_arn}/*"] - } + statement { + effect = "Allow" + actions = [ + "s3:Abort*", + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + ] + resources = [ + local.bucket_arn, + "${local.bucket_arn}/*", + ] } statement { diff --git a/terraform/live/modules/environment-owned/variables.tf b/terraform/live/modules/environment-owned/variables.tf index 6a135679..69434ebd 100644 --- a/terraform/live/modules/environment-owned/variables.tf +++ b/terraform/live/modules/environment-owned/variables.tf @@ -10,7 +10,7 @@ variable "environment" { variable "adoption_complete" { type = bool - description = "Switches only ownership tags and the deploy policy to their adopted values." + description = "Switches ownership tags and drops the auto-delete helper grant from the bucket policy. The GitHub deploy inline policy stays byte-identical to live until the content-CD PR." default = false }