mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-10-05 22:11:59 +00:00
HCP apply already writes /shoc-frontend-new/<env>/deploy/*, but the githubdeploy ceiling omitted GetParameter so Deploy Web cannot resolve bucket and distribution after origin moves to the bucket root.
834 lines
26 KiB
TypeScript
834 lines
26 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 deployParameterArn = (environment: FrontendEnvironment): string =>
|
|
`arn:aws:ssm:us-east-1:${ACCOUNT_ID}:parameter/shoc-frontend-new/${environment.key}/deploy/*`;
|
|
|
|
const isLiveFrontendEnvironment = (
|
|
environment: FrontendEnvironment,
|
|
): boolean => environment.key === "dev" || environment.key === "staging";
|
|
|
|
const environmentSid = (environment: FrontendEnvironment): string =>
|
|
environment.key.charAt(0).toUpperCase() + environment.key.slice(1);
|
|
|
|
const frontendReadPolicy = (
|
|
environment: FrontendEnvironment,
|
|
): Record<string, unknown> => {
|
|
const siteBucketArn = bucketArn(environment.bucketName);
|
|
const statements: Record<string, unknown>[] = [
|
|
{
|
|
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/*",
|
|
},
|
|
];
|
|
if (isLiveFrontendEnvironment(environment)) {
|
|
statements.push(
|
|
{
|
|
Sid: `Read${environmentSid(environment)}DeploySsm`,
|
|
Effect: "Allow",
|
|
Action: [
|
|
"ssm:GetParameter",
|
|
"ssm:GetParameters",
|
|
"ssm:ListTagsForResource",
|
|
],
|
|
Resource: deployParameterArn(environment),
|
|
},
|
|
{
|
|
Sid: `Describe${environmentSid(environment)}DeploySsm`,
|
|
Effect: "Allow",
|
|
Action: "ssm:DescribeParameters",
|
|
Resource: "*",
|
|
},
|
|
);
|
|
}
|
|
return {
|
|
Version: "2012-10-17",
|
|
Statement: statements,
|
|
};
|
|
};
|
|
|
|
const frontendApplyPolicy = (
|
|
environment: FrontendEnvironment,
|
|
): Record<string, unknown> => {
|
|
const live = isLiveFrontendEnvironment(environment);
|
|
const statements: Record<string, unknown>[] = [
|
|
{
|
|
Sid: "DenyRoleLifecycleAndTrustMutation",
|
|
Effect: "Deny",
|
|
Action: [
|
|
"iam:AttachRolePolicy",
|
|
"iam:CreateRole",
|
|
"iam:CreateServiceLinkedRole",
|
|
"iam:DeleteRole",
|
|
"iam:DeleteRolePermissionsBoundary",
|
|
"iam:DeleteRolePolicy",
|
|
"iam:DetachRolePolicy",
|
|
"iam:PassRole",
|
|
"iam:PutRolePermissionsBoundary",
|
|
"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: live
|
|
? ["kms:Decrypt", "secretsmanager:*"]
|
|
: [
|
|
"kms:Decrypt",
|
|
"secretsmanager:*",
|
|
"ssm:GetParameter",
|
|
"ssm:GetParameters",
|
|
"ssm:GetParametersByPath",
|
|
],
|
|
Resource: "*",
|
|
},
|
|
];
|
|
if (live) {
|
|
statements.push({
|
|
Sid: "DenyUnrelatedParameterReads",
|
|
Effect: "Deny",
|
|
Action: [
|
|
"ssm:GetParameter",
|
|
"ssm:GetParameters",
|
|
"ssm:GetParametersByPath",
|
|
],
|
|
NotResource: deployParameterArn(environment),
|
|
});
|
|
}
|
|
statements.push(
|
|
{
|
|
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),
|
|
},
|
|
);
|
|
if (live) {
|
|
statements.push(
|
|
{
|
|
Sid: `Update${environmentSid(environment)}GithubDeployTrust`,
|
|
Effect: "Allow",
|
|
Action: "iam:UpdateAssumeRolePolicy",
|
|
Resource: roleArn(environment.deployRoleName),
|
|
},
|
|
{
|
|
Sid: `Manage${environmentSid(environment)}DeploySsm`,
|
|
Effect: "Allow",
|
|
Action: [
|
|
"ssm:PutParameter",
|
|
"ssm:AddTagsToResource",
|
|
"ssm:RemoveTagsFromResource",
|
|
],
|
|
Resource: deployParameterArn(environment),
|
|
},
|
|
);
|
|
}
|
|
statements.push({
|
|
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"],
|
|
},
|
|
},
|
|
});
|
|
return {
|
|
Version: "2012-10-17",
|
|
Statement: statements,
|
|
};
|
|
};
|
|
|
|
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),
|
|
);
|
|
if (isLiveFrontendEnvironment(environment)) {
|
|
boundaryStatements.push({
|
|
Sid: "ReadDeployParams",
|
|
Effect: "Allow",
|
|
Action: ["ssm:GetParameter", "ssm:GetParameters"],
|
|
Resource: deployParameterArn(environment),
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|