From 650c84baa1ee8b0aa9d5507559c8c01c2adef4a5 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:07:32 +0000 Subject: [PATCH] feat(iam): import hcptf roles into app Terraform (PLAT-146) (#206) * feat(iam): import hcptf roles into app Terraform (PLAT-146) Move the existing hcptf pair into this repo so app Terraform owns prod IAM after the substrate handoff. * fix(iam): attach scoped IAM as a managed policy (PLAT-146) The apply role's services inline already uses 6894 of 10240 bytes, so a second inline policy cannot hold scoped IAM. * fix(iam): add apply-role IAM list permissions (PLAT-146) IamReadOnly omitted ListRoleTags and ListInstanceProfilesForRole needed after detaching the substrate guardrail. * fix(iam): allow ListPolicyTags on scoped IAM policy (PLAT-146) tagged managed scoped IAM is refreshed with ListPolicyTags; apply IamReadOnly and plan-refresh omitted it. --- terraform/hcp_iam.tf | 877 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 877 insertions(+) create mode 100644 terraform/hcp_iam.tf diff --git a/terraform/hcp_iam.tf b/terraform/hcp_iam.tf new file mode 100644 index 0000000..5b3c2fe --- /dev/null +++ b/terraform/hcp_iam.tf @@ -0,0 +1,877 @@ +# HCP plan/apply roles imported from seahaven-terraform-substrate (PLAT-146). +# Import, do not recreate. Role names stay hcptf-procurement-ingest / hcptf-procurement-ingest-plan. +# +# Live seahaven-hcptf-iam-management DenySelfMutation blocks DetachRolePolicy +# and PutRolePolicy on hcptf-* (including this role). Import apply sequence: +# 1. seahaven-org-baseline scripts/create-hcptf-bootstrap-roles.sh +# --account prod --allow-workspace procurement-ingest-prod +# 2. Point this workspace's TFC_AWS_* at hcptf-bootstrap / +# hcptf-bootstrap-plan (workspace vars, never a project set). +# 3. One Manual apply (import + detach seahaven-hcptf-iam-management + +# put scoped IAM as a customer-managed policy). +# 4. Point TFC_AWS_* back at hcptf-procurement-ingest / hcptf-procurement-ingest-plan. +# 5. Re-run the script without --allow-workspace to pin trust back to +# iam-bootstrap-prod only. +# seahaven-lambda-execution-boundary remains seahaven-lambda-execution-boundary-procurement-ingest. + +import { + to = aws_iam_role.hcptf_apply + id = "hcptf-procurement-ingest" +} + +import { + to = aws_iam_role.hcptf_plan + id = "hcptf-procurement-ingest-plan" +} + +import { + to = aws_iam_role_policy.hcptf_apply_services + id = "hcptf-procurement-ingest:procurement-ingest-services" +} + +import { + to = aws_iam_role_policy.hcptf_plan_refresh + id = "hcptf-procurement-ingest-plan:procurement-ingest-plan-refresh" +} + +import { + to = aws_iam_role_policy_attachments_exclusive.hcptf_apply + id = "hcptf-procurement-ingest" +} + +import { + to = aws_iam_role_policy_attachment.hcptf_plan_viewonly + id = "hcptf-procurement-ingest-plan/arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" +} + +import { + to = aws_iam_role_policy_attachments_exclusive.hcptf_plan + id = "hcptf-procurement-ingest-plan" +} + +data "aws_iam_policy_document" "hcptf_apply_trust" { + statement { + sid = "HcpApply" + effect = "Allow" + actions = ["sts:AssumeRoleWithWebIdentity"] + + principals { + type = "Federated" + identifiers = ["arn:aws:iam::${local.account_id}:oidc-provider/app.terraform.io"] + } + + condition { + test = "StringEquals" + variable = "app.terraform.io:aud" + values = ["aws.workload.identity"] + } + + condition { + test = "StringEquals" + variable = "app.terraform.io:sub" + values = [ + "organization:seahaven:project:seahaven-prod:workspace:procurement-ingest-prod:run_phase:apply", + ] + } + } +} + +data "aws_iam_policy_document" "hcptf_plan_trust" { + statement { + sid = "HcpPlan" + effect = "Allow" + actions = ["sts:AssumeRoleWithWebIdentity"] + + principals { + type = "Federated" + identifiers = ["arn:aws:iam::${local.account_id}:oidc-provider/app.terraform.io"] + } + + condition { + test = "StringEquals" + variable = "app.terraform.io:aud" + values = ["aws.workload.identity"] + } + + condition { + test = "StringEquals" + variable = "app.terraform.io:sub" + values = [ + "organization:seahaven:project:seahaven-prod:workspace:procurement-ingest-prod:run_phase:plan", + ] + } + } +} + +data "aws_iam_policy_document" "hcptf_scoped_iam" { + statement { + sid = "DenyCreatePolicy" + effect = "Deny" + actions = ["iam:CreatePolicy", "iam:CreatePolicyVersion"] + resources = ["*"] + } + + statement { + sid = "CreateExecRoleWithBoundary" + effect = "Allow" + actions = ["iam:CreateRole"] + resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/po-*", + "arn:aws:iam::${local.account_id}:role/tf-managed/workorder-*", + "arn:aws:iam::${local.account_id}:role/tf-managed/procurement-api"] + + condition { + test = "StringLike" + variable = "iam:PermissionsBoundary" + values = [ + "arn:aws:iam::${local.account_id}:policy/tf-managed/po-*", + "arn:aws:iam::${local.account_id}:policy/tf-managed/workorder-*", + "arn:aws:iam::${local.account_id}:policy/tf-managed/procurement-api", + "arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary-procurement-ingest" + ] + } + } + + statement { + sid = "MutateExecRoleWithBoundary" + effect = "Allow" + actions = [ + "iam:AttachRolePolicy", + "iam:PutRolePolicy", + "iam:PutRolePermissionsBoundary", + ] + resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/po-*", + "arn:aws:iam::${local.account_id}:role/tf-managed/workorder-*", + "arn:aws:iam::${local.account_id}:role/tf-managed/procurement-api"] + + condition { + test = "StringLike" + variable = "iam:PermissionsBoundary" + values = [ + "arn:aws:iam::${local.account_id}:policy/tf-managed/po-*", + "arn:aws:iam::${local.account_id}:policy/tf-managed/workorder-*", + "arn:aws:iam::${local.account_id}:policy/tf-managed/procurement-api", + "arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary-procurement-ingest" + ] + } + } + + statement { + sid = "WriteExecRoles" + effect = "Allow" + actions = [ + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:DetachRolePolicy", + "iam:TagRole", + "iam:UntagRole", + "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + "iam:UpdateRoleDescription", + ] + resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/po-*", + "arn:aws:iam::${local.account_id}:role/tf-managed/workorder-*", + "arn:aws:iam::${local.account_id}:role/tf-managed/procurement-api"] + } + + statement { + sid = "PassExecRolesToLambda" + effect = "Allow" + actions = ["iam:PassRole"] + resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/po-*", + "arn:aws:iam::${local.account_id}:role/tf-managed/workorder-*", + "arn:aws:iam::${local.account_id}:role/tf-managed/procurement-api"] + + condition { + test = "StringEquals" + variable = "iam:PassedToService" + values = ["lambda.amazonaws.com"] + } + } + + statement { + sid = "IamReadOnly" + effect = "Allow" + actions = [ + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListAttachedRolePolicies", + "iam:ListInstanceProfilesForRole", + "iam:ListPolicies", + "iam:ListPolicyTags", + "iam:ListPolicyVersions", + "iam:ListRolePolicies", + "iam:ListRoles", + "iam:ListRoleTags", + ] + resources = ["*"] + } + + statement { + sid = "DenySelfMutation" + effect = "Deny" + actions = [ + "iam:AttachRolePolicy", + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:DeleteRolePermissionsBoundary", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:PutRolePermissionsBoundary", + "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + "iam:UpdateRoleDescription", + ] + resources = [ + "arn:aws:iam::${local.account_id}:role/hcptf-*", + "arn:aws:iam::${local.account_id}:role/github-cfn-execution-role", + "arn:aws:iam::${local.account_id}:role/githubdeploy-*", + "arn:aws:iam::${local.account_id}:role/cdk-hnb659fds-*", + "arn:aws:iam::${local.account_id}:role/OrganizationAccountAccessRole", + "arn:aws:iam::${local.account_id}:role/seahaven-*", + ] + } + + statement { + sid = "DenyBoundaryTampering" + effect = "Deny" + actions = [ + "iam:DeleteRolePermissionsBoundary", + "iam:DeleteUserPermissionsBoundary", + ] + resources = [ + "arn:aws:iam::${local.account_id}:role/*", + "arn:aws:iam::${local.account_id}:user/*", + ] + } + + statement { + sid = "DenyBoundaryPolicyEdit" + effect = "Deny" + actions = [ + "iam:CreatePolicyVersion", + "iam:DeletePolicy", + "iam:DeletePolicyVersion", + "iam:SetDefaultPolicyVersion", + ] + resources = ["arn:aws:iam::${local.account_id}:policy/seahaven-*"] + } +} + +resource "aws_iam_role_policy" "hcptf_apply_services" { + name = "procurement-ingest-services" + role = aws_iam_role.hcptf_apply.id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "lambda:*", + ] + Resource = [ + "arn:aws:lambda:us-east-1:${local.account_id}:function:po-*", + "arn:aws:lambda:us-east-1:${local.account_id}:function:workorder-*", + "arn:aws:lambda:us-east-1:${local.account_id}:function:procurement-api", + ] + Effect = "Allow" + Sid = "LambdaAll" + }, + { + Action = [ + "lambda:GetEventSourceMapping", + "lambda:ListTags", + "lambda:TagResource", + "lambda:UntagResource", + ] + Resource = "*" + Effect = "Allow" + Sid = "LambdaEventSourceMappingRead" + }, + { + Condition = { + "ForAnyValue:StringLike" = { + "lambda:FunctionArn" = [ + "arn:aws:lambda:us-east-1:${local.account_id}:function:po-*", + "arn:aws:lambda:us-east-1:${local.account_id}:function:workorder-*", + "arn:aws:lambda:us-east-1:${local.account_id}:function:procurement-api", + ] + } + } + Action = [ + "lambda:CreateEventSourceMapping", + "lambda:DeleteEventSourceMapping", + "lambda:UpdateEventSourceMapping", + ] + Resource = "*" + Effect = "Allow" + Sid = "LambdaEventSourceMappings" + }, + { + Action = [ + "lambda:ListFunctions", + "lambda:ListEventSourceMappings", + "lambda:GetAccountSettings", + ] + Resource = "*" + Effect = "Allow" + Sid = "LambdaList" + }, + { + Action = [ + "ssm:GetParameter", + "ssm:GetParameters", + ] + Resource = [ + "arn:aws:ssm:us-east-1:${local.account_id}:parameter/seahaven/dynamodb/cmk-arn", + "arn:aws:ssm:us-east-1:${local.account_id}:parameter/procurement-api/custom-domain/certificate-arn", + ] + Effect = "Allow" + Sid = "SsmRead" + }, + { + Action = [ + "logs:CreateLogGroup", + "logs:DeleteLogGroup", + "logs:PutRetentionPolicy", + "logs:DeleteRetentionPolicy", + "logs:TagResource", + "logs:UntagResource", + "logs:ListTagsForResource", + "logs:PutMetricFilter", + "logs:DeleteMetricFilter", + "logs:DescribeMetricFilters", + ] + Resource = [ + "arn:aws:logs:us-east-1:${local.account_id}:log-group:/aws/lambda/po-*", + "arn:aws:logs:us-east-1:${local.account_id}:log-group:/aws/lambda/workorder-*", + "arn:aws:logs:us-east-1:${local.account_id}:log-group:/aws/lambda/procurement-api*", + ] + Effect = "Allow" + Sid = "CloudWatchLogs" + }, + { + Action = [ + "logs:DescribeLogGroups", + ] + Resource = "*" + Effect = "Allow" + Sid = "CloudWatchLogsDescribe" + }, + { + Action = [ + "s3:*", + ] + Resource = [ + "arn:aws:s3:::procurement-ingest-artifacts-${local.account_id}", + "arn:aws:s3:::procurement-ingest-artifacts-${local.account_id}/*", + ] + Effect = "Allow" + Sid = "ArtifactsBucket" + }, + { + Action = [ + "s3:*", + ] + Resource = [ + "arn:aws:s3:::po-ingest-emails-${local.account_id}", + "arn:aws:s3:::po-ingest-emails-${local.account_id}/*", + "arn:aws:s3:::workorder-ingest-emails-${local.account_id}", + "arn:aws:s3:::workorder-ingest-emails-${local.account_id}/*", + ] + Effect = "Allow" + Sid = "EmailBuckets" + }, + { + Action = [ + "dynamodb:*", + ] + Resource = [ + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/purchase-orders", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/purchase-orders/*", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/verified-sites", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/verified-sites/*", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/pending-site-review", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/pending-site-review/*", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/work-orders", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/work-orders/*", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/work-order-comments", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/work-order-comments/*", + ] + Effect = "Allow" + Sid = "DynamoDBTables" + }, + { + Action = [ + "dynamodb:ListTables", + "dynamodb:ListStreams", + ] + Resource = "*" + Effect = "Allow" + Sid = "DynamoDBList" + }, + { + Action = [ + "sqs:*", + ] + Resource = [ + "arn:aws:sqs:us-east-1:${local.account_id}:po-ingest-*", + "arn:aws:sqs:us-east-1:${local.account_id}:WorkorderIngestStack-*", + "arn:aws:sqs:us-east-1:${local.account_id}:workorder-shoc-emitter-*", + ] + Effect = "Allow" + Sid = "SqsQueues" + }, + { + Action = [ + "cloudwatch:*", + ] + Resource = [ + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:po-*", + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:workorder-*", + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:procurement-api-*", + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:purchase-orders-*", + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:verified-sites-*", + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:pending-site-review-*", + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:work-orders-*", + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:work-order-comments-*", + ] + Effect = "Allow" + Sid = "CloudWatchAlarms" + }, + { + Action = [ + "sns:Publish", + "sns:GetTopicAttributes", + "sns:ListTagsForResource", + ] + Resource = [ + "arn:aws:sns:us-east-1:${local.account_id}:site-alerts", + ] + Effect = "Allow" + Sid = "SiteAlertsSns" + }, + { + Action = [ + "apigateway:*", + ] + Resource = [ + "arn:aws:apigateway:us-east-1::/restapis/mvul1efda2", + "arn:aws:apigateway:us-east-1::/restapis/mvul1efda2/*", + "arn:aws:apigateway:us-east-1::/domainnames/procurement-api.seahaven.com", + "arn:aws:apigateway:us-east-1::/domainnames/procurement-api.seahaven.com/*", + "arn:aws:apigateway:us-east-1::/tags/*", + ] + Effect = "Allow" + Sid = "ApiGateway" + }, + { + Action = [ + "ses:CreateReceiptRule", + "ses:UpdateReceiptRule", + "ses:DeleteReceiptRule", + "ses:DescribeReceiptRule", + "ses:SetReceiptRulePosition", + ] + Resource = [ + "arn:aws:ses:us-east-1:${local.account_id}:receipt-rule-set/INBOUND_MAIL:receipt-rule/ExistingRuleSetPoEmailRuleAC8E9C87-qwGDj9lBoL1G", + "arn:aws:ses:us-east-1:${local.account_id}:receipt-rule-set/INBOUND_MAIL:receipt-rule/ExistingRuleSetWorkorderEmailRuleEA29F845-PKtaDBvIg61a", + ] + Effect = "Allow" + Sid = "SesReceiptRules" + }, + { + Action = [ + "ses:DescribeReceiptRuleSet", + ] + Resource = "*" + Effect = "Allow" + Sid = "SesDescribeRuleSet" + }, + { + Action = [ + "kms:DescribeKey", + "kms:GetKeyPolicy", + "kms:GetKeyRotationStatus", + "kms:ListResourceTags", + "kms:PutKeyPolicy", + "kms:EnableKeyRotation", + "kms:DisableKeyRotation", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:TagResource", + "kms:UntagResource", + "kms:EnableKey", + "kms:DisableKey", + ] + Resource = [ + "arn:aws:kms:us-east-1:${local.account_id}:key/d10fd1f0-a61a-4405-8568-85e9fd11ba18", + ] + Effect = "Allow" + Sid = "ShocKms" + }, + { + Action = [ + "kms:ListAliases", + "kms:ListKeys", + ] + Resource = "*" + Effect = "Allow" + Sid = "KmsList" + }, + { + Action = [ + "kms:CreateAlias", + "kms:DeleteAlias", + "kms:UpdateAlias", + ] + Resource = [ + "arn:aws:kms:us-east-1:${local.account_id}:alias/workorder-ingest-shoc-webhook-kms", + "arn:aws:kms:us-east-1:${local.account_id}:key/d10fd1f0-a61a-4405-8568-85e9fd11ba18", + ] + Effect = "Allow" + Sid = "ShocKmsAlias" + }, + { + Action = [ + "secretsmanager:DeleteSecret", + "secretsmanager:DescribeSecret", + "secretsmanager:GetResourcePolicy", + "secretsmanager:PutResourcePolicy", + "secretsmanager:DeleteResourcePolicy", + "secretsmanager:TagResource", + "secretsmanager:UntagResource", + "secretsmanager:RotateSecret", + "secretsmanager:CancelRotateSecret", + "secretsmanager:UpdateSecretVersionStage", + ] + Resource = [ + "arn:aws:secretsmanager:us-east-1:${local.account_id}:secret:workorder-ingest/shoc-webhook-hmac-*", + ] + Effect = "Allow" + Sid = "ShocSecretShell" + }, + { + Condition = { + StringEquals = { + "secretsmanager:Name" = "workorder-ingest/shoc-webhook-hmac" + } + } + Action = [ + "secretsmanager:CreateSecret", + ] + Resource = "*" + Effect = "Allow" + Sid = "ShocSecretCreate" + }, + { + Action = [ + "secretsmanager:DescribeSecret", + "secretsmanager:GetResourcePolicy", + ] + Resource = [ + "arn:aws:secretsmanager:us-east-1:${local.account_id}:secret:procurement-ingest/web-ui-auth-token-*", + ] + Effect = "Allow" + Sid = "WebUiSecretDescribe" + }, + ] + }) +} + +resource "aws_iam_role_policy" "hcptf_plan_refresh" { + name = "procurement-ingest-plan-refresh" + role = aws_iam_role.hcptf_plan.id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListRolePolicies", + "iam:ListAttachedRolePolicies", + ] + Resource = [ + "arn:aws:iam::${local.account_id}:role/tf-managed/po-*", + "arn:aws:iam::${local.account_id}:role/tf-managed/workorder-*", + "arn:aws:iam::${local.account_id}:role/tf-managed/procurement-api", + "arn:aws:iam::${local.account_id}:role/hcptf-procurement-ingest", + "arn:aws:iam::${local.account_id}:role/hcptf-procurement-ingest-plan", + ] + Effect = "Allow" + Sid = "RefreshIamRoles" + }, + { + Action = [ + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:ListPolicyTags", + ] + Resource = "*" + Effect = "Allow" + Sid = "RefreshManagedPolicies" + }, + { + Action = [ + "lambda:Get*", + ] + Resource = [ + "arn:aws:lambda:us-east-1:${local.account_id}:function:po-*", + "arn:aws:lambda:us-east-1:${local.account_id}:function:workorder-*", + "arn:aws:lambda:us-east-1:${local.account_id}:function:procurement-api", + ] + Effect = "Allow" + Sid = "RefreshLambda" + }, + { + Action = [ + "lambda:ListFunctions", + "lambda:ListEventSourceMappings", + "lambda:GetEventSourceMapping", + "lambda:GetAccountSettings", + ] + Resource = "*" + Effect = "Allow" + Sid = "RefreshLambdaList" + }, + { + Action = [ + "s3:Get*", + "s3:ListBucket", + ] + Resource = [ + "arn:aws:s3:::procurement-ingest-artifacts-${local.account_id}", + "arn:aws:s3:::procurement-ingest-artifacts-${local.account_id}/*", + ] + Effect = "Allow" + Sid = "RefreshArtifactsBucket" + }, + { + Action = [ + "s3:Get*", + "s3:ListBucket", + "s3:GetBucketNotification", + "s3:GetBucketPolicy", + "s3:GetEncryptionConfiguration", + "s3:GetBucketTagging", + "s3:GetBucketVersioning", + "s3:GetBucketPublicAccessBlock", + ] + Resource = [ + "arn:aws:s3:::po-ingest-emails-${local.account_id}", + "arn:aws:s3:::po-ingest-emails-${local.account_id}/*", + "arn:aws:s3:::workorder-ingest-emails-${local.account_id}", + "arn:aws:s3:::workorder-ingest-emails-${local.account_id}/*", + ] + Effect = "Allow" + Sid = "RefreshEmailBuckets" + }, + { + Action = [ + "dynamodb:DescribeTable", + "dynamodb:DescribeTimeToLive", + "dynamodb:DescribeContinuousBackups", + "dynamodb:DescribeStream", + "dynamodb:ListTagsOfResource", + ] + Resource = [ + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/purchase-orders", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/purchase-orders/*", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/verified-sites", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/pending-site-review", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/work-orders", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/work-orders/*", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/work-order-comments", + "arn:aws:dynamodb:us-east-1:${local.account_id}:table/work-order-comments/*", + ] + Effect = "Allow" + Sid = "RefreshDynamoDB" + }, + { + Action = [ + "dynamodb:ListStreams", + "dynamodb:ListTables", + ] + Resource = "*" + Effect = "Allow" + Sid = "RefreshDynamoDBList" + }, + { + Action = [ + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl", + "sqs:ListQueueTags", + ] + Resource = [ + "arn:aws:sqs:us-east-1:${local.account_id}:po-ingest-*", + "arn:aws:sqs:us-east-1:${local.account_id}:WorkorderIngestStack-*", + "arn:aws:sqs:us-east-1:${local.account_id}:workorder-shoc-emitter-*", + ] + Effect = "Allow" + Sid = "RefreshSqs" + }, + { + Action = [ + "cloudwatch:DescribeAlarms", + "cloudwatch:ListTagsForResource", + ] + Resource = [ + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:po-*", + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:workorder-*", + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:procurement-api-*", + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:purchase-orders-*", + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:verified-sites-*", + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:pending-site-review-*", + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:work-orders-*", + "arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:work-order-comments-*", + ] + Effect = "Allow" + Sid = "RefreshCloudWatchAlarms" + }, + { + Action = [ + "apigateway:GET", + ] + Resource = [ + "arn:aws:apigateway:us-east-1::/restapis/mvul1efda2", + "arn:aws:apigateway:us-east-1::/restapis/mvul1efda2/*", + "arn:aws:apigateway:us-east-1::/domainnames/procurement-api.seahaven.com", + "arn:aws:apigateway:us-east-1::/domainnames/procurement-api.seahaven.com/*", + ] + Effect = "Allow" + Sid = "RefreshApiGateway" + }, + { + Action = [ + "kms:DescribeKey", + "kms:GetKeyPolicy", + "kms:GetKeyRotationStatus", + "kms:ListResourceTags", + ] + Resource = [ + "arn:aws:kms:us-east-1:${local.account_id}:key/*", + "arn:aws:kms:us-east-1:${local.account_id}:alias/workorder-ingest-shoc-webhook-kms", + ] + Effect = "Allow" + Sid = "RefreshKms" + }, + { + Action = [ + "kms:ListAliases", + "kms:ListKeys", + ] + Resource = "*" + Effect = "Allow" + Sid = "RefreshKmsList" + }, + { + Action = [ + "secretsmanager:DescribeSecret", + "secretsmanager:GetResourcePolicy", + "secretsmanager:ListSecretVersionIds", + ] + Resource = [ + "arn:aws:secretsmanager:us-east-1:${local.account_id}:secret:workorder-ingest/shoc-webhook-hmac-*", + "arn:aws:secretsmanager:us-east-1:${local.account_id}:secret:procurement-ingest/web-ui-auth-token-*", + ] + Effect = "Allow" + Sid = "RefreshSecrets" + }, + { + Action = [ + "ssm:GetParameter", + "ssm:GetParameters", + ] + Resource = [ + "arn:aws:ssm:us-east-1:${local.account_id}:parameter/seahaven/dynamodb/cmk-arn", + "arn:aws:ssm:us-east-1:${local.account_id}:parameter/procurement-api/custom-domain/certificate-arn", + ] + Effect = "Allow" + Sid = "RefreshSsm" + }, + { + Action = [ + "ses:DescribeReceiptRule", + "ses:DescribeReceiptRuleSet", + ] + Resource = "*" + Effect = "Allow" + Sid = "RefreshSes" + }, + { + Action = [ + "logs:DescribeLogGroups", + "logs:DescribeMetricFilters", + "logs:ListTagsForResource", + ] + Resource = "*" + Effect = "Allow" + Sid = "RefreshLogs" + }, + ] + }) +} + +resource "aws_iam_role" "hcptf_apply" { + name = "hcptf-procurement-ingest" + assume_role_policy = data.aws_iam_policy_document.hcptf_apply_trust.json + max_session_duration = 3600 + + tags = { + Project = "procurement-ingest" + Owner = "adam@seahavenind.com" + ManagedBy = "terraform" + } +} + +# Services inline is 6894 bytes; a second inline for scoped IAM exceeds the +# 10240 role quota. Attach scoped IAM as a customer-managed policy instead. +# Exclusive set keeps seahaven-hcptf-iam-management detached. +resource "aws_iam_role_policy_attachments_exclusive" "hcptf_apply" { + role_name = aws_iam_role.hcptf_apply.name + policy_arns = [ + aws_iam_policy.hcptf_scoped_iam.arn, + ] +} + +resource "aws_iam_role" "hcptf_plan" { + name = "hcptf-procurement-ingest-plan" + assume_role_policy = data.aws_iam_policy_document.hcptf_plan_trust.json + max_session_duration = 3600 + + tags = { + Project = "procurement-ingest" + Owner = "adam@seahavenind.com" + ManagedBy = "terraform" + } +} + +resource "aws_iam_role_policy_attachment" "hcptf_plan_viewonly" { + role = aws_iam_role.hcptf_plan.name + policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" +} + +resource "aws_iam_role_policy_attachments_exclusive" "hcptf_plan" { + role_name = aws_iam_role.hcptf_plan.name + policy_arns = [ + aws_iam_role_policy_attachment.hcptf_plan_viewonly.policy_arn, + ] +} + +resource "aws_iam_policy" "hcptf_scoped_iam" { + name = "hcptf-procurement-ingest-scoped-iam" + path = "/tf-managed/" + policy = data.aws_iam_policy_document.hcptf_scoped_iam.json + + tags = { + Project = "procurement-ingest" + Owner = "adam@seahavenind.com" + ManagedBy = "terraform" + } +} + +resource "aws_iam_role_policy_attachment" "hcptf_scoped_iam" { + role = aws_iam_role.hcptf_apply.name + policy_arn = aws_iam_policy.hcptf_scoped_iam.arn +}