seahaven-org-baseline/lib/terraform-substrate/shoc-frontend-resources.ts
Adam Moussa a492a45e07
Some checks failed
Deploy / deploy-management (push) Has been cancelled
Deploy / deploy-external-dev (push) Has been cancelled
Deploy / deploy-security (push) Has been cancelled
Deploy / deploy-dev (push) Has been cancelled
Deploy / deploy-prod (push) Has been cancelled
chore(iam): remove frontend tf-poc substrate after teardown (PLAT-194) (#146)
2026-09-11 22:17:00 +00:00

756 lines
23 KiB
TypeScript

import * as cdk from "aws-cdk-lib";
import * as cfninc from "aws-cdk-lib/cloudformation-include";
import * as iam from "aws-cdk-lib/aws-iam";
import { CfnTag } from "aws-cdk-lib/core";
import { Construct } from "constructs";
const ACCOUNT_ID = "396287094661";
const CACHE_POLICY_ID = "658327ea-f89d-4fab-a63d-7e88639e58f6";
const SHARED_CERTIFICATE_ARN =
"arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00";
const EXECUTION_BOUNDARY_ARN =
"arn:aws:iam::396287094661:policy/external-dev-execution-boundary";
const HCP_PROVIDER_ARN =
"arn:aws:iam::396287094661:oidc-provider/app.terraform.io";
const GITHUB_PROVIDER_ARN =
"arn:aws:iam::396287094661:oidc-provider/token.actions.githubusercontent.com";
const FORBIDDEN_POC_IDENTIFIERS = new Set([
"E2CWLM1AFB964P",
"E30VSIK87N8H64",
"us-east-1shocfrontenddevSpaRewrite58674DB8",
"Z07671212N75U4YLPWZR8",
"E2JDVEZ6EGD49J",
"E1PF5R6QQNBZAI",
"us-east-1shocfrontendstagingSpaRewriteE9C0CBDA",
"Z02602739VQWBWCAGXP4",
"Z02451891BSZD93CMMGDU",
SHARED_CERTIFICATE_ARN,
]);
interface FrontendEnvironment {
readonly key: "tf-poc" | "dev" | "staging";
readonly workspace: string;
readonly bucketName: string;
readonly domainName: string;
readonly hostedZoneId: string;
readonly certificateArn: string;
readonly deployRoleName: string;
readonly distributionId: string;
readonly originAccessControlId: string;
readonly functionName: string;
readonly roleCondition: cdk.CfnCondition;
readonly invalidationCondition?: cdk.CfnCondition;
}
interface ShocFrontendResourcesProps {
readonly template: cfninc.CfnInclude;
readonly enablePocRoles: boolean;
readonly enableLiveRoles: boolean;
readonly pocDistributionId: string;
readonly pocOriginAccessControlId: string;
readonly pocFunctionName: string;
readonly pocHostedZoneId: string;
readonly pocCertificateArn: string;
}
const retain = (resource: cdk.CfnResource): void => {
resource.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN;
resource.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN;
};
const roleArn = (roleName: string): string =>
`arn:aws:iam::${ACCOUNT_ID}:role/${roleName}`;
const bucketArn = (bucketName: string): string => `arn:aws:s3:::${bucketName}`;
const distributionArn = (distributionId: string): string =>
`arn:aws:cloudfront::${ACCOUNT_ID}:distribution/${distributionId}`;
const functionArn = (functionName: string): string =>
`arn:aws:cloudfront::${ACCOUNT_ID}:function/${functionName}`;
const originAccessControlArn = (originAccessControlId: string): string =>
`arn:aws:cloudfront::${ACCOUNT_ID}:origin-access-control/${originAccessControlId}`;
const hostedZoneArn = (hostedZoneId: string): string =>
`arn:aws:route53:::hostedzone/${hostedZoneId}`;
const frontendReadPolicy = (
environment: FrontendEnvironment,
): Record<string, unknown> => {
const siteBucketArn = bucketArn(environment.bucketName);
return {
Version: "2012-10-17",
Statement: [
{
Sid: "CallerIdentity",
Effect: "Allow",
Action: "sts:GetCallerIdentity",
Resource: "*",
},
{
Sid: "ReadExactSiteBucket",
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: siteBucketArn,
},
{
Sid: "ReadReleasePointerObject",
Effect: "Allow",
Action: ["s3:GetObject", "s3:GetObjectTagging", "s3:GetObjectVersion"],
Resource: `${siteBucketArn}/.release/current`,
},
{
Sid: "ReadExactCloudFrontResources",
Effect: "Allow",
Action: [
"cloudfront:DescribeFunction",
"cloudfront:GetDistribution",
"cloudfront:GetDistributionConfig",
"cloudfront:GetFunction",
"cloudfront:GetOriginAccessControl",
"cloudfront:ListTagsForResource",
],
Resource: [
distributionArn(environment.distributionId),
functionArn(environment.functionName),
originAccessControlArn(environment.originAccessControlId),
],
},
{
Sid: "ListCloudFrontInventory",
Effect: "Allow",
Action: [
"cloudfront:ListDistributions",
"cloudfront:ListFunctions",
"cloudfront:ListOriginAccessControls",
],
Resource: "*",
},
{
Sid: "ReadManagedCachePolicy",
Effect: "Allow",
Action: "cloudfront:GetCachePolicy",
Resource: `arn:aws:cloudfront::${ACCOUNT_ID}:cache-policy/${CACHE_POLICY_ID}`,
},
{
Sid: "ListCachePolicies",
Effect: "Allow",
Action: "cloudfront:ListCachePolicies",
Resource: "*",
},
{
Sid: "ReadExactDeployRole",
Effect: "Allow",
Action: [
"iam:GetRole",
"iam:GetRolePolicy",
"iam:ListAttachedRolePolicies",
"iam:ListRolePolicies",
"iam:ListRoleTags",
],
Resource: roleArn(environment.deployRoleName),
},
{
Sid: "ReadExactDeployBoundary",
Effect: "Allow",
Action: ["iam:GetPolicy", "iam:GetPolicyVersion"],
Resource: `arn:aws:iam::${ACCOUNT_ID}:policy/shoc-frontend-new-${environment.key}-deploy-boundary`,
},
{
Sid: "ReadGithubOidcProvider",
Effect: "Allow",
Action: "iam:GetOpenIDConnectProvider",
Resource: GITHUB_PROVIDER_ARN,
},
{
Sid: "ListOidcProviders",
Effect: "Allow",
Action: "iam:ListOpenIDConnectProviders",
Resource: "*",
},
{
Sid: "ReadExactCertificate",
Effect: "Allow",
Action: [
"acm:DescribeCertificate",
"acm:GetCertificate",
"acm:ListTagsForCertificate",
],
Resource: environment.certificateArn,
},
{
Sid: "ListCertificates",
Effect: "Allow",
Action: "acm:ListCertificates",
Resource: "*",
},
{
Sid: "ReadExactDns",
Effect: "Allow",
Action: [
"route53:GetHostedZone",
"route53:ListResourceRecordSets",
"route53:ListTagsForResource",
],
Resource: hostedZoneArn(environment.hostedZoneId),
},
{
Sid: "FindHostedZone",
Effect: "Allow",
Action: ["route53:ListHostedZones", "route53:ListHostedZonesByName"],
Resource: "*",
},
{
Sid: "ReadDnsChanges",
Effect: "Allow",
Action: "route53:GetChange",
Resource: "arn:aws:route53:::change/*",
},
],
};
};
const frontendApplyPolicy = (
environment: FrontendEnvironment,
): Record<string, unknown> => ({
Version: "2012-10-17",
Statement: [
{
Sid: "DenyRoleLifecycleAndTrustMutation",
Effect: "Deny",
Action: [
"iam:AttachRolePolicy",
"iam:CreateRole",
"iam:CreateServiceLinkedRole",
"iam:DeleteRole",
"iam:DeleteRolePermissionsBoundary",
"iam:DeleteRolePolicy",
"iam:DetachRolePolicy",
"iam:PassRole",
"iam:PutRolePermissionsBoundary",
"iam:UpdateAssumeRolePolicy",
"iam:UpdateRole",
"iam:UpdateRoleDescription",
],
Resource: "*",
},
{
Sid: "DenyManagedPolicyMutation",
Effect: "Deny",
Action: [
"iam:CreatePolicy",
"iam:CreatePolicyVersion",
"iam:DeletePolicy",
"iam:DeletePolicyVersion",
"iam:SetDefaultPolicyVersion",
],
Resource: "*",
},
{
Sid: "DenyInfrastructureReplacement",
Effect: "Deny",
Action: [
"cloudfront:CreateDistribution",
"cloudfront:CreateFunction",
"cloudfront:CreateOriginAccessControl",
"cloudfront:DeleteDistribution",
"cloudfront:DeleteFunction",
"cloudfront:DeleteOriginAccessControl",
"cloudfront:UpdateOriginAccessControl",
"s3:CreateBucket",
"s3:DeleteBucket",
"s3:DeleteBucketEncryption",
"s3:DeleteBucketOwnershipControls",
"s3:DeleteBucketPolicy",
"s3:DeleteBucketPublicAccessBlock",
"s3:PutBucketOwnershipControls",
"s3:PutBucketPublicAccessBlock",
"s3:PutBucketVersioning",
"s3:PutEncryptionConfiguration",
],
Resource: "*",
},
{
Sid: "DenySecretAccess",
Effect: "Deny",
Action: [
"kms:Decrypt",
"secretsmanager:*",
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath",
],
Resource: "*",
},
{
Sid: "LockHcpTerraformWorkspaceTag",
Effect: "Deny",
Action: ["iam:TagRole", "iam:UntagRole"],
Resource: "*",
Condition: {
"ForAnyValue:StringEquals": {
"aws:TagKeys": "HcpTerraformWorkspace",
},
},
},
{
Sid: "TagExactSiteBucket",
Effect: "Allow",
Action: "s3:PutBucketTagging",
Resource: bucketArn(environment.bucketName),
},
{
Sid: "ReplaceExactBucketPolicy",
Effect: "Allow",
Action: "s3:PutBucketPolicy",
Resource: bucketArn(environment.bucketName),
},
{
Sid: "TagExactCloudFrontResources",
Effect: "Allow",
Action: ["cloudfront:TagResource", "cloudfront:UntagResource"],
Resource: [
distributionArn(environment.distributionId),
functionArn(environment.functionName),
],
},
// TagResource is already allowed. Update* and PublishFunction were denied
// on * so Phase 2 in-place CloudFront updates could not apply. Scope them
// to exact ARNs. The AWS provider publishes after UpdateFunction.
{
Sid: "UpdateExactDistribution",
Effect: "Allow",
Action: "cloudfront:UpdateDistribution",
Resource: distributionArn(environment.distributionId),
},
{
Sid: "UpdateExactFunction",
Effect: "Allow",
Action: ["cloudfront:UpdateFunction", "cloudfront:PublishFunction"],
Resource: functionArn(environment.functionName),
},
{
Sid: "InvalidateExactDistribution",
Effect: "Allow",
Action: ["cloudfront:CreateInvalidation", "cloudfront:GetInvalidation"],
Resource: distributionArn(environment.distributionId),
},
{
Sid: "WriteReleasePointerObject",
Effect: "Allow",
Action: [
"s3:GetObject",
"s3:GetObjectTagging",
"s3:GetObjectVersion",
"s3:PutObject",
"s3:PutObjectTagging",
],
Resource: `${bucketArn(environment.bucketName)}/.release/current`,
},
{
Sid: "ReplaceExactDeployInlinePolicy",
Effect: "Allow",
Action: "iam:PutRolePolicy",
Resource: roleArn(environment.deployRoleName),
Condition: {
StringEquals: {
"iam:PermissionsBoundary": `arn:aws:iam::${ACCOUNT_ID}:policy/shoc-frontend-new-${environment.key}-deploy-boundary`,
},
},
},
{
Sid: "TagExactDeployRole",
Effect: "Allow",
Action: ["iam:TagRole", "iam:UntagRole"],
Resource: roleArn(environment.deployRoleName),
},
{
Sid: "ChangeExactSiteAliases",
Effect: "Allow",
Action: "route53:ChangeResourceRecordSets",
Resource: hostedZoneArn(environment.hostedZoneId),
Condition: {
"ForAllValues:StringEquals": {
"route53:ChangeResourceRecordSetsActions": [
"CREATE",
"DELETE",
"UPSERT",
],
"route53:ChangeResourceRecordSetsNormalizedRecordNames": [
environment.domainName,
],
"route53:ChangeResourceRecordSetsRecordTypes": ["A", "AAAA"],
},
},
},
],
});
const assumeRolePolicy = (
workspace: string,
runPhase: "plan" | "apply",
): Record<string, unknown> => ({
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Principal: { Federated: HCP_PROVIDER_ARN },
Action: "sts:AssumeRoleWithWebIdentity",
Condition: {
StringEquals: {
"app.terraform.io:aud": "aws.workload.identity",
"app.terraform.io:sub":
`organization:seahaven:project:seahaven-external-dev:` +
`workspace:${workspace}:run_phase:${runPhase}`,
},
},
},
],
});
const roleTags = (
environment: FrontendEnvironment,
includeManagerTag: boolean,
): CfnTag[] => {
const tags = [
{ key: "Environment", value: environment.key },
{ key: "Workspace", value: environment.workspace },
];
if (includeManagerTag) {
tags.push({
key: "HcpTerraformWorkspace",
value: environment.workspace,
});
}
return tags;
};
export class ShocFrontendResources extends Construct {
constructor(scope: Construct, id: string, props: ShocFrontendResourcesProps) {
super(scope, id);
this.validatePocIdentifiers(props);
const pocInvalidationCondition = new cdk.CfnCondition(
this,
"HasShocFrontendPocDistribution",
{
expression: cdk.Fn.conditionNot(
cdk.Fn.conditionEquals(props.pocDistributionId, ""),
),
},
);
pocInvalidationCondition.overrideLogicalId(
"HasShocFrontendPocDistribution",
);
const pocRoleCondition = new cdk.CfnCondition(
this,
"ShouldManageShocFrontendPocRoles",
{
expression: cdk.Fn.conditionAnd(
cdk.Fn.conditionEquals(cdk.Aws.ACCOUNT_ID, ACCOUNT_ID),
cdk.Fn.conditionEquals(
props.enablePocRoles ? "true" : "false",
"true",
),
cdk.Fn.conditionNot(
cdk.Fn.conditionEquals(props.pocDistributionId, ""),
),
cdk.Fn.conditionNot(
cdk.Fn.conditionEquals(props.pocOriginAccessControlId, ""),
),
cdk.Fn.conditionNot(
cdk.Fn.conditionEquals(props.pocFunctionName, ""),
),
cdk.Fn.conditionNot(
cdk.Fn.conditionEquals(props.pocHostedZoneId, ""),
),
cdk.Fn.conditionNot(
cdk.Fn.conditionEquals(props.pocCertificateArn, ""),
),
),
},
);
pocRoleCondition.overrideLogicalId("ShouldManageShocFrontendPocRoles");
const liveRoleCondition = new cdk.CfnCondition(
this,
"ShouldManageShocFrontendLiveRoles",
{
expression: cdk.Fn.conditionAnd(
cdk.Fn.conditionEquals(cdk.Aws.ACCOUNT_ID, ACCOUNT_ID),
cdk.Fn.conditionEquals(
props.enableLiveRoles ? "true" : "false",
"true",
),
),
},
);
liveRoleCondition.overrideLogicalId("ShouldManageShocFrontendLiveRoles");
const externalDevCondition = props.template.getCondition(
"IsExternalDevAccount",
);
const environments: FrontendEnvironment[] = [
{
key: "tf-poc",
workspace: "shoc-frontend-new-tf-poc",
bucketName: "seahaven-shoc-frontend-tf-poc",
domainName: "frontend-tf-poc.seahaven.com",
hostedZoneId: props.pocHostedZoneId,
certificateArn: props.pocCertificateArn,
deployRoleName: "githubdeploy-shoc-frontend-new-tf-poc",
distributionId: props.pocDistributionId,
originAccessControlId: props.pocOriginAccessControlId,
functionName: props.pocFunctionName,
roleCondition: pocRoleCondition,
invalidationCondition: pocInvalidationCondition,
},
{
key: "dev",
workspace: "shoc-frontend-new-dev",
bucketName: "seahaven-shoc-frontend-dev",
domainName: "dev.seahaven.com",
hostedZoneId: "Z07671212N75U4YLPWZR8",
certificateArn: SHARED_CERTIFICATE_ARN,
deployRoleName: "githubdeploy-shoc-frontend-new-dev",
distributionId: "E2CWLM1AFB964P",
originAccessControlId: "E30VSIK87N8H64",
functionName: "us-east-1shocfrontenddevSpaRewrite58674DB8",
roleCondition: liveRoleCondition,
},
{
key: "staging",
workspace: "shoc-frontend-new-staging",
bucketName: "seahaven-shoc-frontend-staging",
domainName: "staging.seahaven.com",
hostedZoneId: "Z02602739VQWBWCAGXP4",
certificateArn: SHARED_CERTIFICATE_ARN,
deployRoleName: "githubdeploy-shoc-frontend-new-staging",
distributionId: "E2JDVEZ6EGD49J",
originAccessControlId: "E1PF5R6QQNBZAI",
functionName: "us-east-1shocfrontendstagingSpaRewriteE9C0CBDA",
roleCondition: liveRoleCondition,
},
];
for (const environment of environments) {
this.addEnvironment(environment, externalDevCondition);
}
}
private validatePocIdentifiers(props: ShocFrontendResourcesProps): void {
const distributionPattern = /^E[A-Z0-9]+$/;
const functionPattern = /^[A-Za-z0-9_-]+$/;
const hostedZonePattern = /^Z[A-Z0-9]+$/;
const certificatePattern =
/^arn:aws:acm:us-east-1:396287094661:certificate\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const identifiers = [
props.pocDistributionId,
props.pocOriginAccessControlId,
props.pocFunctionName,
props.pocHostedZoneId,
props.pocCertificateArn,
];
const hasPartialIdentifiers =
identifiers.some((value) => value !== "") &&
identifiers.some((value) => value === "");
if (hasPartialIdentifiers) {
throw new Error(
"All five shocFrontendPoc identifiers must be set together",
);
}
if (
props.pocDistributionId !== "" &&
(!distributionPattern.test(props.pocDistributionId) ||
!distributionPattern.test(props.pocOriginAccessControlId) ||
!functionPattern.test(props.pocFunctionName) ||
!hostedZonePattern.test(props.pocHostedZoneId) ||
!certificatePattern.test(props.pocCertificateArn))
) {
throw new Error("Invalid shocFrontendPoc identifier");
}
if (
identifiers.some((identifier) =>
FORBIDDEN_POC_IDENTIFIERS.has(identifier),
)
) {
throw new Error(
"shocFrontendPoc identifiers must not reuse live frontend or backend tf-poc resources",
);
}
if (props.enablePocRoles && props.pocDistributionId === "") {
throw new Error(
"enableShocFrontendPocRoles requires all five identifiers",
);
}
}
private addEnvironment(
environment: FrontendEnvironment,
externalDevCondition: cdk.CfnCondition,
): void {
const logicalSuffix =
environment.key === "tf-poc"
? "Poc"
: environment.key.charAt(0).toUpperCase() + environment.key.slice(1);
const siteBucketArn = bucketArn(environment.bucketName);
const exactDistributionArn = distributionArn(environment.distributionId);
const boundaryStatements: unknown[] = [
{
Sid: "ReadDeploymentBucket",
Effect: "Allow",
Action: [
"s3:GetBucketLocation",
"s3:GetBucketVersioning",
"s3:ListBucket",
"s3:ListBucketVersions",
],
Resource: siteBucketArn,
},
{
Sid: "PublishRollbackAndPruneSiteObjects",
Effect: "Allow",
Action: [
"s3:DeleteObject",
"s3:DeleteObjectVersion",
"s3:GetObject",
"s3:GetObjectVersion",
"s3:PutObject",
],
Resource: `${siteBucketArn}/*`,
},
];
const wrapDistributionStatement = (
statement: Record<string, unknown>,
): unknown =>
environment.invalidationCondition === undefined
? statement
: cdk.Fn.conditionIf(
environment.invalidationCondition.logicalId,
statement,
cdk.Aws.NO_VALUE,
);
// GitHub verify and live-state summary call get-distribution. The identity
// policy already grants these; the boundary was the deny.
const readDistributionStatement = {
Sid: "ReadExactDistribution",
Effect: "Allow",
Action: [
"cloudfront:GetDistribution",
"cloudfront:GetDistributionConfig",
],
Resource: exactDistributionArn,
};
const invalidationStatement = {
Sid: "InvalidateExactDistribution",
Effect: "Allow",
Action: ["cloudfront:CreateInvalidation", "cloudfront:GetInvalidation"],
Resource: exactDistributionArn,
};
boundaryStatements.push(
wrapDistributionStatement(readDistributionStatement),
wrapDistributionStatement(invalidationStatement),
);
const deployBoundary = new iam.CfnManagedPolicy(
this,
`ShocFrontend${logicalSuffix}DeployBoundary`,
{
managedPolicyName: `shoc-frontend-new-${environment.key}-deploy-boundary`,
description:
`Maximum content deployment permissions for ` +
`${environment.deployRoleName}.`,
policyDocument: {
Version: "2012-10-17",
Statement: boundaryStatements,
},
},
);
deployBoundary.cfnOptions.condition = externalDevCondition;
deployBoundary.overrideLogicalId(
`ShocFrontend${logicalSuffix}DeployBoundary`,
);
retain(deployBoundary);
const planRole = new iam.CfnRole(
this,
`HcptfShocFrontend${logicalSuffix}PlanRole`,
{
roleName: `${environment.workspace}-plan`.replace(
"shoc-frontend-new",
"hcptf-shoc-frontend-new",
),
description: `Read-only HCP Terraform plan role for ${environment.workspace}.`,
permissionsBoundary: EXECUTION_BOUNDARY_ARN,
maxSessionDuration: 3600,
assumeRolePolicyDocument: assumeRolePolicy(
environment.workspace,
"plan",
),
policies: [
{
policyName: `${environment.workspace}-import-read`,
policyDocument: frontendReadPolicy(environment),
},
],
tags: roleTags(environment, false),
},
);
planRole.cfnOptions.condition = environment.roleCondition;
planRole.overrideLogicalId(`HcptfShocFrontend${logicalSuffix}PlanRole`);
retain(planRole);
const applyRole = new iam.CfnRole(
this,
`HcptfShocFrontend${logicalSuffix}ApplyRole`,
{
roleName: environment.workspace.replace(
"shoc-frontend-new",
"hcptf-shoc-frontend-new",
),
description: `Constrained HCP Terraform apply role for ${environment.workspace}.`,
permissionsBoundary: EXECUTION_BOUNDARY_ARN,
maxSessionDuration: 3600,
assumeRolePolicyDocument: assumeRolePolicy(
environment.workspace,
"apply",
),
policies: [
{
policyName: `${environment.workspace}-import-read`,
policyDocument: frontendReadPolicy(environment),
},
{
policyName: `${environment.workspace}-import-apply`,
policyDocument: frontendApplyPolicy(environment),
},
],
tags: roleTags(environment, true),
},
);
applyRole.cfnOptions.condition = environment.roleCondition;
applyRole.overrideLogicalId(`HcptfShocFrontend${logicalSuffix}ApplyRole`);
applyRole.addResourceDependency(deployBoundary);
retain(applyRole);
}
}