From 227a5d91a257b33470c943913bde36ba344f3571 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:53:58 +0000 Subject: [PATCH] feat(hcptf): import hcptf-paychex-integrations apply and plan roles into seahaven-hcptf (PLAT-251) (#179) * feat(hcptf): import hcptf-paychex-integrations apply and plan roles into seahaven-hcptf (PLAT-251) PaychexIntegrationsRoles is nested in the prod seahaven-hcptf stack for the paychex-integrations-prod workspace. The two roles already exist and are imported with -c hcptfPaychexImport=true, which names the live inline policies and omits role tags and outputs. The default template replaces the inline policies with managed policies at /tf-managed/: - paychex-integrations-hcptf-iam: the ported scoped IAM document plus CreateRole and the write set on tf-managed/githubdeploy-paychex-integrations (no permissions boundary) and iam:GetOpenIDConnectProvider. TagHcptfRoles is dropped; the roles are no longer Terraform-managed. - paychex-integrations-hcptf-services: the ported services document plus SSM writes on /paychex-integrations/deploy/* and DescribeParameters. - paychex-integrations-hcptf-plan: the ported refresh document plus the deploy role, the GitHub OIDC provider, and the deploy parameters. Trust is unchanged. Every resource is Retain. * docs(hcptf): describe the paychex-integrations import as a sequence * chore(ci): retrigger checks after the GitHub Actions incident --- README.md | 25 +- bin/app.ts | 7 +- .../paychex-integrations-into-hcptf-prod.json | 8 + lib/paychex-integrations-hcptf-stack.ts | 688 ++++++++++++++++++ lib/seahaven-hcptf-stack.ts | 33 +- 5 files changed, 748 insertions(+), 13 deletions(-) create mode 100644 import-maps/paychex-integrations-into-hcptf-prod.json create mode 100644 lib/paychex-integrations-hcptf-stack.ts diff --git a/README.md b/README.md index 9d9f10e..f442249 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ are noted): | `seahaven-view-access` | 328440206208 | us-east-1 | Identity Center group `view`. Permission set `View` is `ViewOnlyAccess` on all five accounts. No data-plane reads and no assume-role. Membership is outside this stack. | | `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-hcptf` | 011934824531, 710827005802 | us-east-1 | payments-dashboard HCP apply and plan roles, scoped policies, and the Lambda boundary in prod and dev. Prod also owns the imported `hcptf-seahaven-site` apply and plan roles (PLAT-225) and the created `hcptf-mta-sts` apply and plan roles (PLAT-243), with policies at `/tf-managed/`. Trust is pinned per workspace. On the dev and prod deploy jobs. Do not create the payments-dashboard or seahaven-site roles by hand. | +| `seahaven-hcptf` | 011934824531, 710827005802 | us-east-1 | payments-dashboard HCP apply and plan roles, scoped policies, and the Lambda boundary in prod and dev. Prod also owns the imported `hcptf-seahaven-site` apply and plan roles (PLAT-225), the created `hcptf-mta-sts` apply and plan roles (PLAT-243), and the imported `hcptf-paychex-integrations` apply and plan roles (PLAT-251), with policies at `/tf-managed/`. Trust is pinned per workspace. On the dev and prod deploy jobs. Do not create the payments-dashboard, seahaven-site, or paychex-integrations roles by hand. | | `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) | @@ -240,15 +240,26 @@ 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. Prod HCP exec roles for -payments-dashboard, seahaven-site, and mta-sts all live in `seahaven-hcptf` -(`lib/seahaven-hcptf-stack.ts`, `lib/seahaven-site-hcptf-stack.ts`, -`lib/mta-sts-hcptf-stack.ts`). A new prod HCP workspace adds a nested -construct there. Dev `seahaven-hcptf` is payments-dashboard only. Both +payments-dashboard, seahaven-site, mta-sts, and paychex-integrations all +live in `seahaven-hcptf` (`lib/seahaven-hcptf-stack.ts`, +`lib/seahaven-site-hcptf-stack.ts`, `lib/mta-sts-hcptf-stack.ts`, +`lib/paychex-integrations-hcptf-stack.ts`). A new prod HCP workspace adds a +nested construct there. Dev `seahaven-hcptf` is payments-dashboard only. Both account copies are on the deploy jobs. The payments deploy creates `payments-dashboard-hcptf-iam`, `payments-dashboard-hcptf-services`, and `payments-dashboard-hcptf-plan`, and removes the inline policies. Do not -create the payments-dashboard roles, the boundary, or the seahaven-site -roles by hand; they are imported. The mta-sts roles are a plain create. +create the payments-dashboard roles, the boundary, the seahaven-site roles, +or the paychex-integrations roles by hand; they are imported. The mta-sts +roles are a plain create. + +The paychex-integrations roles enter this stack by import, in this order: +the paychex-integrations workspace forgets them with `removed` blocks, then +`cdk import seahaven-hcptf -c hcptfPaychexImport=true --resource-mapping import-maps/paychex-integrations-into-hcptf-prod.json` +adopts the two roles, then the default template deploys. That deploy creates +`paychex-integrations-hcptf-iam`, `paychex-integrations-hcptf-services`, and +`paychex-integrations-hcptf-plan` and removes the inline policies. The apply +role may create `githubdeploy-paychex-integrations` at `/tf-managed/` and +write SSM `/paychex-integrations/deploy/*`. **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 3951cf9..cc9a64d 100644 --- a/bin/app.ts +++ b/bin/app.ts @@ -244,10 +244,13 @@ new AppWebAclStack(app, "app-web-acl-prod", { // payments-dashboard HCP roles, scoped policies, and the Lambda boundary. // Prod also includes the imported seahaven-site apply and plan roles // (PLAT-225). A create fails. Import site with `-c hcptfSiteImport=true`. -// Prod also creates the mta-sts apply and plan roles (PLAT-243). +// Prod also creates the mta-sts apply and plan roles (PLAT-243) and +// includes the imported paychex-integrations apply and plan roles +// (PLAT-251). Import paychex with `-c hcptfPaychexImport=true`. // Trust is pinned per workspace. const hcptfPaymentsImport = contextBoolean("hcptfPaymentsImport"); const hcptfSiteImport = contextBoolean("hcptfSiteImport"); +const hcptfPaychexImport = contextBoolean("hcptfPaychexImport"); const paymentsSecrets = ( account: string, suffixes: readonly string[], @@ -276,6 +279,8 @@ new SeahavenHcptfStack(app, "seahaven-hcptf", { includeSeahavenSite: true, siteImportExisting: hcptfSiteImport, includeMtaSts: true, + includePaychexIntegrations: true, + paychexImportExisting: hcptfPaychexImport, }); new SeahavenHcptfStack(app, "seahaven-hcptf-dev", { diff --git a/import-maps/paychex-integrations-into-hcptf-prod.json b/import-maps/paychex-integrations-into-hcptf-prod.json new file mode 100644 index 0000000..260de6f --- /dev/null +++ b/import-maps/paychex-integrations-into-hcptf-prod.json @@ -0,0 +1,8 @@ +{ + "PaychexIntegrationsApplyRoleF0B199BB": { + "RoleName": "hcptf-paychex-integrations" + }, + "PaychexIntegrationsPlanRole71013409": { + "RoleName": "hcptf-paychex-integrations-plan" + } +} diff --git a/lib/paychex-integrations-hcptf-stack.ts b/lib/paychex-integrations-hcptf-stack.ts new file mode 100644 index 0000000..ec2e2bc --- /dev/null +++ b/lib/paychex-integrations-hcptf-stack.ts @@ -0,0 +1,688 @@ +import * as cdk from "aws-cdk-lib"; +import * as iam from "aws-cdk-lib/aws-iam"; +import { Construct } from "constructs"; + +/** + * Prod exec roles for the paychex-integrations HCP workspace (PLAT-251). + * + * Nested in the prod seahaven-hcptf stack. `hcptf-paychex-integrations` and + * `hcptf-paychex-integrations-plan` already exist. They were created by + * create-hcptf-bootstrap-roles.sh and then managed by the paychex-integrations + * workspace itself through a bootstrap credential swap. That workspace forgets + * them with `removed` blocks before this construct imports them. Do not create + * them. A plain create fails because the roles already exist. + * + * Import identifiers are the two role names. Construct ids stay ApplyRole and + * PlanRole under PaychexIntegrations. The three /tf-managed/ managed policies + * do not exist before the deploy that follows the import. + * + * `importExisting` is the `-c hcptfPaychexImport=true` template. It names the + * roles' live inline policies (`paychex-integrations-services`, + * `scoped-iam-management`, `paychex-integrations-plan-refresh`) so the + * following deploy can delete them, and it omits role tags and the role ARN + * outputs. CloudFormation rejects both on an IAM role import. The default + * template is the managed-policy state. + * + * Beyond the ported documents, the apply role can create and manage + * `githubdeploy-paychex-integrations` at /tf-managed/ with no permissions + * boundary, and can write the deploy contract under SSM + * /paychex-integrations/deploy/*. The plan role can refresh both. + */ +export interface PaychexIntegrationsRolesProps { + /** Synthesize the import template. Set from `-c hcptfPaychexImport=true`. */ + importExisting?: boolean; +} + +const VIEW_ONLY = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"; +const WORKSPACE = + "organization:seahaven:project:seahaven-prod:workspace:paychex-integrations-prod"; + +export class PaychexIntegrationsRoles extends Construct { + constructor(scope: Construct, id: string, props: PaychexIntegrationsRolesProps = {}) { + super(scope, id); + + const importing = props.importExisting === true; + // Overrides the parent stack's Project=payments-dashboard tag. + cdk.Tags.of(this).add("Project", "paychex-integrations", { priority: 200 }); + if (importing) { + const roleOnly = { priority: 300, includeResourceTypes: ["AWS::IAM::Role"] }; + cdk.Tags.of(this).remove("Project", roleOnly); + cdk.Tags.of(this).remove("Owner", roleOnly); + cdk.Tags.of(this).remove("ManagedBy", roleOnly); + } + + const account = cdk.Stack.of(this).account; + const hcpOidc = `arn:aws:iam::${account}:oidc-provider/app.terraform.io`; + const deployRole = `arn:aws:iam::${account}:role/tf-managed/githubdeploy-paychex-integrations`; + const deployParams = `arn:aws:ssm:us-east-1:${account}:parameter/paychex-integrations/deploy/*`; + + const iamDocument = scopedIamPolicy(account, deployRole); + const servicesDocument = servicesPolicy(account, deployParams); + const planDocument = planPolicy(account, deployRole, deployParams); + + const apply = new iam.CfnRole(this, "ApplyRole", { + roleName: "hcptf-paychex-integrations", + maxSessionDuration: 3600, + assumeRolePolicyDocument: trust(hcpOidc, "apply"), + ...(importing + ? { + policies: [ + { policyName: "paychex-integrations-services", policyDocument: servicesDocument }, + { policyName: "scoped-iam-management", policyDocument: iamDocument }, + ], + } + : { + managedPolicyArns: [ + managedPolicy(this, "IamPolicy", "paychex-integrations-hcptf-iam", iamDocument).ref, + managedPolicy( + this, + "ServicesPolicy", + "paychex-integrations-hcptf-services", + servicesDocument, + ).ref, + ], + tags: roleTags(), + }), + }); + retain(apply); + + const plan = new iam.CfnRole(this, "PlanRole", { + roleName: "hcptf-paychex-integrations-plan", + maxSessionDuration: 3600, + assumeRolePolicyDocument: trust(hcpOidc, "plan"), + ...(importing + ? { + managedPolicyArns: [VIEW_ONLY], + policies: [ + { policyName: "paychex-integrations-plan-refresh", policyDocument: planDocument }, + ], + } + : { + managedPolicyArns: [ + VIEW_ONLY, + managedPolicy(this, "PlanPolicy", "paychex-integrations-hcptf-plan", planDocument).ref, + ], + tags: roleTags(), + }), + }); + retain(plan); + + if (!importing) { + const applyArn = new cdk.CfnOutput(this, "ApplyRoleArn", { value: apply.attrArn }); + const planArn = new cdk.CfnOutput(this, "PlanRoleArn", { value: plan.attrArn }); + applyArn.overrideLogicalId("PaychexIntegrationsApplyRoleArn"); + planArn.overrideLogicalId("PaychexIntegrationsPlanRoleArn"); + } + } +} + +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[] { + return [ + { key: "Project", value: "paychex-integrations" }, + { key: "Owner", value: "adam@seahavenind.com" }, + { key: "ManagedBy", value: "cdk" }, + ]; +} + +function trust(providerArn: string, phase: "apply" | "plan"): object { + return { + Version: "2012-10-17", + Statement: [ + { + Sid: phase === "apply" ? "HcpApply" : "HcpPlan", + Effect: "Allow", + Action: "sts:AssumeRoleWithWebIdentity", + Principal: { Federated: providerArn }, + Condition: { + StringEquals: { + "app.terraform.io:aud": "aws.workload.identity", + "app.terraform.io:sub": `${WORKSPACE}:run_phase:${phase}`, + }, + }, + }, + ], + }; +} + +/** Ported from paychex-integrations terraform/hcp_iam.tf hcptf_scoped_iam plus the deploy role. */ +function scopedIamPolicy(account: string, deployRole: string): object { + const execRoles = `arn:aws:iam::${account}:role/tf-managed/paychex-*`; + const execBoundaries = [ + `arn:aws:iam::${account}:policy/tf-managed/paychex-*`, + `arn:aws:iam::${account}:policy/seahaven-lambda-execution-boundary-paychex-integrations`, + ]; + return { + Version: "2012-10-17", + Statement: [ + { + Sid: "DenyCreatePolicy", + Effect: "Deny", + Action: ["iam:CreatePolicy", "iam:CreatePolicyVersion"], + Resource: "*", + }, + { + Sid: "CreateExecRoleWithBoundary", + Effect: "Allow", + Action: "iam:CreateRole", + Resource: execRoles, + Condition: { StringLike: { "iam:PermissionsBoundary": execBoundaries } }, + }, + { + Sid: "MutateExecRoleWithBoundary", + Effect: "Allow", + Action: ["iam:AttachRolePolicy", "iam:PutRolePolicy", "iam:PutRolePermissionsBoundary"], + Resource: execRoles, + Condition: { StringLike: { "iam:PermissionsBoundary": execBoundaries } }, + }, + { + Sid: "WriteExecRoles", + Effect: "Allow", + Action: [ + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:DetachRolePolicy", + "iam:TagRole", + "iam:UntagRole", + "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + "iam:UpdateRoleDescription", + ], + Resource: execRoles, + }, + { + Sid: "PassExecRolesToLambda", + Effect: "Allow", + Action: "iam:PassRole", + Resource: execRoles, + Condition: { StringEquals: { "iam:PassedToService": "lambda.amazonaws.com" } }, + }, + { + Sid: "PassPayrollScheduleToScheduler", + Effect: "Allow", + Action: "iam:PassRole", + Resource: `arn:aws:iam::${account}:role/tf-managed/paychex-payroll-schedule-invoke`, + Condition: { StringEquals: { "iam:PassedToService": "scheduler.amazonaws.com" } }, + }, + { + // GitHub Actions deploy role, owned by the workspace. Path /tf-managed/ + // keeps it outside DenySelfMutation's role/githubdeploy-* pattern. No + // permissions boundary: it is not a Lambda execution role. + Sid: "CreateDeployRole", + Effect: "Allow", + Action: "iam:CreateRole", + Resource: deployRole, + Condition: { Null: { "iam:PermissionsBoundary": "true" } }, + }, + { + Sid: "WriteDeployRole", + Effect: "Allow", + Action: [ + "iam:AttachRolePolicy", + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:TagRole", + "iam:UntagRole", + "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + "iam:UpdateRoleDescription", + ], + Resource: deployRole, + }, + { + Sid: "IamReadOnly", + Effect: "Allow", + Action: [ + "iam:GetOpenIDConnectProvider", + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListAttachedRolePolicies", + "iam:ListInstanceProfilesForRole", + "iam:ListPolicies", + "iam:ListPolicyTags", + "iam:ListPolicyVersions", + "iam:ListRolePolicies", + "iam:ListRoles", + "iam:ListRoleTags", + ], + 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::${account}:role/hcptf-*`, + `arn:aws:iam::${account}:role/github-cfn-execution-role`, + `arn:aws:iam::${account}:role/githubdeploy-*`, + `arn:aws:iam::${account}:role/cdk-hnb659fds-*`, + `arn:aws:iam::${account}:role/OrganizationAccountAccessRole`, + `arn:aws:iam::${account}:role/seahaven-*`, + ], + }, + { + Sid: "DenyBoundaryTampering", + Effect: "Deny", + Action: ["iam:DeleteRolePermissionsBoundary", "iam:DeleteUserPermissionsBoundary"], + Resource: [`arn:aws:iam::${account}:role/*`, `arn:aws:iam::${account}:user/*`], + }, + { + Sid: "DenyBoundaryPolicyEdit", + Effect: "Deny", + Action: [ + "iam:CreatePolicyVersion", + "iam:DeletePolicy", + "iam:DeletePolicyVersion", + "iam:SetDefaultPolicyVersion", + ], + Resource: `arn:aws:iam::${account}:policy/seahaven-*`, + }, + ], + }; +} + +/** Ported from paychex-integrations terraform/hcp_iam.tf hcptf_apply_services plus the deploy contract. */ +function servicesPolicy(account: string, deployParams: string): object { + const functions = `arn:aws:lambda:us-east-1:${account}:function:paychex-*`; + const artifacts = `arn:aws:s3:::paychex-integrations-artifacts-${account}`; + return { + Version: "2012-10-17", + Statement: [ + { + Sid: "LambdaAll", + Effect: "Allow", + Action: "lambda:*", + Resource: functions, + }, + { + Sid: "LambdaEventSourceMappingRead", + Effect: "Allow", + Action: [ + "lambda:GetEventSourceMapping", + "lambda:ListTags", + "lambda:TagResource", + "lambda:UntagResource", + ], + Resource: "*", + }, + { + Sid: "LambdaEventSourceMappings", + Effect: "Allow", + Action: [ + "lambda:CreateEventSourceMapping", + "lambda:DeleteEventSourceMapping", + "lambda:UpdateEventSourceMapping", + ], + Resource: "*", + Condition: { "ForAnyValue:StringLike": { "lambda:FunctionArn": functions } }, + }, + { + Sid: "LambdaList", + Effect: "Allow", + Action: [ + "lambda:ListFunctions", + "lambda:ListLayers", + "lambda:ListEventSourceMappings", + "lambda:GetAccountSettings", + ], + Resource: "*", + }, + { + Sid: "CloudWatchLogs", + Effect: "Allow", + Action: [ + "logs:CreateLogGroup", + "logs:DeleteLogGroup", + "logs:PutRetentionPolicy", + "logs:DeleteRetentionPolicy", + "logs:TagResource", + "logs:UntagResource", + "logs:ListTagsForResource", + ], + Resource: [ + `arn:aws:logs:us-east-1:${account}:log-group:/aws/lambda/paychex-*`, + `arn:aws:logs:us-east-1:${account}:log-group:/aws/apigateway/paychex-webhooks`, + `arn:aws:logs:us-east-1:${account}:log-group:/aws/apigateway/paychex-webhooks:*`, + ], + }, + { + Sid: "CloudWatchLogsDescribe", + Effect: "Allow", + Action: "logs:DescribeLogGroups", + Resource: "*", + }, + { + // HTTP API access logging is delivered through CloudWatch vended logs. + // None of these actions accept a resource ARN. + Sid: "CloudWatchLogsDelivery", + Effect: "Allow", + Action: [ + "logs:CreateLogDelivery", + "logs:GetLogDelivery", + "logs:UpdateLogDelivery", + "logs:DeleteLogDelivery", + "logs:ListLogDeliveries", + "logs:PutResourcePolicy", + "logs:DescribeResourcePolicies", + ], + Resource: "*", + }, + { + Sid: "LambdaArtifactsBucket", + Effect: "Allow", + Action: "s3:*", + Resource: [artifacts, `${artifacts}/*`], + }, + { + Sid: "CloudWatchAlarms", + Effect: "Allow", + Action: "cloudwatch:*", + Resource: `arn:aws:cloudwatch:us-east-1:${account}:alarm:paychex-*`, + }, + { + Sid: "SiteAlertsSns", + Effect: "Allow", + Action: ["sns:Publish", "sns:GetTopicAttributes"], + Resource: `arn:aws:sns:us-east-1:${account}:site-alerts`, + }, + { + Sid: "PaychexSecretShell", + Effect: "Allow", + Action: [ + "secretsmanager:DeleteSecret", + "secretsmanager:DescribeSecret", + "secretsmanager:GetResourcePolicy", + "secretsmanager:PutResourcePolicy", + "secretsmanager:DeleteResourcePolicy", + "secretsmanager:TagResource", + "secretsmanager:UntagResource", + ], + Resource: `arn:aws:secretsmanager:us-east-1:${account}:secret:paychex-integrations/*`, + }, + { + Sid: "PaychexSecretCreate", + Effect: "Allow", + Action: "secretsmanager:CreateSecret", + Resource: "*", + Condition: { StringLike: { "secretsmanager:Name": "paychex-integrations/*" } }, + }, + { + Sid: "DynamoDBTable", + Effect: "Allow", + Action: "dynamodb:*", + Resource: [ + `arn:aws:dynamodb:us-east-1:${account}:table/paychex-worker-ledger`, + `arn:aws:dynamodb:us-east-1:${account}:table/paychex-worker-ledger/index/*`, + `arn:aws:dynamodb:us-east-1:${account}:table/paychex-webhook-notifications`, + `arn:aws:dynamodb:us-east-1:${account}:table/paychex-webhook-notifications/index/*`, + `arn:aws:dynamodb:us-east-1:${account}:table/paychex-payroll-notices`, + `arn:aws:dynamodb:us-east-1:${account}:table/paychex-payroll-notices/index/*`, + `arn:aws:dynamodb:us-east-1:${account}:table/paychex-checkcomponents-posted`, + `arn:aws:dynamodb:us-east-1:${account}:table/paychex-checkcomponents-posted/index/*`, + `arn:aws:dynamodb:us-east-1:${account}:table/paychex-checkcomponents-period`, + `arn:aws:dynamodb:us-east-1:${account}:table/paychex-checkcomponents-period/index/*`, + ], + }, + { + Sid: "DynamoDBList", + Effect: "Allow", + Action: "dynamodb:ListTables", + Resource: "*", + }, + { + Sid: "SqsQueues", + Effect: "Allow", + Action: "sqs:*", + Resource: queueArns(account), + }, + { + Sid: "SqsList", + Effect: "Allow", + Action: "sqs:ListQueues", + Resource: "*", + }, + { + Sid: "HttpApi", + 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/*", + ], + }, + { + Sid: "PayrollSchedules", + Effect: "Allow", + Action: [ + "scheduler:CreateSchedule", + "scheduler:UpdateSchedule", + "scheduler:DeleteSchedule", + "scheduler:GetSchedule", + "scheduler:ListTagsForResource", + "scheduler:TagResource", + "scheduler:UntagResource", + ], + Resource: scheduleArns(account), + }, + { + // Deploy contract read by the thin deploy.yaml caller. + Sid: "WriteDeployContract", + Effect: "Allow", + Action: [ + "ssm:AddTagsToResource", + "ssm:DeleteParameter", + "ssm:GetParameter", + "ssm:GetParameters", + "ssm:ListTagsForResource", + "ssm:PutParameter", + "ssm:RemoveTagsFromResource", + ], + Resource: deployParams, + }, + { + // DescribeParameters accepts only Resource "*". + Sid: "DescribeParameters", + Effect: "Allow", + Action: "ssm:DescribeParameters", + Resource: "*", + }, + ], + }; +} + +/** Ported from paychex-integrations terraform/hcp_iam.tf hcptf_plan_refresh plus the deploy role and contract. */ +function planPolicy(account: string, deployRole: string, deployParams: string): object { + const artifacts = `arn:aws:s3:::paychex-integrations-artifacts-${account}`; + return { + Version: "2012-10-17", + Statement: [ + { + Sid: "RefreshIamRoles", + Effect: "Allow", + Action: [ + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListRolePolicies", + "iam:ListAttachedRolePolicies", + ], + Resource: [ + `arn:aws:iam::${account}:role/tf-managed/paychex-*`, + deployRole, + `arn:aws:iam::${account}:role/hcptf-paychex-integrations`, + `arn:aws:iam::${account}:role/hcptf-paychex-integrations-plan`, + ], + }, + { + Sid: "RefreshGithubOidcProvider", + Effect: "Allow", + Action: "iam:GetOpenIDConnectProvider", + Resource: `arn:aws:iam::${account}:oidc-provider/token.actions.githubusercontent.com`, + }, + { + Sid: "RefreshManagedPolicies", + Effect: "Allow", + Action: ["iam:GetPolicy", "iam:GetPolicyVersion"], + Resource: "*", + }, + { + Sid: "RefreshLambda", + Effect: "Allow", + Action: "lambda:Get*", + Resource: `arn:aws:lambda:us-east-1:${account}:function:paychex-*`, + }, + { + Sid: "RefreshLambdaList", + Effect: "Allow", + Action: [ + "lambda:ListFunctions", + "lambda:ListEventSourceMappings", + "lambda:GetEventSourceMapping", + "lambda:GetAccountSettings", + ], + Resource: "*", + }, + { + Sid: "RefreshArtifactsBucket", + Effect: "Allow", + Action: ["s3:Get*", "s3:ListBucket"], + Resource: [artifacts, `${artifacts}/*`], + }, + { + Sid: "RefreshCloudWatchAlarms", + Effect: "Allow", + Action: ["cloudwatch:DescribeAlarms", "cloudwatch:ListTagsForResource"], + Resource: `arn:aws:cloudwatch:us-east-1:${account}:alarm:paychex-*`, + }, + { + Sid: "RefreshLogs", + Effect: "Allow", + Action: ["logs:DescribeLogGroups", "logs:ListTagsForResource"], + Resource: "*", + }, + { + Sid: "RefreshSecrets", + Effect: "Allow", + Action: [ + "secretsmanager:DescribeSecret", + "secretsmanager:GetResourcePolicy", + "secretsmanager:ListSecretVersionIds", + ], + Resource: `arn:aws:secretsmanager:us-east-1:${account}:secret:paychex-integrations/*`, + }, + { + Sid: "RefreshDynamoDB", + Effect: "Allow", + Action: [ + "dynamodb:DescribeTable", + "dynamodb:DescribeTimeToLive", + "dynamodb:DescribeContinuousBackups", + "dynamodb:ListTagsOfResource", + ], + Resource: [ + `arn:aws:dynamodb:us-east-1:${account}:table/paychex-worker-ledger`, + `arn:aws:dynamodb:us-east-1:${account}:table/paychex-webhook-notifications`, + `arn:aws:dynamodb:us-east-1:${account}:table/paychex-payroll-notices`, + `arn:aws:dynamodb:us-east-1:${account}:table/paychex-checkcomponents-posted`, + `arn:aws:dynamodb:us-east-1:${account}:table/paychex-checkcomponents-period`, + ], + }, + { + Sid: "RefreshSqs", + Effect: "Allow", + Action: ["sqs:GetQueueAttributes", "sqs:GetQueueUrl", "sqs:ListQueueTags"], + Resource: queueArns(account), + }, + { + Sid: "RefreshSqsList", + Effect: "Allow", + Action: "sqs:ListQueues", + 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: "RefreshPayrollSchedules", + Effect: "Allow", + Action: ["scheduler:GetSchedule", "scheduler:ListTagsForResource"], + Resource: scheduleArns(account), + }, + { + Sid: "RefreshDeployContract", + Effect: "Allow", + Action: ["ssm:GetParameter", "ssm:GetParameters", "ssm:ListTagsForResource"], + Resource: deployParams, + }, + { + // DescribeParameters accepts only Resource "*". The AWS provider + // calls it while refreshing aws_ssm_parameter. + Sid: "DescribeParameters", + Effect: "Allow", + Action: "ssm:DescribeParameters", + Resource: "*", + }, + ], + }; +} + +function queueArns(account: string): string[] { + return [ + "paychex-webhook-events", + "paychex-webhook-events-dlq", + "paychex-login-delay", + "paychex-login-delay-dlq", + "paychex-checkcomponents", + "paychex-checkcomponents-dlq", + "paychex-payroll-schedule", + "paychex-payroll-schedule-dlq", + ].map((name) => `arn:aws:sqs:us-east-1:${account}:${name}`); +} + +function scheduleArns(account: string): string[] { + return [ + `arn:aws:scheduler:us-east-1:${account}:schedule/default/paychex-payroll-monday`, + `arn:aws:scheduler:us-east-1:${account}:schedule/default/paychex-payroll-thursday`, + ]; +} diff --git a/lib/seahaven-hcptf-stack.ts b/lib/seahaven-hcptf-stack.ts index 92a0daf..70d6232 100644 --- a/lib/seahaven-hcptf-stack.ts +++ b/lib/seahaven-hcptf-stack.ts @@ -3,12 +3,13 @@ import * as iam from "aws-cdk-lib/aws-iam"; import { Construct } from "constructs"; import { HcptfPolicyAspect } from "./hcptf-policy-aspect"; import { MtaStsRoles } from "./mta-sts-hcptf-stack"; +import { PaychexIntegrationsRoles } from "./paychex-integrations-hcptf-stack"; import { SeahavenSiteRoles } from "./seahaven-site-hcptf-stack"; /** * HCP exec roles. One stack per account. Prod is 011934824531 and also - * hosts the seahaven-site and mta-sts apply and plan roles. Dev is - * 710827005802 and stays payments-dashboard only. + * hosts the seahaven-site, mta-sts, and paychex-integrations apply and plan + * roles. Dev is 710827005802 and stays payments-dashboard only. * * payments-dashboard: workspace payments-dashboard-prod, project * seahaven-prod, or workspace payments-dashboard-dev, project seahaven-dev. @@ -51,13 +52,28 @@ export interface SeahavenHcptfStackProps extends cdk.StackProps { siteImportExisting?: boolean; /** Prod only. Fold mta-sts exec roles into this stack (PLAT-243). */ includeMtaSts?: boolean; + /** Prod only. Fold paychex-integrations exec roles into this stack (PLAT-251). */ + includePaychexIntegrations?: boolean; + /** + * Synthesize the paychex-integrations import template. Set from + * `-c hcptfPaychexImport=true`. Names the live inline policies and omits + * role tags and outputs. + */ + paychexImportExisting?: boolean; } export class SeahavenHcptfStack extends cdk.Stack { constructor(scope: Construct, id: string, props: SeahavenHcptfStackProps) { super(scope, id, props); - if (props.importExisting && props.siteImportExisting) { - throw new Error("hcptfPaymentsImport and hcptfSiteImport cannot both be set"); + const importFlags = [ + props.importExisting, + props.siteImportExisting, + props.paychexImportExisting, + ].filter((flag) => flag === true); + if (importFlags.length > 1) { + throw new Error( + "hcptfPaymentsImport, hcptfSiteImport, and hcptfPaychexImport cannot be combined", + ); } paymentsDashboard(this, props); if (props.includeSeahavenSite) { @@ -68,7 +84,14 @@ export class SeahavenHcptfStack extends cdk.Stack { if (props.includeMtaSts) { new MtaStsRoles(this, "MtaSts"); } - cdk.Aspects.of(this).add(new HcptfPolicyAspect(props.importExisting === true)); + if (props.includePaychexIntegrations) { + new PaychexIntegrationsRoles(this, "PaychexIntegrations", { + importExisting: props.paychexImportExisting === true, + }); + } + const allowInlinePolicies = + props.importExisting === true || props.paychexImportExisting === true; + cdk.Aspects.of(this).add(new HcptfPolicyAspect(allowInlinePolicies)); } }