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