seahaven-org-baseline/lib/seahaven-site-hcptf-stack.ts
Adam Moussa 031e1d1a3b
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
refactor(iam): fold seahaven-site roles into seahaven-hcptf (#175)
Prod HCP exec roles for seahaven-site now live in the same stack as payments-dashboard so role ownership is one stack.
2026-10-02 17:30:52 +00:00

516 lines
15 KiB
TypeScript

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 seahaven-site HCP workspace (PLAT-225).
*
* Nested in the prod seahaven-hcptf stack. These roles already exist.
* Do not create them. A plain create fails because the roles already exist.
*
* Import identifiers are the role names `hcptf-seahaven-site` and
* `hcptf-seahaven-site-plan`, plus the three /tf-managed/ policy ARNs.
* Construct ids stay ApplyRole and PlanRole under SeahavenSite.
* Inline policies are managed policies at /tf-managed/. Do not rename
* the roles.
*
* `importExisting` is the `-c hcptfSiteImport=true` template. It omits
* role tags and the role ARN outputs. CloudFormation rejects both on an
* IAM role import. The steady-state template adds them back.
*/
export interface SeahavenSiteRolesProps {
/** Omit role tags and outputs. Set from `-c hcptfSiteImport=true`. */
importExisting?: boolean;
}
export class SeahavenSiteRoles extends Construct {
constructor(scope: Construct, id: string, props: SeahavenSiteRolesProps = {}) {
super(scope, id);
const importing = props.importExisting === true;
// Overrides the parent stack's Project=payments-dashboard tag.
cdk.Tags.of(this).add("Project", "seahaven-site", { 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 deployRole = `arn:aws:iam::${account}:role/tf-managed/githubdeploy-seahaven-site`;
const boundary = `arn:aws:iam::${account}:policy/tf-managed/seahaven-site-githubdeploy-boundary`;
const bucket = "arn:aws:s3:::seahaven-site-prod";
const functionArn = `arn:aws:cloudfront::${account}:function/seahaven-site-prod-directory-index`;
const deployParams = `arn:aws:ssm:us-east-1:${account}:parameter/seahaven-site/deploy/*`;
const wafParam = `arn:aws:ssm:us-east-1:${account}:parameter/seahaven/waf/app-web-acl-arn`;
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: [iamPolicy.ref, services.ref],
...(importing ? {} : { tags: roleTags() }),
});
retain(apply);
const plan = new iam.CfnRole(this, "PlanRole", {
roleName: "hcptf-seahaven-site-plan",
maxSessionDuration: 3600,
assumeRolePolicyDocument: trust(hcpOidc, "plan"),
managedPolicyArns: [
"arn:aws:iam::aws:policy/job-function/ViewOnlyAccess",
planRefresh.ref,
],
...(importing ? {} : { 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("SeahavenSiteApplyRoleArn");
planArn.overrideLogicalId("SeahavenSitePlanRoleArn");
}
}
}
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: "seahaven-site" },
{ key: "Owner", value: "adam@seahavenind.com" },
{ key: "ManagedBy", value: "cdk" },
];
}
function trust(providerArn: string, phase: "apply" | "plan"): iam.PolicyDocument {
return iam.PolicyDocument.fromJson({
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":
`organization:seahaven:project:seahaven-prod:workspace:seahaven-site-prod:run_phase:${phase}`,
},
},
},
],
});
}
function servicesPolicy(
bucket: string,
deployParams: string,
wafParam: string,
githubOidc: string,
): object {
return {
Version: "2012-10-17",
Statement: [
{
Sid: "OriginBucket",
Effect: "Allow",
Action: "s3:*",
Resource: [bucket, `${bucket}/*`],
},
{
Sid: "ReadGithubOidcProvider",
Effect: "Allow",
Action: "iam:GetOpenIDConnectProvider",
Resource: githubOidc,
},
{
Sid: "CloudFrontManage",
Effect: "Allow",
Action: "cloudfront:*",
Resource: "*",
},
{
Sid: "AcmCreate",
Effect: "Allow",
Action: "acm:RequestCertificate",
Resource: "*",
Condition: { StringEquals: { "aws:RequestTag/Project": "seahaven-site" } },
},
{
Sid: "AcmList",
Effect: "Allow",
Action: ["acm:ListCertificates", "acm:ListTagsForCertificate"],
Resource: "*",
},
{
Sid: "AcmManageTagged",
Effect: "Allow",
Action: [
"acm:AddTagsToCertificate",
"acm:DeleteCertificate",
"acm:DescribeCertificate",
"acm:GetCertificate",
"acm:RemoveTagsFromCertificate",
"acm:RenewCertificate",
],
Resource: "*",
Condition: { StringEquals: { "aws:ResourceTag/Project": "seahaven-site" } },
},
{
Sid: "ReadAppWebAclSsm",
Effect: "Allow",
Action: ["ssm:GetParameter", "ssm:GetParameters"],
Resource: wafParam,
},
{
Sid: "WriteDeployContract",
Effect: "Allow",
Action: [
"ssm:AddTagsToResource",
"ssm:DeleteParameter",
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:ListTagsForResource",
"ssm:PutParameter",
"ssm:RemoveTagsFromResource",
],
Resource: deployParams,
},
{
Sid: "DescribeParameters",
Effect: "Allow",
Action: "ssm:DescribeParameters",
Resource: "*",
},
{
Sid: "ReadWafWebAcl",
Effect: "Allow",
Action: [
"wafv2:GetWebACL",
"wafv2:GetWebACLForResource",
"wafv2:ListResourcesForWebACL",
"wafv2:ListWebACLs",
],
Resource: "*",
},
],
};
}
function scopedIamPolicy(account: string, deployRole: string, boundary: string): object {
return {
Version: "2012-10-17",
Statement: [
{
Sid: "DenyUntaggedCreatePolicy",
Effect: "Deny",
Action: "iam:CreatePolicy",
Resource: "*",
Condition: { Null: { "aws:RequestTag/BoundaryFor": "true" } },
},
{
Sid: "DenyOtherCreatePolicy",
Effect: "Deny",
Action: "iam:CreatePolicy",
Resource: "*",
Condition: {
StringNotEquals: { "aws:RequestTag/BoundaryFor": "githubdeploy-seahaven-site" },
},
},
{
Sid: "DenyOtherPolicyVersions",
Effect: "Deny",
Action: [
"iam:CreatePolicyVersion",
"iam:DeletePolicy",
"iam:DeletePolicyVersion",
"iam:SetDefaultPolicyVersion",
],
NotResource: boundary,
},
{
Sid: "CreateDeployBoundary",
Effect: "Allow",
Action: "iam:CreatePolicy",
Resource: "*",
Condition: {
StringEquals: { "aws:RequestTag/BoundaryFor": "githubdeploy-seahaven-site" },
},
},
{
Sid: "ManageDeployBoundary",
Effect: "Allow",
Action: [
"iam:CreatePolicyVersion",
"iam:DeletePolicy",
"iam:DeletePolicyVersion",
"iam:GetPolicy",
"iam:GetPolicyVersion",
"iam:ListPolicyTags",
"iam:SetDefaultPolicyVersion",
"iam:TagPolicy",
"iam:UntagPolicy",
],
Resource: boundary,
},
{
Sid: "WriteDeployRoles",
Effect: "Allow",
Action: [
"iam:AttachRolePolicy",
"iam:DeleteRolePolicy",
"iam:DetachRolePolicy",
"iam:PutRolePolicy",
"iam:TagRole",
"iam:UntagRole",
"iam:UpdateAssumeRolePolicy",
"iam:UpdateRole",
"iam:UpdateRoleDescription",
],
Resource: deployRole,
},
{
Sid: "PutDeployRoleBoundary",
Effect: "Allow",
Action: "iam:PutRolePermissionsBoundary",
Resource: deployRole,
Condition: { StringEquals: { "iam:PermissionsBoundary": boundary } },
},
{
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::${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-*`,
},
],
};
}
function planPolicy(
account: string,
bucket: string,
functionArn: string,
deployParams: string,
wafParam: string,
githubOidc: string,
deployRole: string,
boundary: string,
): object {
return {
Version: "2012-10-17",
Statement: [
{
Sid: "RefreshDeployRole",
Effect: "Allow",
Action: [
"iam:GetRole",
"iam:GetRolePolicy",
"iam:ListAttachedRolePolicies",
"iam:ListRolePolicies",
"iam:ListRoleTags",
],
Resource: [
deployRole,
`arn:aws:iam::${account}:role/hcptf-seahaven-site`,
`arn:aws:iam::${account}:role/hcptf-seahaven-site-plan`,
],
},
{
Sid: "RefreshGithubOidcProvider",
Effect: "Allow",
Action: "iam:GetOpenIDConnectProvider",
Resource: githubOidc,
},
{
Sid: "RefreshManagedPolicies",
Effect: "Allow",
Action: ["iam:GetPolicy", "iam:GetPolicyVersion"],
Resource: "*",
},
{
Sid: "RefreshDeployBoundaryTags",
Effect: "Allow",
Action: "iam:ListPolicyTags",
Resource: boundary,
},
{
Sid: "RefreshOriginBucket",
Effect: "Allow",
Action: [
"s3:GetAccelerateConfiguration",
"s3:GetBucketAcl",
"s3:GetBucketCORS",
"s3:GetBucketLocation",
"s3:GetBucketLogging",
"s3:GetBucketObjectLockConfiguration",
"s3:GetBucketOwnershipControls",
"s3:GetBucketPolicy",
"s3:GetBucketPolicyStatus",
"s3:GetBucketPublicAccessBlock",
"s3:GetBucketRequestPayment",
"s3:GetBucketTagging",
"s3:GetBucketVersioning",
"s3:GetBucketWebsite",
"s3:GetEncryptionConfiguration",
"s3:GetLifecycleConfiguration",
"s3:GetReplicationConfiguration",
"s3:ListBucket",
],
Resource: [bucket, `${bucket}/*`],
},
{
Sid: "RefreshCloudFront",
Effect: "Allow",
Action: [
"cloudfront:GetDistribution",
"cloudfront:GetDistributionConfig",
"cloudfront:GetOriginAccessControl",
"cloudfront:ListTagsForResource",
],
Resource: "*",
},
{
Sid: "RefreshCloudFrontFunction",
Effect: "Allow",
Action: ["cloudfront:DescribeFunction", "cloudfront:GetFunction"],
Resource: functionArn,
},
{
Sid: "RefreshAcm",
Effect: "Allow",
Action: [
"acm:DescribeCertificate",
"acm:GetCertificate",
"acm:ListCertificates",
"acm:ListTagsForCertificate",
],
Resource: "*",
},
{
Sid: "RefreshAppWebAclSsm",
Effect: "Allow",
Action: ["ssm:GetParameter", "ssm:GetParameters", "ssm:ListTagsForResource"],
Resource: [wafParam, deployParams],
},
{
// DescribeParameters accepts only Resource "*". The AWS provider
// calls it while refreshing aws_ssm_parameter.
Sid: "DescribeParameters",
Effect: "Allow",
Action: "ssm:DescribeParameters",
Resource: "*",
},
{
Sid: "RefreshWafWebAcl",
Effect: "Allow",
Action: ["wafv2:GetWebACL", "wafv2:ListWebACLs"],
Resource: "*",
},
],
};
}