feat(hcptf): import hcptf-paychex-integrations apply and plan roles into seahaven-hcptf (PLAT-251) (#179)
Some checks are pending
Deploy / deploy-management (push) Waiting to run
Deploy / deploy-external-dev (push) Waiting to run
Deploy / deploy-security (push) Waiting to run
Deploy / deploy-dev (push) Waiting to run
Deploy / deploy-prod (push) Waiting to run

* 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
This commit is contained in:
Adam Moussa 2026-10-05 21:53:58 +00:00 • committed by GitHub
parent a43ec1f0f5
commit 227a5d91a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 748 additions and 13 deletions

View file

@ -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-<stack>` pairs, Lambda exec
roles, `policy/tf-managed/<stack>` ceilings) in the console. Do not append
new prod/dev `hcptf-<stack>` 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

View file

@ -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", {

View file

@ -0,0 +1,8 @@
{
"PaychexIntegrationsApplyRoleF0B199BB": {
"RoleName": "hcptf-paychex-integrations"
},
"PaychexIntegrationsPlanRole71013409": {
"RoleName": "hcptf-paychex-integrations-plan"
}
}

View file

@ -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`,
];
}

View file

@ -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));
}
}