diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 56e541a..fd6eb5d 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -47,7 +47,9 @@ jobs: uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@e5691d8a7f96ac4d5a841a82975ff0a4354d53ac # v1.0.7 with: node-version: "24" - stacks: "dev-baseline deploy-substrate-dev" + # seahaven-hcptf-dev was imported. The deploy creates the three managed + # policies and removes the inline policies. Do not create the roles. + stacks: "dev-baseline deploy-substrate-dev seahaven-hcptf-dev" stack-name: "seahaven-dev-baseline" secrets: deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_DEV }} @@ -57,7 +59,9 @@ jobs: with: node-version: "24" # seahaven-site-hcptf was imported after the roles left terraform-substrate. - stacks: "prod-baseline dynamodb-cmk-prod alarm-topic-prod deploy-substrate-prod app-web-acl-prod seahaven-site-hcptf" + # seahaven-hcptf was imported. The deploy creates the three managed + # policies and removes the inline policies. Do not create the roles. + stacks: "prod-baseline dynamodb-cmk-prod alarm-topic-prod deploy-substrate-prod app-web-acl-prod seahaven-site-hcptf seahaven-hcptf" stack-name: "seahaven-prod-baseline" secrets: deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_PROD }} diff --git a/README.md b/README.md index 8dfb255..05fe885 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,8 @@ are noted): | `seahaven-engineering-access` | 328440206208 | us-east-1 | Identity Center group `engineering`. `EngineeringProd` reads payments-dashboard configuration and seahaven-site in 011934824531. `EngineeringDev` has no allow (PLAT-235, PLAT-236). | | `seahaven-external-dev-baseline` | 396287094661 | us-east-1 | Member-account baseline: Config, GuardDuty, Security Hub (FSBP + CIS v3.0), Access Analyzer, flow logs, budget | | `seahaven-terraform-substrate` | 011934824531, 710827005802 | us-east-1 | Removed from the CDK app and from CD (PLAT-147). Live stacks remain until `scripts/delete-terraform-substrate-prod-dev.sh`. The six imported prod pairs are already forgotten. | -| `seahaven-site-hcptf` | 011934824531 | us-east-1 | Imported `hcptf-seahaven-site` apply and plan roles (PLAT-225). On the prod deploy job. Do not create. | +| `seahaven-site-hcptf` | 011934824531 | us-east-1 | Imported `hcptf-seahaven-site` apply and plan roles (PLAT-225). Policies are managed at `/tf-managed/`. On the prod deploy job. Do not create the roles. | +| `seahaven-hcptf` | 011934824531, 710827005802 | us-east-1 | payments-dashboard HCP apply and plan roles, scoped policies, and the Lambda boundary. Trust is pinned to `payments-dashboard-prod` in project `seahaven-prod`, and to `payments-dashboard-dev` in project `seahaven-dev`. Roles and boundary are imported. On the dev and prod deploy jobs. | | `seahaven-terraform-substrate` | 396287094661 | us-east-1 | Staged manually for SHOC backend/frontend adoption; exact HCP roles and deploy boundaries referencing the existing OIDC provider. Stays (PLAT-148). | | `seahaven-security-baseline` | 001520130573 | us-east-1 | Member-account baseline for the delegated security-admin account (same construct set) | | `seahaven-dev-baseline` | 710827005802 | us-east-1 | Member-account baseline for internal dev/staging (org-managed detection — no local GuardDuty/SecurityHub) | @@ -77,6 +78,9 @@ the TypeScript source — no separate compile step needed for `cdk synth` / | `dynamodb-cmk-prod` | `seahaven-dynamodb-cmk` | 011934824531 | us-east-1 | `lib/dynamodb-cmk-stack.ts` | | `alarm-topic-prod` | `seahaven-alarm-topic` | 011934824531 | us-east-1 | `lib/alarm-topic-stack.ts` | | `app-web-acl-prod` | `seahaven-app-web-acl` | 011934824531 | us-east-1 | `lib/app-web-acl-stack.ts` | +| `seahaven-site-hcptf` | `seahaven-site-hcptf` | 011934824531 | us-east-1 | `lib/seahaven-site-hcptf-stack.ts` | +| `seahaven-hcptf` | `seahaven-hcptf` | 011934824531 | us-east-1 | `lib/seahaven-hcptf-stack.ts` | +| `seahaven-hcptf-dev` | `seahaven-hcptf` | 710827005802 | us-east-1 | `lib/seahaven-hcptf-stack.ts` | Member-account stacks deploy with per-account credentials — the CD workflow runs one job per account, each assuming that account's OIDC deploy role. Local @@ -236,7 +240,12 @@ Console / one-shot CLI owns only: Do not manage prod/dev workload IAM (`hcptf-` pairs, Lambda exec roles, `policy/tf-managed/` ceilings) in the console. Do not append new prod/dev `hcptf-` pairs to this template. New prod/dev HCP stacks -do not need an org-baseline IAM PR. +do not need an org-baseline IAM PR. payments-dashboard HCP roles and +the Lambda boundary are stack `seahaven-hcptf` in prod and dev. Both copies +are imported and are on the deploy jobs. The deploy creates +`payments-dashboard-hcptf-iam`, `payments-dashboard-hcptf-services`, and +`payments-dashboard-hcptf-plan`, and removes the inline policies. Do not +create the roles or the boundary. **External-dev IAM stays in this repo (PLAT-148).** SHOC backend/frontend HCP roles, SHOC deploy/runtime boundaries, `shoc-frontend-resources.ts`, and diff --git a/bin/app.ts b/bin/app.ts index ea9910b..e5b8488 100644 --- a/bin/app.ts +++ b/bin/app.ts @@ -10,6 +10,7 @@ import { DeploySubstrateStack } from "../lib/deploy-substrate-stack"; import { TerraformSubstrateStack } from "../lib/terraform-substrate-stack"; import { DynamoDbCmkStack } from "../lib/dynamodb-cmk-stack"; import { AppWebAclStack } from "../lib/app-web-acl-stack"; +import { SeahavenHcptfStack } from "../lib/seahaven-hcptf-stack"; import { SeahavenSiteHcptfStack } from "../lib/seahaven-site-hcptf-stack"; import { MemberBaselineStack } from "../lib/member-baseline-stack"; import { OrgGovernanceStack } from "../lib/org-governance-stack"; @@ -232,11 +233,62 @@ new AppWebAclStack(app, "app-web-acl-prod", { // seahaven-site exec roles (PLAT-225). Not part of terraform-substrate. // Imported. On the prod deploy job. A create fails because the roles already exist. +// Inline policies are managed policies at /tf-managed/. Do not recreate the roles. new SeahavenSiteHcptfStack(app, "seahaven-site-hcptf", { stackName: "seahaven-site-hcptf", env: { account: PROD_ACCOUNT, region: "us-east-1" }, }); +// payments-dashboard HCP roles, scoped policies, and the Lambda boundary. +// The same three names already exist in prod and dev. Import them with +// `-c hcptfPaymentsImport=true`. A create fails. Neither stack is on a +// deploy job until that import succeeds. Trust is pinned per workspace. +const hcptfPaymentsImport = contextBoolean("hcptfPaymentsImport"); +const paymentsSecrets = ( + account: string, + suffixes: readonly string[], +): string[] => + suffixes.map( + (suffix) => + `arn:aws:secretsmanager:us-east-1:${account}:secret:payments-dashboard/${suffix}`, + ); + +new SeahavenHcptfStack(app, "seahaven-hcptf", { + stackName: "seahaven-hcptf", + env: { account: PROD_ACCOUNT, region: "us-east-1" }, + hcpProject: "seahaven-prod", + hcpWorkspace: "payments-dashboard-prod", + dynamodbCmkArn: + "arn:aws:kms:us-east-1:011934824531:key/be5fa4cb-c546-40fe-a13d-c7bec79f5d12", + secretArns: paymentsSecrets(PROD_ACCOUNT, [ + "slack-bot-token-0pAM3S", + "slack-signing-secret-u0T6h8", + "boa-check-mgmt-LEbC65", + "boa-reporting-JoR9lq", + "expense-slack-token-SeMg3s", + "expense-slack-signing-secret-lbb78J", + ]), + importExisting: hcptfPaymentsImport, +}); + +new SeahavenHcptfStack(app, "seahaven-hcptf-dev", { + stackName: "seahaven-hcptf", + env: { account: DEV_ACCOUNT, region: "us-east-1" }, + hcpProject: "seahaven-dev", + hcpWorkspace: "payments-dashboard-dev", + dynamodbCmkArn: + "arn:aws:kms:us-east-1:710827005802:key/600997e6-418e-4b7f-9d63-cd42a7505a95", + secretArns: paymentsSecrets(DEV_ACCOUNT, [ + "slack-bot-token-KhPaLp", + "slack-signing-secret-CDxsUK", + "boa-check-mgmt-kkEnCw", + "boa-reporting-uMHHVo", + "expense-slack-token-05OZg3", + "expense-slack-signing-secret-DQAoXS", + ]), + importExisting: hcptfPaymentsImport, +}); + // External-dev already has app.terraform.io federation. Both role gates start // false in cdk.json: POC is enabled by a normal update; dev/staging only by // CloudFormation import after Terraform relinquishes those four live roles. diff --git a/import-maps/seahaven-hcptf-dev.json b/import-maps/seahaven-hcptf-dev.json new file mode 100644 index 0000000..2ada23f --- /dev/null +++ b/import-maps/seahaven-hcptf-dev.json @@ -0,0 +1,11 @@ +{ + "PaymentsDashboardApplyRole": { + "RoleName": "hcptf-payments-dashboard" + }, + "PaymentsDashboardPlanRole": { + "RoleName": "hcptf-payments-dashboard-plan" + }, + "PaymentsDashboardLambdaBoundary": { + "PolicyArn": "arn:aws:iam::710827005802:policy/tf-managed/payments-dashboard-lambda-boundary" + } +} diff --git a/import-maps/seahaven-hcptf-prod.json b/import-maps/seahaven-hcptf-prod.json new file mode 100644 index 0000000..b2dbba2 --- /dev/null +++ b/import-maps/seahaven-hcptf-prod.json @@ -0,0 +1,11 @@ +{ + "PaymentsDashboardApplyRole": { + "RoleName": "hcptf-payments-dashboard" + }, + "PaymentsDashboardPlanRole": { + "RoleName": "hcptf-payments-dashboard-plan" + }, + "PaymentsDashboardLambdaBoundary": { + "PolicyArn": "arn:aws:iam::011934824531:policy/tf-managed/payments-dashboard-lambda-boundary" + } +} diff --git a/lib/hcptf-policy-aspect.ts b/lib/hcptf-policy-aspect.ts new file mode 100644 index 0000000..78e74d9 --- /dev/null +++ b/lib/hcptf-policy-aspect.ts @@ -0,0 +1,58 @@ +import * as cdk from "aws-cdk-lib"; +import * as iam from "aws-cdk-lib/aws-iam"; +import { IConstruct } from "constructs"; + +/** IAM managed-policy quota, whitespace excluded. */ +const MAX_POLICY_CHARS = 6144; + +/** + * Fails synth when an HCP stack's managed policy document reaches the IAM + * size quota, or when a role in that stack carries an inline policy. + * Register it on seahaven-hcptf and seahaven-site-hcptf only. + * + * `allowInlinePolicies` is only for the one-time `cdk import` template. + * That template has to name the live inline policies so the following + * deploy can delete them. The default template still rejects inline policies. + */ +export class HcptfPolicyAspect implements cdk.IAspect { + constructor(private readonly allowInlinePolicies = false) {} + + public visit(node: IConstruct): void { + if (node instanceof iam.CfnManagedPolicy) { + const size = JSON.stringify(node.policyDocument).replace(/\s/g, "").length; + if (size >= MAX_POLICY_CHARS) { + cdk.Annotations.of(node).addError( + `managed policy document is ${size} characters, whitespace excluded (limit ${MAX_POLICY_CHARS})`, + ); + } + } + + if (node instanceof iam.CfnRole) { + const policies = node.policies; + if (Array.isArray(policies)) { + for (const policy of policies) { + if (cdk.Token.isUnresolved(policy) || !("policyDocument" in policy)) { + continue; + } + const size = JSON.stringify(policy.policyDocument) + .replace(/\s/g, "") + .length; + if (size >= MAX_POLICY_CHARS) { + cdk.Annotations.of(node).addError( + `inline policy document is ${size} characters, whitespace excluded (limit ${MAX_POLICY_CHARS})`, + ); + } + } + if (!this.allowInlinePolicies && policies.length > 0) { + cdk.Annotations.of(node).addError( + "inline policy is not allowed on this role", + ); + } + } + } + + if (node instanceof iam.CfnPolicy) { + cdk.Annotations.of(node).addError("inline policy is not allowed in this stack"); + } + } +} diff --git a/lib/seahaven-hcptf-stack.ts b/lib/seahaven-hcptf-stack.ts new file mode 100644 index 0000000..38b6ee8 --- /dev/null +++ b/lib/seahaven-hcptf-stack.ts @@ -0,0 +1,1232 @@ +import * as cdk from "aws-cdk-lib"; +import * as iam from "aws-cdk-lib/aws-iam"; +import { Construct } from "constructs"; +import { HcptfPolicyAspect } from "./hcptf-policy-aspect"; + +/** + * payments-dashboard HCP exec roles. One stack per account. Prod is + * 011934824531, workspace payments-dashboard-prod, project seahaven-prod. + * Dev is 710827005802, workspace payments-dashboard-dev, project seahaven-dev. + * + * hcptf-payments-dashboard, hcptf-payments-dashboard-plan, and + * payments-dashboard-lambda-boundary already existed in both accounts and + * were imported. A create fails with a name conflict. The stacks are on + * the dev and prod deploy jobs. That deploy creates the three managed + * policies and removes the inline policies. + * + * `cdk import -c hcptfPaymentsImport=true` synthesizes only those three + * resources, with the roles' current inline policy names and no reference + * to the policies that do not exist yet. The default template is the + * managed-policy state. + */ +export interface SeahavenHcptfStackProps extends cdk.StackProps { + /** HCP project name: seahaven-prod or seahaven-dev. */ + hcpProject: string; + /** HCP workspace name: payments-dashboard-prod or payments-dashboard-dev. */ + hcpWorkspace: string; + /** DynamoDB CMK in this account. */ + dynamodbCmkArn: string; + /** + * Secret ARNs in this order: slack-bot-token, slack-signing-secret, + * boa-check-mgmt, boa-reporting, expense-slack-token, + * expense-slack-signing-secret. + */ + secretArns: readonly string[]; + /** + * Synthesize the import template. Set from `-c hcptfPaymentsImport=true`. + * Default is the managed-policy template. + */ + importExisting?: boolean; +} + +export class SeahavenHcptfStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: SeahavenHcptfStackProps) { + super(scope, id, props); + paymentsDashboard(this, props); + cdk.Aspects.of(this).add(new HcptfPolicyAspect(props.importExisting === true)); + } +} + +const VIEW_ONLY = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"; + +/** Account and CMK id baked into the policy literals. Retarget rewrites them. */ +const TEMPLATE_ACCOUNT = "011934824531"; +const TEMPLATE_CMK_ID = "be5fa4cb-c546-40fe-a13d-c7bec79f5d12"; +const TEMPLATE_SECRET_ARNS = [ + "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/slack-bot-token-0pAM3S", + "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/slack-signing-secret-u0T6h8", + "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/boa-check-mgmt-LEbC65", + "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/boa-reporting-JoR9lq", + "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/expense-slack-token-SeMg3s", + "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/expense-slack-signing-secret-lbb78J", +] as const; + +function retarget( + document: object, + account: string, + target: SeahavenHcptfStackProps, +): object { + if (target.secretArns.length !== TEMPLATE_SECRET_ARNS.length) { + throw new Error("payments-dashboard secretArns must list six secrets"); + } + const cmkId = target.dynamodbCmkArn.split("/").pop(); + if (!cmkId || !target.dynamodbCmkArn.includes(":key/")) { + throw new Error("dynamodbCmkArn must be a KMS key ARN"); + } + let json = JSON.stringify(document); + const containedCmk = json.includes(TEMPLATE_CMK_ID); + for (let i = 0; i < TEMPLATE_SECRET_ARNS.length; i++) { + json = json.replaceAll(TEMPLATE_SECRET_ARNS[i], target.secretArns[i]); + } + json = json.replaceAll(TEMPLATE_CMK_ID, cmkId); + json = json.replaceAll(TEMPLATE_ACCOUNT, account); + if (containedCmk && !json.includes(target.dynamodbCmkArn)) { + throw new Error("CMK retarget did not produce dynamodbCmkArn"); + } + return JSON.parse(json); +} + +function paymentsDashboard( + stack: cdk.Stack, + target: SeahavenHcptfStackProps, +): void { + const account = stack.account; + const providerArn = `arn:aws:iam::${account}:oidc-provider/app.terraform.io`; + const workspace = `organization:seahaven:project:${target.hcpProject}:workspace:${target.hcpWorkspace}`; + + // CloudFormation rejects Tags and any RoleArn output on an IAM role import. + // The deploy after import adds both. + if (!target.importExisting) { + cdk.Tags.of(stack).add("Project", "payments-dashboard"); + cdk.Tags.of(stack).add("Owner", "adam@seahavenind.com"); + cdk.Tags.of(stack).add("ManagedBy", "cdk"); + } + + const boundary = managedPolicy( + stack, + "PaymentsDashboardLambdaBoundary", + "payments-dashboard-lambda-boundary", + boundaryDocument(account, target), + "Per-workload Lambda permissions boundary for payments-dashboard (PLAT-79).", + ); + + const applyTrust = trust( + providerArn, + `${workspace}:run_phase:apply`, + "HcpApply", + ); + const planTrust = trust(providerArn, `${workspace}:run_phase:plan`, "HcpPlan"); + + // Import template: the two roles and the boundary only. Inline policy names + // match the live roles so the later deploy can delete them. No Ref to the + // three managed policies that do not exist yet. + const apply = new iam.CfnRole(stack, "PaymentsDashboardApplyRole", { + roleName: "hcptf-payments-dashboard", + maxSessionDuration: 3600, + assumeRolePolicyDocument: applyTrust, + ...(target.importExisting + ? { + policies: [ + { + policyName: "payments-dashboard-services", + policyDocument: servicesDocument(account, target), + }, + { + policyName: "scoped-iam-management", + policyDocument: scopedIamDocument(account, target), + }, + ], + } + : { + managedPolicyArns: [ + managedPolicy( + stack, + "PaymentsDashboardIam", + "payments-dashboard-hcptf-iam", + scopedIamDocument(account, target), + ).ref, + managedPolicy( + stack, + "PaymentsDashboardServices", + "payments-dashboard-hcptf-services", + servicesDocument(account, target), + ).ref, + ], + }), + ...(target.importExisting ? {} : { tags: roleTags() }), + }); + retain(apply); + + const plan = new iam.CfnRole(stack, "PaymentsDashboardPlanRole", { + roleName: "hcptf-payments-dashboard-plan", + maxSessionDuration: 3600, + assumeRolePolicyDocument: planTrust, + ...(target.importExisting + ? { + managedPolicyArns: [VIEW_ONLY], + policies: [ + { + policyName: "payments-dashboard-plan-refresh", + policyDocument: planDocument(account, target), + }, + ], + } + : { + managedPolicyArns: [ + VIEW_ONLY, + managedPolicy( + stack, + "PaymentsDashboardPlan", + "payments-dashboard-hcptf-plan", + planDocument(account, target), + ).ref, + ], + }), + ...(target.importExisting ? {} : { tags: roleTags() }), + }); + retain(plan); + + if (!target.importExisting) { + new cdk.CfnOutput(stack, "ApplyRoleArn", { value: apply.attrArn }); + new cdk.CfnOutput(stack, "PlanRoleArn", { value: plan.attrArn }); + new cdk.CfnOutput(stack, "LambdaBoundaryArn", { value: boundary.ref }); + } +} + +function managedPolicy( + scope: Construct, + id: string, + name: string, + policyDocument: object, + description?: string, +): iam.CfnManagedPolicy { + const policy = new iam.CfnManagedPolicy(scope, id, { + managedPolicyName: name, + path: "/tf-managed/", + policyDocument, + ...(description === undefined ? {} : { description }), + }); + retain(policy); + return policy; +} + +function retain(resource: cdk.CfnResource): void { + resource.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN; + resource.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN; +} + +function roleTags(): cdk.CfnTag[] { + return [ + { key: "Project", value: "payments-dashboard" }, + { key: "Owner", value: "adam@seahavenind.com" }, + { key: "ManagedBy", value: "cdk" }, + ]; +} + +function trust(providerArn: string, sub: string, sid: string): object { + return { + Version: "2012-10-17", + Statement: [ + { + Sid: sid, + Effect: "Allow", + Action: "sts:AssumeRoleWithWebIdentity", + Principal: { Federated: providerArn }, + Condition: { + StringEquals: { + "app.terraform.io:aud": "aws.workload.identity", + "app.terraform.io:sub": sub, + }, + }, + }, + ], + }; +} + +function scopedIamDocument( + account: string, + target: SeahavenHcptfStackProps, +): object { + return retarget({ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "DenyCreatePolicy", + "Effect": "Deny", + "Action": [ + "iam:CreatePolicy", + "iam:CreatePolicyVersion", + "iam:DeletePolicy", + "iam:DeletePolicyVersion", + "iam:SetDefaultPolicyVersion" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "CreateExecRoleWithBoundary", + "Effect": "Allow", + "Action": [ + "iam:CreateRole" + ], + "Resource": [ + "arn:aws:iam::011934824531:role/tf-managed/payments-dashboard-*" + ], + "Condition": { + "StringLike": { + "iam:PermissionsBoundary": [ + "arn:aws:iam::011934824531:policy/tf-managed/payments-dashboard-*", + "arn:aws:iam::011934824531:policy/seahaven-lambda-execution-boundary", + "arn:aws:iam::011934824531:policy/seahaven-lambda-execution-boundary-payments-dashboard" + ] + } + } + }, + { + "Sid": "MutateExecRoleWithBoundary", + "Effect": "Allow", + "Action": [ + "iam:AttachRolePolicy", + "iam:PutRolePolicy", + "iam:PutRolePermissionsBoundary" + ], + "Resource": [ + "arn:aws:iam::011934824531:role/tf-managed/payments-dashboard-*" + ], + "Condition": { + "StringLike": { + "iam:PermissionsBoundary": [ + "arn:aws:iam::011934824531:policy/tf-managed/payments-dashboard-*", + "arn:aws:iam::011934824531:policy/seahaven-lambda-execution-boundary", + "arn:aws:iam::011934824531:policy/seahaven-lambda-execution-boundary-payments-dashboard" + ] + } + } + }, + { + "Sid": "WriteExecRoles", + "Effect": "Allow", + "Action": [ + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:DetachRolePolicy", + "iam:TagRole", + "iam:UntagRole", + "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + "iam:UpdateRoleDescription" + ], + "Resource": [ + "arn:aws:iam::011934824531:role/tf-managed/payments-dashboard-*" + ] + }, + { + "Sid": "PassExecRolesToLambda", + "Effect": "Allow", + "Action": [ + "iam:PassRole" + ], + "Resource": [ + "arn:aws:iam::011934824531:role/tf-managed/payments-dashboard-*" + ], + "Condition": { + "StringEquals": { + "iam:PassedToService": [ + "lambda.amazonaws.com" + ] + } + } + }, + { + "Sid": "CreateDeployRole", + "Effect": "Allow", + "Action": [ + "iam:CreateRole" + ], + "Resource": [ + "arn:aws:iam::011934824531:role/tf-managed/githubdeploy-payments-dashboard" + ], + "Condition": { + "Null": { + "iam:PermissionsBoundary": [ + "true" + ] + } + } + }, + { + "Sid": "WriteDeployRoles", + "Effect": "Allow", + "Action": [ + "iam:AttachRolePolicy", + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:TagRole", + "iam:UntagRole", + "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + "iam:UpdateRoleDescription" + ], + "Resource": [ + "arn:aws:iam::011934824531:role/tf-managed/githubdeploy-payments-dashboard" + ] + }, + { + "Sid": "IamReadOnly", + "Effect": "Allow", + "Action": [ + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListAttachedRolePolicies", + "iam:ListInstanceProfilesForRole", + "iam:ListPolicies", + "iam:ListPolicyVersions", + "iam:ListRolePolicies", + "iam:ListRoleTags", + "iam:ListRoles" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "DenySelfMutation", + "Effect": "Deny", + "Action": [ + "iam:AttachRolePolicy", + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:DeleteRolePermissionsBoundary", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:PutRolePermissionsBoundary", + "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + "iam:UpdateRoleDescription" + ], + "Resource": [ + "arn:aws:iam::011934824531:role/hcptf-*", + "arn:aws:iam::011934824531:role/github-cfn-execution-role", + "arn:aws:iam::011934824531:role/githubdeploy-*", + "arn:aws:iam::011934824531:role/cdk-hnb659fds-*", + "arn:aws:iam::011934824531:role/OrganizationAccountAccessRole", + "arn:aws:iam::011934824531:role/seahaven-*" + ] + }, + { + "Sid": "DenyBoundaryTampering", + "Effect": "Deny", + "Action": [ + "iam:DeleteRolePermissionsBoundary", + "iam:DeleteUserPermissionsBoundary" + ], + "Resource": [ + "arn:aws:iam::011934824531:role/*", + "arn:aws:iam::011934824531:user/*" + ] + }, + { + "Sid": "DenyBoundaryPolicyEdit", + "Effect": "Deny", + "Action": [ + "iam:CreatePolicyVersion", + "iam:DeletePolicy", + "iam:DeletePolicyVersion", + "iam:SetDefaultPolicyVersion" + ], + "Resource": [ + "arn:aws:iam::011934824531:policy/seahaven-*" + ] + } + ] +}, account, target); +} +function servicesDocument( + account: string, + target: SeahavenHcptfStackProps, +): object { + return retarget({ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "LambdaAll", + "Effect": "Allow", + "Action": [ + "lambda:*" + ], + "Resource": [ + "arn:aws:lambda:us-east-1:011934824531:function:payments-*" + ] + }, + { + "Sid": "LambdaList", + "Effect": "Allow", + "Action": [ + "lambda:ListFunctions", + "lambda:ListLayers", + "lambda:GetAccountSettings" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "EventBridgeRules", + "Effect": "Allow", + "Action": [ + "events:*" + ], + "Resource": [ + "arn:aws:events:us-east-1:011934824531:rule/payments-dashboard-*" + ] + }, + { + "Sid": "EventBridgeList", + "Effect": "Allow", + "Action": [ + "events:ListRules", + "events:ListRuleNamesByTarget" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "CloudWatchLogs", + "Effect": "Allow", + "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:011934824531:log-group:/aws/lambda/payments-*", + "arn:aws:logs:us-east-1:011934824531:log-group:/aws/apigateway/payments-dashboard", + "arn:aws:logs:us-east-1:011934824531:log-group:/aws/apigateway/payments-dashboard:*" + ] + }, + { + "Sid": "CloudWatchLogsDescribe", + "Effect": "Allow", + "Action": [ + "logs:DescribeLogGroups" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "ApiGwAccessLogDelivery", + "Effect": "Allow", + "Action": [ + "logs:CreateLogDelivery", + "logs:GetLogDelivery", + "logs:UpdateLogDelivery", + "logs:DeleteLogDelivery", + "logs:ListLogDeliveries", + "logs:PutResourcePolicy", + "logs:DescribeResourcePolicies" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "StackBuckets", + "Effect": "Allow", + "Action": [ + "s3:*" + ], + "Resource": [ + "arn:aws:s3:::payments-dashboard-artifacts-011934824531", + "arn:aws:s3:::payments-dashboard-artifacts-011934824531/*", + "arn:aws:s3:::seahaven-payments-csv-011934824531", + "arn:aws:s3:::seahaven-payments-csv-011934824531/*", + "arn:aws:s3:::seahaven-payments-boa-raw-011934824531", + "arn:aws:s3:::seahaven-payments-boa-raw-011934824531/*" + ] + }, + { + "Sid": "DynamoDBTable", + "Effect": "Allow", + "Action": [ + "dynamodb:*" + ], + "Resource": [ + "arn:aws:dynamodb:us-east-1:011934824531:table/PaymentsDashboard", + "arn:aws:dynamodb:us-east-1:011934824531:table/PaymentsDashboard/*" + ] + }, + { + "Sid": "DynamoDBList", + "Effect": "Allow", + "Action": [ + "dynamodb:ListTables" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "SqsDlq", + "Effect": "Allow", + "Action": [ + "sqs:*" + ], + "Resource": [ + "arn:aws:sqs:us-east-1:011934824531:payments-processPaymentCsv-async-dlq" + ] + }, + { + "Sid": "SqsList", + "Effect": "Allow", + "Action": [ + "sqs:ListQueues" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "HttpApiManage", + "Effect": "Allow", + "Action": [ + "apigateway:*" + ], + "Resource": [ + "arn:aws:apigateway:us-east-1::/apis", + "arn:aws:apigateway:us-east-1::/apis/*", + "arn:aws:apigateway:us-east-1::/tags/*", + "arn:aws:apigateway:us-east-1::/vpclinks", + "arn:aws:apigateway:us-east-1::/vpclinks/*" + ] + }, + { + "Sid": "PaymentsSsm", + "Effect": "Allow", + "Action": [ + "ssm:GetParameter", + "ssm:GetParameters", + "ssm:PutParameter", + "ssm:DeleteParameter", + "ssm:AddTagsToResource", + "ssm:RemoveTagsFromResource", + "ssm:ListTagsForResource" + ], + "Resource": [ + "arn:aws:ssm:us-east-1:011934824531:parameter/payments-dashboard/*", + "arn:aws:ssm:us-east-1:011934824531:parameter/seahaven/dynamodb/cmk-arn" + ] + }, + { + "Sid": "SsmDescribeParameters", + "Effect": "Allow", + "Action": [ + "ssm:DescribeParameters" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "SecretsManagerRead", + "Effect": "Allow", + "Action": [ + "secretsmanager:DescribeSecret", + "secretsmanager:GetResourcePolicy", + "secretsmanager:ListSecretVersionIds", + "secretsmanager:TagResource", + "secretsmanager:UntagResource" + ], + "Resource": [ + "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/*" + ] + }, + { + "Sid": "SecretsManagerList", + "Effect": "Allow", + "Action": [ + "secretsmanager:ListSecrets" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "KmsTableCmk", + "Effect": "Allow", + "Action": [ + "kms:DescribeKey", + "kms:GetKeyPolicy", + "kms:ListResourceTags", + "kms:CreateGrant", + "kms:ListGrants", + "kms:RetireGrant", + "kms:Encrypt", + "kms:Decrypt", + "kms:GenerateDataKey", + "kms:GenerateDataKeyWithoutPlaintext" + ], + "Resource": [ + "arn:aws:kms:us-east-1:011934824531:key/be5fa4cb-c546-40fe-a13d-c7bec79f5d12" + ] + }, + { + "Sid": "CloudWatchAlarms", + "Effect": "Allow", + "Action": [ + "cloudwatch:PutMetricAlarm", + "cloudwatch:DeleteAlarms", + "cloudwatch:DescribeAlarms", + "cloudwatch:TagResource", + "cloudwatch:UntagResource", + "cloudwatch:ListTagsForResource" + ], + "Resource": [ + "arn:aws:cloudwatch:us-east-1:011934824531:alarm:payments-*" + ] + }, + { + "Sid": "CloudWatchDescribeAlarms", + "Effect": "Allow", + "Action": [ + "cloudwatch:DescribeAlarms" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "SnsPublishSiteAlerts", + "Effect": "Allow", + "Action": [ + "sns:Publish", + "sns:GetTopicAttributes", + "sns:ListTagsForResource" + ], + "Resource": [ + "arn:aws:sns:us-east-1:011934824531:site-alerts" + ] + }, + { + "Sid": "ManageTfManagedBoundary", + "Effect": "Allow", + "Action": [ + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:ListPolicyVersions", + "iam:ListPolicyTags", + "iam:TagPolicy", + "iam:UntagPolicy" + ], + "Resource": [ + "arn:aws:iam::011934824531:policy/tf-managed/payments-dashboard-*" + ] + }, + { + "Sid": "Ec2VpcManagement", + "Effect": "Allow", + "Action": [ + "ec2:AllocateAddress", + "ec2:AssociateRouteTable", + "ec2:AttachInternetGateway", + "ec2:AuthorizeSecurityGroupEgress", + "ec2:AuthorizeSecurityGroupIngress", + "ec2:CreateInternetGateway", + "ec2:CreateNatGateway", + "ec2:CreateRoute", + "ec2:CreateRouteTable", + "ec2:CreateSecurityGroup", + "ec2:CreateSubnet", + "ec2:CreateVpc", + "ec2:CreateVpcEndpoint", + "ec2:CreateTags", + "ec2:DeleteInternetGateway", + "ec2:DeleteNatGateway", + "ec2:DeleteRoute", + "ec2:DeleteRouteTable", + "ec2:DeleteSecurityGroup", + "ec2:DeleteSubnet", + "ec2:DeleteVpc", + "ec2:DeleteVpcEndpoints", + "ec2:DescribeAccountAttributes", + "ec2:DescribeAddresses", + "ec2:DescribeAddressesAttribute", + "ec2:DescribeAvailabilityZones", + "ec2:DescribeInternetGateways", + "ec2:DescribeNatGateways", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeRouteTables", + "ec2:DescribeSecurityGroupRules", + "ec2:DescribeSecurityGroups", + "ec2:DescribeSubnets", + "ec2:DescribeTags", + "ec2:DescribeVpcAttribute", + "ec2:DescribeVpcEndpoints", + "ec2:DescribeVpcs", + "ec2:DescribePrefixLists", + "ec2:DetachInternetGateway", + "ec2:DisassociateAddress", + "ec2:DisassociateRouteTable", + "ec2:ModifySubnetAttribute", + "ec2:ModifyVpcAttribute", + "ec2:ModifyVpcEndpoint", + "ec2:ReleaseAddress", + "ec2:RevokeSecurityGroupEgress", + "ec2:RevokeSecurityGroupIngress", + "ec2:UpdateSecurityGroupRuleDescriptionsEgress", + "ec2:UpdateSecurityGroupRuleDescriptionsIngress" + ], + "Resource": [ + "*" + ] + } + ] +}, account, target); +} +function planDocument( + account: string, + target: SeahavenHcptfStackProps, +): object { + return retarget({ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "RefreshIamRoles", + "Effect": "Allow", + "Action": [ + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListRolePolicies", + "iam:ListAttachedRolePolicies", + "iam:ListRoleTags" + ], + "Resource": [ + "arn:aws:iam::011934824531:role/tf-managed/payments-dashboard-*", + "arn:aws:iam::011934824531:role/tf-managed/githubdeploy-payments-dashboard", + "arn:aws:iam::011934824531:role/hcptf-payments-dashboard", + "arn:aws:iam::011934824531:role/hcptf-payments-dashboard-plan" + ] + }, + { + "Sid": "RefreshManagedPolicies", + "Effect": "Allow", + "Action": [ + "iam:GetPolicy", + "iam:GetPolicyVersion" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "RefreshLambda", + "Effect": "Allow", + "Action": [ + "lambda:GetFunction", + "lambda:GetFunctionConfiguration", + "lambda:GetPolicy", + "lambda:GetFunctionCodeSigningConfig", + "lambda:GetFunctionConcurrency", + "lambda:GetFunctionEventInvokeConfig", + "lambda:GetFunctionUrlConfig", + "lambda:GetRuntimeManagementConfig", + "lambda:GetFunctionRecursionConfig", + "lambda:ListTags", + "lambda:ListVersionsByFunction", + "lambda:ListAliases" + ], + "Resource": [ + "arn:aws:lambda:us-east-1:011934824531:function:payments-*" + ] + }, + { + "Sid": "RefreshLambdaList", + "Effect": "Allow", + "Action": [ + "lambda:ListFunctions", + "lambda:ListLayers", + "lambda:GetAccountSettings" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "RefreshBuckets", + "Effect": "Allow", + "Action": [ + "s3:GetAccelerateConfiguration", + "s3:GetAnalyticsConfiguration", + "s3:GetBucketAcl", + "s3:GetBucketCORS", + "s3:GetBucketLifecycleConfiguration", + "s3:GetBucketLocation", + "s3:GetBucketLogging", + "s3:GetBucketNotification", + "s3:GetBucketObjectLockConfiguration", + "s3:GetBucketOwnershipControls", + "s3:GetBucketPolicy", + "s3:GetBucketPolicyStatus", + "s3:GetBucketPublicAccessBlock", + "s3:GetBucketReplication", + "s3:GetBucketRequestPayment", + "s3:GetBucketTagging", + "s3:GetBucketVersioning", + "s3:GetBucketWebsite", + "s3:GetEncryptionConfiguration", + "s3:GetIntelligentTieringConfiguration", + "s3:GetInventoryConfiguration", + "s3:GetLifecycleConfiguration", + "s3:GetMetricsConfiguration", + "s3:GetObject", + "s3:GetObjectTagging", + "s3:GetObjectVersion", + "s3:GetReplicationConfiguration", + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::payments-dashboard-artifacts-011934824531", + "arn:aws:s3:::payments-dashboard-artifacts-011934824531/*", + "arn:aws:s3:::seahaven-payments-csv-011934824531", + "arn:aws:s3:::seahaven-payments-csv-011934824531/*", + "arn:aws:s3:::seahaven-payments-boa-raw-011934824531", + "arn:aws:s3:::seahaven-payments-boa-raw-011934824531/*" + ] + }, + { + "Sid": "RefreshDynamoDB", + "Effect": "Allow", + "Action": [ + "dynamodb:DescribeTable", + "dynamodb:DescribeTimeToLive", + "dynamodb:DescribeContinuousBackups", + "dynamodb:DescribeKinesisStreamingDestination", + "dynamodb:ListTagsOfResource" + ], + "Resource": [ + "arn:aws:dynamodb:us-east-1:011934824531:table/PaymentsDashboard" + ] + }, + { + "Sid": "RefreshEventBridge", + "Effect": "Allow", + "Action": [ + "events:DescribeRule", + "events:ListTargetsByRule", + "events:ListTagsForResource" + ], + "Resource": [ + "arn:aws:events:us-east-1:011934824531:rule/payments-dashboard-*" + ] + }, + { + "Sid": "RefreshLogs", + "Effect": "Allow", + "Action": [ + "logs:DescribeLogGroups", + "logs:ListTagsForResource" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "RefreshHttpApi", + "Effect": "Allow", + "Action": [ + "apigateway:GET" + ], + "Resource": [ + "arn:aws:apigateway:us-east-1::/apis", + "arn:aws:apigateway:us-east-1::/apis/*", + "arn:aws:apigateway:us-east-1::/tags/*" + ] + }, + { + "Sid": "RefreshSsm", + "Effect": "Allow", + "Action": [ + "ssm:GetParameter", + "ssm:GetParameters", + "ssm:ListTagsForResource" + ], + "Resource": [ + "arn:aws:ssm:us-east-1:011934824531:parameter/payments-dashboard/*", + "arn:aws:ssm:us-east-1:011934824531:parameter/seahaven/dynamodb/cmk-arn" + ] + }, + { + "Sid": "RefreshSsmDescribeParameters", + "Effect": "Allow", + "Action": [ + "ssm:DescribeParameters" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "RefreshSecrets", + "Effect": "Allow", + "Action": [ + "secretsmanager:DescribeSecret", + "secretsmanager:GetResourcePolicy", + "secretsmanager:ListSecretVersionIds" + ], + "Resource": [ + "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/*" + ] + }, + { + "Sid": "RefreshSecretsList", + "Effect": "Allow", + "Action": [ + "secretsmanager:ListSecrets" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "RefreshAlarms", + "Effect": "Allow", + "Action": [ + "cloudwatch:DescribeAlarms", + "cloudwatch:ListTagsForResource" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "RefreshSns", + "Effect": "Allow", + "Action": [ + "sns:GetTopicAttributes", + "sns:ListTagsForResource" + ], + "Resource": [ + "arn:aws:sns:us-east-1:011934824531:site-alerts" + ] + }, + { + "Sid": "RefreshSqs", + "Effect": "Allow", + "Action": [ + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl", + "sqs:ListQueueTags" + ], + "Resource": [ + "arn:aws:sqs:us-east-1:011934824531:payments-processPaymentCsv-async-dlq" + ] + }, + { + "Sid": "RefreshKms", + "Effect": "Allow", + "Action": [ + "kms:DescribeKey", + "kms:GetKeyPolicy", + "kms:ListResourceTags", + "kms:CreateGrant", + "kms:ListGrants" + ], + "Resource": [ + "arn:aws:kms:us-east-1:011934824531:key/be5fa4cb-c546-40fe-a13d-c7bec79f5d12" + ] + }, + { + "Sid": "RefreshEc2", + "Effect": "Allow", + "Action": [ + "ec2:DescribeAccountAttributes", + "ec2:DescribeAddresses", + "ec2:DescribeAddressesAttribute", + "ec2:DescribeAvailabilityZones", + "ec2:DescribeInternetGateways", + "ec2:DescribeNatGateways", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeRouteTables", + "ec2:DescribeSecurityGroupRules", + "ec2:DescribeSecurityGroups", + "ec2:DescribeSubnets", + "ec2:DescribeTags", + "ec2:DescribeVpcAttribute", + "ec2:DescribeVpcEndpoints", + "ec2:DescribeVpcs", + "ec2:DescribePrefixLists" + ], + "Resource": [ + "*" + ] + } + ] +}, account, target); +} +function boundaryDocument( + account: string, + target: SeahavenHcptfStackProps, +): object { + return retarget({ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "CloudWatchLogsWrite", + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "Resource": [ + "arn:aws:logs:us-east-1:011934824531:log-group:/aws/lambda*" + ] + }, + { + "Sid": "CloudWatchLogsDescribe", + "Effect": "Allow", + "Action": [ + "logs:DescribeLogGroups" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "XRay", + "Effect": "Allow", + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "Ec2Eni", + "Effect": "Allow", + "Action": [ + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DeleteNetworkInterface", + "ec2:DescribeSubnets", + "ec2:DescribeSecurityGroups", + "ec2:DescribeVpcs" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "PaymentsSecrets", + "Effect": "Allow", + "Action": [ + "secretsmanager:GetSecretValue" + ], + "Resource": [ + "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/slack-bot-token-0pAM3S", + "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/slack-signing-secret-u0T6h8", + "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/boa-check-mgmt-LEbC65", + "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/boa-reporting-JoR9lq", + "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/expense-slack-token-SeMg3s", + "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/expense-slack-signing-secret-lbb78J" + ] + }, + { + "Sid": "PaymentsDynamoDB", + "Effect": "Allow", + "Action": [ + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:BatchGetItem", + "dynamodb:BatchWriteItem", + "dynamodb:DescribeTable", + "dynamodb:ConditionCheckItem" + ], + "Resource": [ + "arn:aws:dynamodb:us-east-1:011934824531:table/PaymentsDashboard", + "arn:aws:dynamodb:us-east-1:011934824531:table/PaymentsDashboard/*" + ] + }, + { + "Sid": "PaymentsCmk", + "Effect": "Allow", + "Action": [ + "kms:Decrypt", + "kms:GenerateDataKey", + "kms:DescribeKey" + ], + "Resource": [ + "arn:aws:kms:us-east-1:011934824531:key/be5fa4cb-c546-40fe-a13d-c7bec79f5d12" + ], + "Condition": { + "StringEquals": { + "kms:ViaService": [ + "dynamodb.us-east-1.amazonaws.com" + ] + } + } + }, + { + "Sid": "PaymentsCsvRead", + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:GetObjectVersion" + ], + "Resource": [ + "arn:aws:s3:::seahaven-payments-csv-011934824531/*" + ] + }, + { + "Sid": "PaymentsBoaRawPut", + "Effect": "Allow", + "Action": [ + "s3:PutObject" + ], + "Resource": [ + "arn:aws:s3:::seahaven-payments-boa-raw-011934824531/*" + ] + }, + { + "Sid": "PaymentsDlqSend", + "Effect": "Allow", + "Action": [ + "sqs:SendMessage" + ], + "Resource": [ + "arn:aws:sqs:us-east-1:011934824531:payments-processPaymentCsv-async-dlq" + ] + }, + { + "Sid": "PaymentsInvokeExpenseProcessor", + "Effect": "Allow", + "Action": [ + "lambda:InvokeFunction" + ], + "Resource": [ + "arn:aws:lambda:us-east-1:011934824531:function:payments-expenseProcessor" + ] + } + ] +}, account, target); +} diff --git a/lib/seahaven-site-hcptf-stack.ts b/lib/seahaven-site-hcptf-stack.ts index 975348d..9c4ff27 100644 --- a/lib/seahaven-site-hcptf-stack.ts +++ b/lib/seahaven-site-hcptf-stack.ts @@ -1,6 +1,7 @@ import * as cdk from "aws-cdk-lib"; import * as iam from "aws-cdk-lib/aws-iam"; import { Construct } from "constructs"; +import { HcptfPolicyAspect } from "./hcptf-policy-aspect"; /** * Prod exec roles for the seahaven-site HCP workspace (PLAT-225). @@ -10,7 +11,9 @@ import { Construct } from "constructs"; * The stack is on the prod deploy job. * * Import identifiers were the role names `hcptf-seahaven-site` and - * `hcptf-seahaven-site-plan`. + * `hcptf-seahaven-site-plan`. Construct ids stay ApplyRole and PlanRole. + * Inline policies are managed policies at /tf-managed/. Do not rename + * the roles. */ export class SeahavenSiteHcptfStack extends cdk.Stack { constructor(scope: Construct, id: string, props: cdk.StackProps) { @@ -26,21 +29,39 @@ export class SeahavenSiteHcptfStack extends cdk.Stack { const githubOidc = `arn:aws:iam::${account}:oidc-provider/token.actions.githubusercontent.com`; const hcpOidc = `arn:aws:iam::${account}:oidc-provider/app.terraform.io`; + const iamPolicy = managedPolicy( + this, + "IamPolicy", + "seahaven-site-hcptf-iam", + scopedIamPolicy(account, deployRole, boundary), + ); + const services = managedPolicy( + this, + "ServicesPolicy", + "seahaven-site-hcptf-services", + servicesPolicy(bucket, deployParams, wafParam, githubOidc), + ); + const planRefresh = managedPolicy( + this, + "PlanPolicy", + "seahaven-site-hcptf-plan", + planPolicy( + account, + bucket, + functionArn, + deployParams, + wafParam, + githubOidc, + deployRole, + boundary, + ), + ); + const apply = new iam.CfnRole(this, "ApplyRole", { roleName: "hcptf-seahaven-site", maxSessionDuration: 3600, assumeRolePolicyDocument: trust(hcpOidc, "apply"), - managedPolicyArns: [], - policies: [ - { - policyName: "seahaven-site-services", - policyDocument: servicesPolicy(bucket, deployParams, wafParam, githubOidc), - }, - { - policyName: "scoped-iam-management", - policyDocument: scopedIamPolicy(account, deployRole, boundary), - }, - ], + managedPolicyArns: [iamPolicy.ref, services.ref], tags: roleTags(), }); retain(apply); @@ -49,35 +70,42 @@ export class SeahavenSiteHcptfStack extends cdk.Stack { roleName: "hcptf-seahaven-site-plan", maxSessionDuration: 3600, assumeRolePolicyDocument: trust(hcpOidc, "plan"), - managedPolicyArns: ["arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"], - policies: [ - { - policyName: "seahaven-site-plan-refresh", - policyDocument: planPolicy( - account, - bucket, - functionArn, - deployParams, - wafParam, - githubOidc, - deployRole, - boundary, - ), - }, + managedPolicyArns: [ + "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess", + planRefresh.ref, ], tags: roleTags(), }); retain(plan); + new cdk.CfnOutput(this, "ApplyRoleArn", { value: apply.attrArn }); + new cdk.CfnOutput(this, "PlanRoleArn", { value: plan.attrArn }); + cdk.Tags.of(this).add("Project", "seahaven-site"); cdk.Tags.of(this).add("Owner", "adam@seahavenind.com"); cdk.Tags.of(this).add("ManagedBy", "cdk"); + cdk.Aspects.of(this).add(new HcptfPolicyAspect()); } } -function retain(role: iam.CfnRole): void { - role.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN; - role.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN; +function managedPolicy( + scope: Construct, + id: string, + name: string, + policyDocument: object, +): iam.CfnManagedPolicy { + const policy = new iam.CfnManagedPolicy(scope, id, { + managedPolicyName: name, + path: "/tf-managed/", + policyDocument, + }); + retain(policy); + return policy; +} + +function retain(resource: cdk.CfnResource): void { + resource.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN; + resource.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN; } function roleTags(): cdk.CfnTag[] { @@ -114,8 +142,8 @@ function servicesPolicy( deployParams: string, wafParam: string, githubOidc: string, -): iam.PolicyDocument { - return iam.PolicyDocument.fromJson({ +): object { + return { Version: "2012-10-17", Statement: [ { @@ -201,11 +229,11 @@ function servicesPolicy( Resource: "*", }, ], - }); + }; } -function scopedIamPolicy(account: string, deployRole: string, boundary: string): iam.PolicyDocument { - return iam.PolicyDocument.fromJson({ +function scopedIamPolicy(account: string, deployRole: string, boundary: string): object { + return { Version: "2012-10-17", Statement: [ { @@ -343,7 +371,7 @@ function scopedIamPolicy(account: string, deployRole: string, boundary: string): Resource: `arn:aws:iam::${account}:policy/seahaven-*`, }, ], - }); + }; } function planPolicy( @@ -355,8 +383,8 @@ function planPolicy( githubOidc: string, deployRole: string, boundary: string, -): iam.PolicyDocument { - return iam.PolicyDocument.fromJson({ +): object { + return { Version: "2012-10-17", Statement: [ { @@ -467,5 +495,5 @@ function planPolicy( Resource: "*", }, ], - }); + }; }