From f8ef82eb0668fcb6061bc8c052982a2b13952ee3 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Tue, 8 Apr 2025 14:02:12 +0000 Subject: [PATCH 1/5] Add codegen for Asset Account Stack CFN and TF --- .../examples/self-managed/.terraform.lock.hcl | 34 +- codegen/package-lock.json | 16 +- codegen/package.json | 5 +- codegen/src/asset-account/index.ts | 436 ++++++++++++++++++ codegen/src/{ => common}/iam.ts | 8 +- codegen/src/common/inputs.ts | 76 +++ codegen/src/common/inventory.ts | 174 +++++++ codegen/src/common/string.ts | 10 + codegen/src/main.ts | 2 +- .../policies/ElastioAssetAccountDeployer.ts | 2 +- .../src/policies/ElastioAwsBackupEc2Scan.ts | 2 +- 11 files changed, 736 insertions(+), 29 deletions(-) create mode 100644 codegen/src/asset-account/index.ts rename codegen/src/{ => common}/iam.ts (92%) create mode 100644 codegen/src/common/inputs.ts create mode 100644 codegen/src/common/inventory.ts create mode 100644 codegen/src/common/string.ts diff --git a/asset-account/terraform/stack-set/examples/self-managed/.terraform.lock.hcl b/asset-account/terraform/stack-set/examples/self-managed/.terraform.lock.hcl index 7de3aa8..2c5d0fb 100644 --- a/asset-account/terraform/stack-set/examples/self-managed/.terraform.lock.hcl +++ b/asset-account/terraform/stack-set/examples/self-managed/.terraform.lock.hcl @@ -2,24 +2,24 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "5.92.0" - constraints = ">= 5.0.0" + version = "5.94.1" + constraints = "~> 5.0" hashes = [ - "h1:ZnpTxMfg5PThZc5WZCsZELinsR0gPhdTpNmXjVcf7aE=", - "zh:1d3a0b40831360e8e988aee74a9ff3d69d95cb541c2eae5cb843c64303a091ba", - "zh:3d29cbced6c708be2041a708d25c7c0fc22d09e4d0b174360ed113bfae786137", - "zh:4341a203cf5820a0ca18bb514ae10a6c113bc6a728fb432acbf817d232e8eff4", - "zh:4a49e2d91e4d92b6b93ccbcbdcfa2d67935ce62e33b939656766bb81b3fd9a2c", - "zh:54c7189358b37fd895dedbabf84e509c1980a8c404a1ee5b29b06e40497b8655", - "zh:5d8bb1ff089c37cb65c83b4647f1981fded993e87d8132915d92d79f29e2fcd8", - "zh:618f2eb87cd65b245aefba03991ad714a51ff3b841016ef68e2da2b85d0b2325", - "zh:7bce07bc542d0588ca42bac5098dd4f8af715417cd30166b4fb97cedd44ab109", - "zh:81419eab2d8810beb114b1ff5cbb592d21edc21b809dc12bb066e4b88fdd184a", + "h1:pm3uoaQYHaavwE83zsEzAFn/LKD1EWGiYRfzVxNCaIA=", + "zh:14fb41e50219660d5f02b977e6f786d8ce78766cce8c2f6b8131411b087ae945", + "zh:3bc5d12acd5e1a5f1cf78a7f05d0d63f988b57485e7d20c47e80a0b723a99d26", + "zh:4835e49377f80a37c6191a092f636e227a9f086e3cc3f0c9e1b554da8793cfe8", + "zh:605971275adae25096dca30a94e29931039133c667c1d9b38778a09594312964", + "zh:8ae46b4a9a67815facf59da0c56d74ef71bcc77ae79e8bfbac504fa43f267f8e", + "zh:913f3f371c3e6d1f040d6284406204b049977c13cb75aae71edb0ef8361da7dd", + "zh:91f85ae8c73932547ad7139ce0b047a6a7c7be2fd944e51db13231cc80ce6d8e", + "zh:96352ae4323ce137903b9fe879941f894a3ce9ef30df1018a0f29f285a448793", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:9dea39d4748eeeebe2e76ca59bca4ccd161c2687050878c47289a98407a23372", - "zh:d692fc33b67ac89e916c8f9233d39eacab8c438fe10172990ee9d94fba5ca372", - "zh:d9075c7da48947c029ba47d5985e1e8e3bf92367bfee8ca1ff0e747765e779a1", - "zh:e81c62db317f3b640b2e04eba0ada8aa606bcbae0152c09f6242e86b86ef5889", - "zh:f68562e073722c378d2f3529eb80ad463f12c44aa5523d558ae3b69f4de5ca1f", + "zh:9b51922c9201b1dc3d05b39f9972715db5f67297deee088793d02dea1832564b", + "zh:a689e82112aa71e15647b06502d5b585980cd9002c3cc8458f092e8c8a667696", + "zh:c3723fa3e6aff3c1cc0088bdcb1edee168fe60020f2f77161d135bf473f45ab2", + "zh:d6a2052b864dd394b01ad1bae32d0a7d257940ee47908d02df7fa7873981d619", + "zh:dda4c9c0406cc54ad8ee4f19173a32de7c6e73abb5a948ea0f342d567df26a1d", + "zh:f42e0fe592b97cbdf70612f0fbe2bab851835e2d1aaf8cbb87c3ab0f2c96bb27", ] } diff --git a/codegen/package-lock.json b/codegen/package-lock.json index 53f9a46..174ce43 100644 --- a/codegen/package-lock.json +++ b/codegen/package-lock.json @@ -6,10 +6,11 @@ "packages": { "": { "dependencies": { - "aws-iam-policy-types": "^1.0.2" + "aws-iam-policy-types": "^1.0.2", + "lodash": "^4.17.21" }, "devDependencies": { - "@types/node": "^22.13.13", + "@types/node": "^22.13.14", "tsx": "^4.19.3", "typescript": "^5.8.2" } @@ -415,9 +416,9 @@ } }, "node_modules/@types/node": { - "version": "22.13.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz", - "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==", + "version": "22.13.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", + "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", "dev": true, "dependencies": { "undici-types": "~6.20.0" @@ -500,6 +501,11 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", diff --git a/codegen/package.json b/codegen/package.json index dd6500f..0477c9d 100644 --- a/codegen/package.json +++ b/codegen/package.json @@ -4,11 +4,12 @@ "start": "tsc --noEmit && tsx ./src/main.ts" }, "devDependencies": { - "@types/node": "^22.13.13", + "@types/node": "^22.13.14", "tsx": "^4.19.3", "typescript": "^5.8.2" }, "dependencies": { - "aws-iam-policy-types": "^1.0.2" + "aws-iam-policy-types": "^1.0.2", + "lodash": "^4.17.21" } } diff --git a/codegen/src/asset-account/index.ts b/codegen/src/asset-account/index.ts new file mode 100644 index 0000000..b0707bb --- /dev/null +++ b/codegen/src/asset-account/index.ts @@ -0,0 +1,436 @@ +// import { Construct } from "constructs"; +// import { CfnCondition, Fn, aws_iam as iam, aws_ssm as ssm } from "aws-cdk-lib"; +// import * as condition from "../common/iam/condition"; +// import { version } from "../common/version"; +// import { Aws, CfnOutput, Stack, StackProps } from "aws-cdk-lib"; +// import { DeploymentNotifier } from "../common/deployment-notifier"; +// import { InputParam } from "../common/input-param"; +// import { Globals, MaybeEmpty } from "../common/globals"; +// import { KmsEncryptionKey } from "../common/kms"; +// import { allowKms, createRole, IamRoleProps } from "../common/iam"; +import * as inventory from "../common/inventory"; +import { Input } from "../common/inputs"; +import { collapse } from "../common/string"; +// import _ from "lodash"; + +const iamPolicyArnRegex = "^arn:aws:iam::[^:]*:policy/.*"; + +export const inputs = { + cloudConnectorAccountId: { + group: "internal", + displayName: "Cloud connector AWS account ID", + description: collapse` + The ID of the Elastio Connector's account that should scan assets + in this account. It will be trusted to assume the role in this + account to read the assets and create snapshots. + `, + default: null, + type: "string", + }, + cloudConnectorRoleExternalId: { + group: "internal", + displayName: "Connector IAM role external ID", + description: collapse` + The secret token generated specifically for this account that + authenticates the source Elastio Connector to assume the + ElastioCloudConnector role in this account + `, + default: "", + type: "string", + }, + iamResourceNamesPrefix: { + group: "configurable", + displayName: "IAM resource names prefix", + description: + "Add a custom prefix to names of all IAM resources deployed by this stack", + type: "string", + default: "", + }, + + iamResourceNamesSuffix: { + group: "configurable", + displayName: "IAM resource names suffix", + description: + "Add a custom suffix to names of all IAM resources deployed by this stack", + type: "string", + default: "", + }, + + globalManagedPolicies: { + group: "configurable", + displayName: "Global IAM managed policies ARNs", + description: "IAM managed policies ARNs to attach to all Elastio IAM roles", + default: null, + type: "set(string)", + allowedPattern: iamPolicyArnRegex, + }, + + globalPermissionBoundary: { + group: "configurable", + displayName: "Global IAM permission boundary policy ARN", + description: collapse` + The ARN of the IAM managed policy to use as a + permission boundary for all Elastio IAM roles + `, + type: "string", + default: "", + allowedPattern: iamPolicyArnRegex, + }, + + encryptWithCmk: { + group: "configurable", + displayName: "Encrypt data with customer-managed KMS keys", + type: "bool", + default: false, + description: collapse` + Provision additional customer-managed KMS keys to encrypt + Lambda environment variables, DynamoDB tables, S3. Note that + by default data is encrypted with AWS-managed keys. Enable this + option only if your compliance requirements mandate the usage of CMKs. + If this option is disabled Elastio creates only 1 CMK per region where + the Elastio Connector stack is deployed. If this option is enabled then + Elastio creates 1 KMS key per AWS account and 2 KMS keys per every AWS + region where Elastio is deployed in your AWS Account + `, + }, + + lambdaTracing: { + group: "configurable", + displayName: "Enable AWS X-Ray tracing for Lambda functions", + description: "This increases the cost of the stack. Enable only if needed", + type: "bool", + default: false, + }, +} satisfies Record; + +// this.globals = new Globals({ +// stackName: "AssetAccount", +// iamPrefix: iamResourceNamesPrefix.valueAsString, +// iamSuffix: iamResourceNamesSuffix.valueAsString, +// globalManagedPolicies: MaybeEmpty.fromListInput(globalManagedPolicies), +// globalPermissionBoundary: MaybeEmpty.fromStringInput( +// globalPermissionBoundary, +// ), +// kmsEncryptionKey: new KmsEncryptionKey( +// this, +// "kmsEncryptionKey", +// encryptWithCmk, +// "alias/elastio-asset-account-encryption", +// ).key, +// lambdaTracing: new CfnCondition(this, "lambdaTracingCondition", { +// expression: Fn.conditionEquals(lambdaTracing, "true"), +// }), +// }); + +// new DeploymentNotifier({ +// scope: this, +// id: "deploymentNotifier", +// globals: this.globals, +// payload: { +// stackKind: "asset", +// stackVersion: version.asset_account, +// accountId: Aws.ACCOUNT_ID, +// cloudConnectorAccountId, +// }, +// }); + +// new ssm.StringParameter(this, "assetAccountStackName", { +// parameterName: "/elastio/asset/account/stack-name", +// description: +// "The name of the Asset Account CloudFormation stack used " + +// "for discovery by the Cloud Connector", +// stringValue: Aws.STACK_NAME, +// }); + +// const inventoryEventTargetRole = this.createRole({ +// id: "inventoryEventTarget", +// name: "InventoryEventTarget", +// assumedBy: new iam.ServicePrincipal("events.amazonaws.com"), +// statements: [ +// { +// actions: ["events:PutEvents"], +// resources: [ +// `arn:aws:events:*:${cloudConnectorAccountId.valueAsString}:event-bus/elastio-*`, +// ], +// }, +// ], +// }); + +// const assetRegionStackDeployerRole = this.createRole({ +// id: "assetRegionStackDeployer", +// name: "AssetRegionStackDeployer", +// description: "Used by Cloudformation to deploy region-level stack", +// assumedBy: new iam.ServicePrincipal("cloudformation.amazonaws.com"), +// statements: [ +// { +// actions: [ +// "events:DescribeRule", +// "events:ListTargetsByRule", +// "events:ListTagsForResource", +// "events:PutRule", +// "events:PutTargets", +// "events:RemoveTargets", +// "events:DeleteRule", +// "events:EnableRule", +// "events:DisableRule", +// ], +// resources: [`arn:aws:events:*:${Aws.ACCOUNT_ID}:rule/elastio-*`], +// }, +// { +// actions: [ +// // Allows assigning of `inventoryEventTargetRole` role +// // to the event targets +// "iam:PassRole", +// ], +// resources: [inventoryEventTargetRole.roleArn], +// }, +// ], +// }); + +// const cloudConnectorStatements: Record = +// { +// WriteEc2: [ +// // Create and copy snapshots to the cloud connector account +// { +// actions: [ +// "ec2:CreateSnapshot", +// "ec2:CreateSnapshots", +// "ec2:CopySnapshot", +// ], +// resources: ["*"], +// }, +// { +// actions: [ +// "ec2:DeleteSnapshot", + +// // This is used in AWS Backup restore test scenario +// "ec2:StopInstances", +// "ec2:TerminateInstances", + +// // Allows additional tags for elastio resources. +// // It's used, for example, to add new tag on a +// // snapshot which indicates that it's clean or infected +// "ec2:CreateTags", +// "ec2:DeleteTags", +// ], +// resources: ["*"], +// conditions: condition.hasResourceTag("elastio:resource"), +// }, + +// { +// actions: ["ec2:ModifySnapshotAttribute"], +// resources: ["*"], + +// conditions: { +// // Needed to add createVolumePermission for the connector account. +// StringEquals: { +// "ec2:Add/userId": cloudConnectorAccountId.valueAsString, +// // Even though EC2 IAM reference says there are two more +// // things that could be used in the condition: +// // "ec2:Attribute" and "ec2:Attribute/${AttributeName}", +// // none of them are actually present when the request +// // is evaluated. This can be seen by adding a condition like +// // "ec2:Attribute": "createVolumePermission" and observing +// // an error with an encoded authorization message, that +// // shows the real evaluation context when decoded. +// }, +// }, +// }, + +// // This is used in AWS Backup restore test scenario to mutate +// // the created temporary instance/volumes +// { +// actions: [ +// "ec2:ModifyInstanceAttribute", +// "ec2:CreateTags", +// "ec2:DeleteTags", +// ], +// resources: ["*"], +// conditions: condition.hasResourceTag("awsbackup-restore-test"), +// }, + +// // Allow assigning tags when creating new resources +// { +// actions: ["ec2:CreateTags"], +// resources: [ +// "arn:aws:ec2:*:*:volume/*", +// "arn:aws:ec2:*::snapshot/*", +// ], +// conditions: { +// StringLike: { +// "ec2:CreateAction": "*", +// }, +// }, +// }, +// { +// actions: ["ssm:SendCommand"], +// resources: [ +// "arn:aws:ssm:*:*:document/AWSEC2-CreateVssSnapshot", +// "arn:aws:ec2:*:*:instance/*", +// ], +// }, +// { +// actions: [ +// "ssm:GetConnectionStatus", +// "ssm:GetCommandInvocation", +// "ssm:ListCommands", +// ], +// resources: ["*"], +// }, +// ], +// ReadEbs: [ +// { +// actions: ["ebs:ListSnapshotBlocks", "ebs:GetSnapshotBlock"], +// resources: ["*"], +// }, +// ], +// ReadSsm: [ +// // Required to be able to try to do app-consistent snapshots +// // of EC2 Windows (VSS snapshots). +// { +// actions: ["ssm:GetParameters", "ssm:GetParameter"], +// resources: [ +// `arn:aws:ssm:*:${Aws.ACCOUNT_ID}:parameter/elastio/*`, +// `arn:aws:ssm:*::parameter/aws/*`, +// ], +// }, +// ], +// ReadIam: [ +// { +// actions: ["iam:GetInstanceProfile", "iam:SimulatePrincipalPolicy"], +// resources: ["*"], +// }, +// ], +// WriteCloudformation: [ +// { +// actions: [ +// "cloudformation:CreateStack", +// "cloudformation:UpdateStack", +// ], +// resources: [ +// `arn:aws:cloudformation:*:${Aws.ACCOUNT_ID}:stack/elastio-*/*`, +// ], +// conditions: { +// StringEquals: { +// ["cloudformation:RoleArn"]: +// assetRegionStackDeployerRole.roleArn, +// }, +// }, +// }, +// { +// // Allows running cloudformation:CreateStack/UpdateStack +// // on behalf of `assetRegionStackDeployer` role. +// actions: ["iam:PassRole"], +// resources: [assetRegionStackDeployerRole.roleArn], +// }, +// { +// actions: ["cloudformation:TagResource"], +// resources: [ +// `arn:aws:cloudformation:*:${Aws.ACCOUNT_ID}:stack/elastio-*/*`, +// ], +// conditions: { +// StringLike: { +// "ec2:CreateAction": "*", +// }, +// }, +// }, +// { +// actions: [ +// // We need to delete the asset region stack +// "cloudformation:DeleteStack", +// "cloudformation:TagResource", +// "cloudformation:UntagResource", +// ], +// resources: [ +// `arn:aws:cloudformation:*:${Aws.ACCOUNT_ID}:stack/elastio-*/*`, +// ], +// conditions: condition.hasResourceTag("elastio:resource"), +// }, +// ], +// ReadS3: [ +// { +// actions: [ +// // These are actually used by the scan job at the time of this writing +// "s3:ListBucket", +// "s3:GetObjectVersion", +// "s3:GetObject", +// "s3:GetBucketTagging", + +// // These permissions were used at the time when we supported native Elastio +// // backups and restores. These are readonly, and they are left here for +// // the future when we might need them or for easier debugging. +// "s3:GetReplicationConfiguration", +// "s3:GetMetricsConfiguration", +// "s3:GetLifecycleConfiguration", +// "s3:GetInventoryConfiguration", +// "s3:GetIntelligentTieringConfiguration", +// "s3:GetEncryptionConfiguration", +// "s3:GetBucketWebsite", +// "s3:GetBucketVersioning", +// "s3:GetBucketRequestPayment", +// "s3:GetBucketPublicAccessBlock", +// "s3:GetBucketPolicy", +// "s3:GetBucketOwnershipControls", +// "s3:GetBucketObjectLockConfiguration", +// "s3:GetBucketNotification", +// "s3:GetBucketLogging", +// "s3:GetBucketLocation", +// "s3:GetBucketAcl", +// "s3:GetAnalyticsConfiguration", +// "s3:GetAccelerateConfiguration", +// ], +// resources: ["*"], +// }, +// ], +// ReadSqs: [ +// { +// // Read the S3 changelog SQS queue +// actions: ["sqs:ReceiveMessage", "sqs:DeleteMessage"], +// resources: ["*"], +// conditions: condition.hasResourceTag("elastio:resource"), +// }, +// ], +// ReadDrs: [ +// { +// actions: [ +// "drs:DescribeRecoverySnapshots", +// "drs:DescribeSourceServers", +// "drs:ListTagsForResource", +// ], +// resources: ["*"], +// }, +// ], +// // Required by `ModifySnapshotAttributeRequest`, on encrypted snapshots +// ReadKms: [allowKms()], +// }; + +// const statements = _.mergeWith( +// inventoryIamPolicy, +// cloudConnectorStatements, +// (dest, _src, key) => { +// if (dest !== undefined) { +// throw new Error( +// `Duplicate policy statements in cloud connector policy at ${key}`, +// ); +// } +// }, +// ); + +// this.createRole({ +// id: "cloudConnector", +// name: "CloudConnector", +// description: +// "Allows Elastio Cloud Connector to access the assets in this account", +// assumedBy: new iam.ArnPrincipal( +// `arn:aws:iam::${cloudConnectorAccountId.valueAsString}:role/` + +// iamResourceNamesPrefix.valueAsString + +// "ElastioCloudConnectorBastion" + +// iamResourceNamesSuffix.valueAsString, +// ), +// externalIds: [cloudConnectorRoleExternalId.valueAsString], +// statements, +// }); + +// new CfnOutput(this, "cfnTemplateVersion", { +// value: version.asset_account, +// }); diff --git a/codegen/src/iam.ts b/codegen/src/common/iam.ts similarity index 92% rename from codegen/src/iam.ts rename to codegen/src/common/iam.ts index 911bb82..e06ebb8 100644 --- a/codegen/src/iam.ts +++ b/codegen/src/common/iam.ts @@ -9,7 +9,7 @@ export interface Policy { statements: PolicyStatement[]; } -interface PolicyStatement { +export interface PolicyStatement { /** * If not specified then `Allow` is assumed. */ @@ -37,12 +37,16 @@ type Principal = Service: string | string[]; }; -type Action = +export type Action = | `${string}:*` | `${iam.AwsBackupActions}` + | `${iam.AwsBatchActions}` | `${iam.AwsCloudformationActions}` + | `${iam.AwsDrsActions}` | `${iam.AwsEbsActions}` | `${iam.AwsEc2Actions}` + | `${iam.AwsElasticfilesystemActions}` + | `${iam.AwsFsxActions}` | `${iam.AwsIamActions}` | `${iam.AwsKmsActions}` | `${iam.AwsLambdaActions}` diff --git a/codegen/src/common/inputs.ts b/codegen/src/common/inputs.ts new file mode 100644 index 0000000..d25d5cc --- /dev/null +++ b/codegen/src/common/inputs.ts @@ -0,0 +1,76 @@ +// The order of keys and values matters! This is the order in which + +import { InputType } from "zlib"; + +// the groups will be displayed in the CloudFormation UI and in the docs. +export const group = { + configurable: "Configurable parameters", + internal: "Non-configurable (internal) parameters", + experimental: "Experimental parameters", +}; + +type BoolInput = { + type: "bool"; + default?: null | boolean; +}; + +type StringInput = { + type: "string"; + default?: null | string; + allowedValues?: string[]; + + /** + * The allowedPattern is a regular expression that the input value must + * match. The regular expression must be very conservative in its syntax. + * It must be compatible with both the CFN (Java) and Terraform (Go) + * regular expression engines. + */ + allowedPattern?: string; +}; + +type NumberInput = { + type: "number"; + default?: null | number; +}; + +type StringSetInput = { + type: "set(string)"; + default?: null | string[]; + + /** + * Allowed pattern for a single item in the string set. + * + * The allowedPattern is a regular expression that the input value must + * match. The regular expression must be very conservative in its syntax. + * It must be compatible with both the CFN (Java) and Terraform (Go) + * regular expression engines. + */ + allowedPattern?: string; +}; + +export type Input = (BoolInput | StringInput | NumberInput | StringSetInput) & { + /** + * The description is required at the type level to make a developer to + * pay attention to documenting the input parameter. The description will + * be visible to the end users, so keep it in mind that they will likely + * read it. + */ + description?: string; + + /** + * Whether the parameter is user configurable. If false, the parameter + * must not be modified by the end users. A special warning will be inserted + * into the description of the parameter to prevent the form changing the + * value of the parameter. The value of the parameter is expected to be set + * by the Elastio Tenant or the Cloud Connector itself (for asset region stack, + * for example). + * + * Default: false + */ + group?: keyof typeof group; + + /** + * Set how the parameter is displayed in the CloudFormation UI. + */ + displayName: string; +}; diff --git a/codegen/src/common/inventory.ts b/codegen/src/common/inventory.ts new file mode 100644 index 0000000..d8f9449 --- /dev/null +++ b/codegen/src/common/inventory.ts @@ -0,0 +1,174 @@ +import * as iam from "./iam"; + +/** + * Permissions for reading inventory shared across lambas, background jobs, + * ElastioTenant (Connector Account) and CloudConnector (Asset Account) roles. + */ +export const policy: Record = { + ReadBackupInventory: { + Action: [ + // Vaults + "backup:ListBackupVaults", + "backup:DescribeBackupVault", + + // Recovery points + "backup:ListRecoveryPointsByResource", + "backup:DescribeRecoveryPoint", + "backup:ListRecoveryPointsByBackupVault", + "backup:GetRecoveryPointRestoreMetadata", + + // Common for all resources + "backup:ListTags", + + // Misc. This won't be used at the time of this writing, but + // may come in handy in the future? + "backup:ListProtectedResources", + "backup:ListProtectedResourcesByBackupVault", + ], + Resource: "*", + }, + + ReadEfsInventory: { + Action: [ + "elasticfilesystem:DescribeFileSystems", + "elasticfilesystem:ListTagsForResource", + "elasticfilesystem:DescribeTags", + ], + Resource: "*", + }, + + ReadFsxInventory: { + Action: [ + "fsx:DescribeVolumes", + "fsx:DescribeBackups", + "fsx:DescribeFileSystems", + "fsx:DescribeStorageVirtualMachines", + "fsx:ListTagsForResource", + ], + Resource: "*", + }, + + ReadEbsInventory: { + Action: [ + // Volumes + "ec2:DescribeVolumeStatus", + "ec2:DescribeVolumes", + + // Snapshots + "ec2:DescribeSnapshots", + "ec2:DescribeSnapshotAttribute", + + // Common for all resources + "ec2:DescribeTags", + + // Used for cost estimation + "ebs:ListSnapshotBlocks", + "ebs:ListChangedBlocks", + ], + Resource: "*", + }, + + ReadEc2Inventory: { + Action: [ + "ec2:DescribeInstances", + "ec2:DescribeImages", + "ec2:DescribeHosts", + "ssm:DescribeInstanceInformation", + ], + Resource: "*", + }, + + ReadVpcInventory: { + Action: [ + // Used for network config troubleshooting + "ec2:DescribeAvailabilityZones", + "ec2:DescribeSecurityGroups", + "ec2:DescribeSubnets", + "ec2:DescribeVpcs", + "ec2:DescribeRouteTables", + "ec2:DescribeNatGateways", + "ec2:DescribeVpcEndpoints", + ], + Resource: "*", + }, + + ReadS3Inventory: { + Action: [ + "s3:ListAllMyBuckets", + "s3:GetBucketLocation", + "s3:GetBucketTagging", + "s3:GetBucketObjectLockConfiguration", + "s3:GetBucketAcl", + "s3:GetBucketVersioning", + "s3:GetBucketPolicy", + "s3:GetBucketLogging", + "s3:ListBucket", + ], + Resource: "*", + }, + + ReadAccountsInventory: { + Action: ["iam:ListAccountAliases", "ec2:DescribeRegions"], + Resource: "*", + }, + + ReadKmsInventory: { + Action: ["kms:DescribeKey"], + Resource: "*", + }, + + ReadCloudformationInventory: { + Action: [ + // Required for discovering Elastio CFN stack version by blue stack + // Also used by used by the `accs` service to validate the status + // of the asset account CFN stack in cross-account scenario (this policy + // is reused when defining permissions for the connector role in asset account) + "cloudformation:DescribeStacks", + "cloudformation:DescribeStackSet", + "cloudformation:ListStacks", + ], + Resource: "*", + }, + + ReadBatchInventory: { + Action: [ + // Just for debugging + "batch:DescribeComputeEnvironments", + "batch:DescribeJobDefinitions", + "batch:DescribeJobQueues", + "batch:DescribeJobs", + "batch:DescribeSchedulingPolicies", + + "batch:GetJobQueueSnapshot", + + "batch:ListJobs", + "batch:ListSchedulingPolicies", + "batch:ListTagsForResource", + ], + Resource: "*", + }, + + ReadDrsInventory: { + Action: [ + "drs:DescribeRecoverySnapshots", + "drs:DescribeSourceServers", + "drs:ListTagsForResource", + ], + Resource: "*", + }, +}; + +/** + * Allow just the readonly actions. Make sure the policy doesn't compile if we + * specify some mutating actions. + */ +type InventoryIamActionPrefix = "List" | "Get" | "Describe"; + +type InventoryIamAction = Extract< + iam.Action, + `${string}:${InventoryIamActionPrefix}${string}` +>; + +interface InventoryIamPolicyStatement extends iam.PolicyStatement { + Action: InventoryIamAction | InventoryIamAction[]; +} diff --git a/codegen/src/common/string.ts b/codegen/src/common/string.ts new file mode 100644 index 0000000..b6a02d9 --- /dev/null +++ b/codegen/src/common/string.ts @@ -0,0 +1,10 @@ +export function collapse( + strings: TemplateStringsArray, + ...args: (string | number)[] +): string { + return strings + .map((str, i) => (i < args.length ? str + args[i] : str)) + .join("") + .replace(/(^\s+|\s+$)/gm, "") + .replace(/\n/g, " "); +} diff --git a/codegen/src/main.ts b/codegen/src/main.ts index 01161a4..9041e66 100644 --- a/codegen/src/main.ts +++ b/codegen/src/main.ts @@ -9,7 +9,7 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; -import { Policy } from "./iam"; +import { Policy } from "./common/iam"; const scriptPath = fileURLToPath(import.meta.url); const scriptDir = path.dirname(scriptPath); diff --git a/codegen/src/policies/ElastioAssetAccountDeployer.ts b/codegen/src/policies/ElastioAssetAccountDeployer.ts index 4ca6bd8..3f03029 100644 --- a/codegen/src/policies/ElastioAssetAccountDeployer.ts +++ b/codegen/src/policies/ElastioAssetAccountDeployer.ts @@ -1,4 +1,4 @@ -import * as iam from "../iam"; +import * as iam from "../common/iam"; /** * Use the following command to discover what resource types are deployed by diff --git a/codegen/src/policies/ElastioAwsBackupEc2Scan.ts b/codegen/src/policies/ElastioAwsBackupEc2Scan.ts index 2254569..d1e7066 100644 --- a/codegen/src/policies/ElastioAwsBackupEc2Scan.ts +++ b/codegen/src/policies/ElastioAwsBackupEc2Scan.ts @@ -1,4 +1,4 @@ -import * as iam from "../iam"; +import * as iam from "../common/iam"; export default { description: "Allows Elastio to scan AWS Backup EC2 and EBS recovery points.", From c3e3b62cbf5eaf260d6b88243770047556bc1957 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Thu, 10 Apr 2025 22:01:56 +0000 Subject: [PATCH 2/5] Start codegen for TF/CFN versions --- codegen/.terraform.lock.hcl | 24 + codegen/package-lock.json | 516 +++++++++++++++++- codegen/package.json | 13 +- codegen/src/asset-account/index.ts | 447 +-------------- codegen/src/asset-account/inputs.ts | 122 +++++ .../src/asset-account/mir/cloud-connector.ts | 343 ++++++++++++ codegen/src/asset-account/mir/index.ts | 83 +++ codegen/src/asset-account/mir/resource.ts | 18 + .../orchestrator/cloudformation.ts | 0 .../asset-account/orchestrator/terraform.ts | 87 +++ codegen/src/common/iam.ts | 15 +- codegen/src/common/inputs.ts | 76 --- codegen/src/common/inventory.ts | 332 +++++------ 13 files changed, 1394 insertions(+), 682 deletions(-) create mode 100644 codegen/.terraform.lock.hcl create mode 100644 codegen/src/asset-account/inputs.ts create mode 100644 codegen/src/asset-account/mir/cloud-connector.ts create mode 100644 codegen/src/asset-account/mir/index.ts create mode 100644 codegen/src/asset-account/mir/resource.ts create mode 100644 codegen/src/asset-account/orchestrator/cloudformation.ts create mode 100644 codegen/src/asset-account/orchestrator/terraform.ts delete mode 100644 codegen/src/common/inputs.ts diff --git a/codegen/.terraform.lock.hcl b/codegen/.terraform.lock.hcl new file mode 100644 index 0000000..49862d5 --- /dev/null +++ b/codegen/.terraform.lock.hcl @@ -0,0 +1,24 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.94.1" + hashes = [ + "h1:pm3uoaQYHaavwE83zsEzAFn/LKD1EWGiYRfzVxNCaIA=", + "zh:14fb41e50219660d5f02b977e6f786d8ce78766cce8c2f6b8131411b087ae945", + "zh:3bc5d12acd5e1a5f1cf78a7f05d0d63f988b57485e7d20c47e80a0b723a99d26", + "zh:4835e49377f80a37c6191a092f636e227a9f086e3cc3f0c9e1b554da8793cfe8", + "zh:605971275adae25096dca30a94e29931039133c667c1d9b38778a09594312964", + "zh:8ae46b4a9a67815facf59da0c56d74ef71bcc77ae79e8bfbac504fa43f267f8e", + "zh:913f3f371c3e6d1f040d6284406204b049977c13cb75aae71edb0ef8361da7dd", + "zh:91f85ae8c73932547ad7139ce0b047a6a7c7be2fd944e51db13231cc80ce6d8e", + "zh:96352ae4323ce137903b9fe879941f894a3ce9ef30df1018a0f29f285a448793", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9b51922c9201b1dc3d05b39f9972715db5f67297deee088793d02dea1832564b", + "zh:a689e82112aa71e15647b06502d5b585980cd9002c3cc8458f092e8c8a667696", + "zh:c3723fa3e6aff3c1cc0088bdcb1edee168fe60020f2f77161d135bf473f45ab2", + "zh:d6a2052b864dd394b01ad1bae32d0a7d257940ee47908d02df7fa7873981d619", + "zh:dda4c9c0406cc54ad8ee4f19173a32de7c6e73abb5a948ea0f342d567df26a1d", + "zh:f42e0fe592b97cbdf70612f0fbe2bab851835e2d1aaf8cbb87c3ab0f2c96bb27", + ] +} diff --git a/codegen/package-lock.json b/codegen/package-lock.json index 174ce43..2d7d7d1 100644 --- a/codegen/package-lock.json +++ b/codegen/package-lock.json @@ -6,15 +6,85 @@ "packages": { "": { "dependencies": { + "@cdktf/hcl-tools": "^0.20.11", + "arktype": "^2.1.19", + "aws-cdk-lib": "^2.189.0", "aws-iam-policy-types": "^1.0.2", - "lodash": "^4.17.21" + "constructs": "^10.4.2", + "fs-extra": "^11.3.0", + "lodash": "^4.17.21", + "prettier": "^3.5.3" }, "devDependencies": { - "@types/node": "^22.13.14", + "@types/lodash": "^4.17.16", + "@types/node": "^22.14.0", "tsx": "^4.19.3", - "typescript": "^5.8.2" + "typescript": "^5.8.3" } }, + "node_modules/@ark/schema": { + "version": "0.45.9", + "resolved": "https://registry.npmjs.org/@ark/schema/-/schema-0.45.9.tgz", + "integrity": "sha512-rG0v/JI0sibn/0wERAHTYVLCtEqoMP2IIlxnb+S5DrEjCI5wpubbZSWMDW50tZ8tV6FANu6zzHDeeKbp6lsZdg==", + "dependencies": { + "@ark/util": "0.45.9" + } + }, + "node_modules/@ark/util": { + "version": "0.45.9", + "resolved": "https://registry.npmjs.org/@ark/util/-/util-0.45.9.tgz", + "integrity": "sha512-0WYNAb8aRGp7dNt6xIvIrRzL7V1XL3u3PK2vcklhtTrdaP235DjC9qJhzidrxtWr68mA5ySSjUrgrXk622bKkw==" + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.231", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.231.tgz", + "integrity": "sha512-vPqD/K2pK/ALhU5r5Nafdc2nLB+LJKxNyxUmQnLsazU6AWDJfkqjHQx8m3J4Cjl2C3chQkIRMdzSDuXIlo43GA==" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "41.2.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-41.2.0.tgz", + "integrity": "sha512-JaulVS6z9y5+u4jNmoWbHZRs9uGOnmn/ktXygNWKNu1k6lF3ad4so3s18eRu15XCbUIomxN9WPYT6Ehh7hzONw==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 14.15.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.1", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@cdktf/hcl-tools": { + "version": "0.20.11", + "resolved": "https://registry.npmjs.org/@cdktf/hcl-tools/-/hcl-tools-0.20.11.tgz", + "integrity": "sha512-ymg3a3vFlLgo3Nbz9zpUUYorQuq0WChJpbyDIfCvAvQ4j1OWwJubmoeDSzh645WCRecwgurRJCly7QACetE03A==" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", @@ -415,13 +485,377 @@ "node": ">=18" } }, + "node_modules/@types/lodash": { + "version": "4.17.16", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", + "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", + "dev": true + }, "node_modules/@types/node": { - "version": "22.13.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", - "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "version": "22.14.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", + "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", "dev": true, "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" + } + }, + "node_modules/arktype": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/arktype/-/arktype-2.1.19.tgz", + "integrity": "sha512-notORSuTSpfLV7rq0kYC4mTgIVlVR0xQuvtFxOaE9aKiXyON/kgoIBwZZcKeSSb4BebNcfJoGlxJicAUl/HMdw==", + "dependencies": { + "@ark/schema": "0.45.9", + "@ark/util": "0.45.9" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.189.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.189.0.tgz", + "integrity": "sha512-B5Uha7uRntOAyuKfU0eFtxij3ZVTzGAbetw5qaXlURa68wsWpKlU72/OyKugB6JYkhjCZkSTVVBxd1pVTosxEw==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "dependencies": { + "@aws-cdk/asset-awscli-v1": "^2.2.229", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^41.0.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.0", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.7.1", + "table": "^6.9.0", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.17.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.11", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.0.6", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.1", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" } }, "node_modules/aws-iam-policy-types": { @@ -435,6 +869,11 @@ "node": ">=18.0.0" } }, + "node_modules/constructs": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", + "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==" + }, "node_modules/esbuild": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", @@ -475,6 +914,19 @@ "@esbuild/win32-x64": "0.25.1" } }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -501,11 +953,41 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -535,9 +1017,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -548,10 +1030,18 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } } } } diff --git a/codegen/package.json b/codegen/package.json index 0477c9d..617546c 100644 --- a/codegen/package.json +++ b/codegen/package.json @@ -4,12 +4,19 @@ "start": "tsc --noEmit && tsx ./src/main.ts" }, "devDependencies": { - "@types/node": "^22.13.14", + "@types/lodash": "^4.17.16", + "@types/node": "^22.14.0", "tsx": "^4.19.3", - "typescript": "^5.8.2" + "typescript": "^5.8.3" }, "dependencies": { + "@cdktf/hcl-tools": "^0.20.11", + "arktype": "^2.1.19", + "aws-cdk-lib": "^2.189.0", "aws-iam-policy-types": "^1.0.2", - "lodash": "^4.17.21" + "constructs": "^10.4.2", + "fs-extra": "^11.3.0", + "lodash": "^4.17.21", + "prettier": "^3.5.3" } } diff --git a/codegen/src/asset-account/index.ts b/codegen/src/asset-account/index.ts index b0707bb..bfa5ec8 100644 --- a/codegen/src/asset-account/index.ts +++ b/codegen/src/asset-account/index.ts @@ -1,436 +1,23 @@ -// import { Construct } from "constructs"; -// import { CfnCondition, Fn, aws_iam as iam, aws_ssm as ssm } from "aws-cdk-lib"; -// import * as condition from "../common/iam/condition"; -// import { version } from "../common/version"; -// import { Aws, CfnOutput, Stack, StackProps } from "aws-cdk-lib"; -// import { DeploymentNotifier } from "../common/deployment-notifier"; -// import { InputParam } from "../common/input-param"; -// import { Globals, MaybeEmpty } from "../common/globals"; -// import { KmsEncryptionKey } from "../common/kms"; -// import { allowKms, createRole, IamRoleProps } from "../common/iam"; -import * as inventory from "../common/inventory"; -import { Input } from "../common/inputs"; -import { collapse } from "../common/string"; -// import _ from "lodash"; +import { ArkErrors } from "arktype"; +import { inputs, InputsIn } from "./inputs"; +import * as mir from "./mir"; +import * as tf from "./orchestrator/terraform"; -const iamPolicyArnRegex = "^arn:aws:iam::[^:]*:policy/.*"; +export { inputs }; -export const inputs = { - cloudConnectorAccountId: { - group: "internal", - displayName: "Cloud connector AWS account ID", - description: collapse` - The ID of the Elastio Connector's account that should scan assets - in this account. It will be trusted to assume the role in this - account to read the assets and create snapshots. - `, - default: null, - type: "string", - }, - cloudConnectorRoleExternalId: { - group: "internal", - displayName: "Connector IAM role external ID", - description: collapse` - The secret token generated specifically for this account that - authenticates the source Elastio Connector to assume the - ElastioCloudConnector role in this account - `, - default: "", - type: "string", - }, - iamResourceNamesPrefix: { - group: "configurable", - displayName: "IAM resource names prefix", - description: - "Add a custom prefix to names of all IAM resources deployed by this stack", - type: "string", - default: "", - }, +export function generate(inputsIn: InputsIn) { + const result = inputs(inputsIn); - iamResourceNamesSuffix: { - group: "configurable", - displayName: "IAM resource names suffix", - description: - "Add a custom suffix to names of all IAM resources deployed by this stack", - type: "string", - default: "", - }, + if (result instanceof ArkErrors) { + throw new Error(`Invalid inputs: ${result}`); + } - globalManagedPolicies: { - group: "configurable", - displayName: "Global IAM managed policies ARNs", - description: "IAM managed policies ARNs to attach to all Elastio IAM roles", - default: null, - type: "set(string)", - allowedPattern: iamPolicyArnRegex, - }, + const resources = mir.resources(result); - globalPermissionBoundary: { - group: "configurable", - displayName: "Global IAM permission boundary policy ARN", - description: collapse` - The ARN of the IAM managed policy to use as a - permission boundary for all Elastio IAM roles - `, - type: "string", - default: "", - allowedPattern: iamPolicyArnRegex, - }, + tf.generate(resources); +} - encryptWithCmk: { - group: "configurable", - displayName: "Encrypt data with customer-managed KMS keys", - type: "bool", - default: false, - description: collapse` - Provision additional customer-managed KMS keys to encrypt - Lambda environment variables, DynamoDB tables, S3. Note that - by default data is encrypted with AWS-managed keys. Enable this - option only if your compliance requirements mandate the usage of CMKs. - If this option is disabled Elastio creates only 1 CMK per region where - the Elastio Connector stack is deployed. If this option is enabled then - Elastio creates 1 KMS key per AWS account and 2 KMS keys per every AWS - region where Elastio is deployed in your AWS Account - `, - }, - - lambdaTracing: { - group: "configurable", - displayName: "Enable AWS X-Ray tracing for Lambda functions", - description: "This increases the cost of the stack. Enable only if needed", - type: "bool", - default: false, - }, -} satisfies Record; - -// this.globals = new Globals({ -// stackName: "AssetAccount", -// iamPrefix: iamResourceNamesPrefix.valueAsString, -// iamSuffix: iamResourceNamesSuffix.valueAsString, -// globalManagedPolicies: MaybeEmpty.fromListInput(globalManagedPolicies), -// globalPermissionBoundary: MaybeEmpty.fromStringInput( -// globalPermissionBoundary, -// ), -// kmsEncryptionKey: new KmsEncryptionKey( -// this, -// "kmsEncryptionKey", -// encryptWithCmk, -// "alias/elastio-asset-account-encryption", -// ).key, -// lambdaTracing: new CfnCondition(this, "lambdaTracingCondition", { -// expression: Fn.conditionEquals(lambdaTracing, "true"), -// }), -// }); - -// new DeploymentNotifier({ -// scope: this, -// id: "deploymentNotifier", -// globals: this.globals, -// payload: { -// stackKind: "asset", -// stackVersion: version.asset_account, -// accountId: Aws.ACCOUNT_ID, -// cloudConnectorAccountId, -// }, -// }); - -// new ssm.StringParameter(this, "assetAccountStackName", { -// parameterName: "/elastio/asset/account/stack-name", -// description: -// "The name of the Asset Account CloudFormation stack used " + -// "for discovery by the Cloud Connector", -// stringValue: Aws.STACK_NAME, -// }); - -// const inventoryEventTargetRole = this.createRole({ -// id: "inventoryEventTarget", -// name: "InventoryEventTarget", -// assumedBy: new iam.ServicePrincipal("events.amazonaws.com"), -// statements: [ -// { -// actions: ["events:PutEvents"], -// resources: [ -// `arn:aws:events:*:${cloudConnectorAccountId.valueAsString}:event-bus/elastio-*`, -// ], -// }, -// ], -// }); - -// const assetRegionStackDeployerRole = this.createRole({ -// id: "assetRegionStackDeployer", -// name: "AssetRegionStackDeployer", -// description: "Used by Cloudformation to deploy region-level stack", -// assumedBy: new iam.ServicePrincipal("cloudformation.amazonaws.com"), -// statements: [ -// { -// actions: [ -// "events:DescribeRule", -// "events:ListTargetsByRule", -// "events:ListTagsForResource", -// "events:PutRule", -// "events:PutTargets", -// "events:RemoveTargets", -// "events:DeleteRule", -// "events:EnableRule", -// "events:DisableRule", -// ], -// resources: [`arn:aws:events:*:${Aws.ACCOUNT_ID}:rule/elastio-*`], -// }, -// { -// actions: [ -// // Allows assigning of `inventoryEventTargetRole` role -// // to the event targets -// "iam:PassRole", -// ], -// resources: [inventoryEventTargetRole.roleArn], -// }, -// ], -// }); - -// const cloudConnectorStatements: Record = -// { -// WriteEc2: [ -// // Create and copy snapshots to the cloud connector account -// { -// actions: [ -// "ec2:CreateSnapshot", -// "ec2:CreateSnapshots", -// "ec2:CopySnapshot", -// ], -// resources: ["*"], -// }, -// { -// actions: [ -// "ec2:DeleteSnapshot", - -// // This is used in AWS Backup restore test scenario -// "ec2:StopInstances", -// "ec2:TerminateInstances", - -// // Allows additional tags for elastio resources. -// // It's used, for example, to add new tag on a -// // snapshot which indicates that it's clean or infected -// "ec2:CreateTags", -// "ec2:DeleteTags", -// ], -// resources: ["*"], -// conditions: condition.hasResourceTag("elastio:resource"), -// }, - -// { -// actions: ["ec2:ModifySnapshotAttribute"], -// resources: ["*"], - -// conditions: { -// // Needed to add createVolumePermission for the connector account. -// StringEquals: { -// "ec2:Add/userId": cloudConnectorAccountId.valueAsString, -// // Even though EC2 IAM reference says there are two more -// // things that could be used in the condition: -// // "ec2:Attribute" and "ec2:Attribute/${AttributeName}", -// // none of them are actually present when the request -// // is evaluated. This can be seen by adding a condition like -// // "ec2:Attribute": "createVolumePermission" and observing -// // an error with an encoded authorization message, that -// // shows the real evaluation context when decoded. -// }, -// }, -// }, - -// // This is used in AWS Backup restore test scenario to mutate -// // the created temporary instance/volumes -// { -// actions: [ -// "ec2:ModifyInstanceAttribute", -// "ec2:CreateTags", -// "ec2:DeleteTags", -// ], -// resources: ["*"], -// conditions: condition.hasResourceTag("awsbackup-restore-test"), -// }, - -// // Allow assigning tags when creating new resources -// { -// actions: ["ec2:CreateTags"], -// resources: [ -// "arn:aws:ec2:*:*:volume/*", -// "arn:aws:ec2:*::snapshot/*", -// ], -// conditions: { -// StringLike: { -// "ec2:CreateAction": "*", -// }, -// }, -// }, -// { -// actions: ["ssm:SendCommand"], -// resources: [ -// "arn:aws:ssm:*:*:document/AWSEC2-CreateVssSnapshot", -// "arn:aws:ec2:*:*:instance/*", -// ], -// }, -// { -// actions: [ -// "ssm:GetConnectionStatus", -// "ssm:GetCommandInvocation", -// "ssm:ListCommands", -// ], -// resources: ["*"], -// }, -// ], -// ReadEbs: [ -// { -// actions: ["ebs:ListSnapshotBlocks", "ebs:GetSnapshotBlock"], -// resources: ["*"], -// }, -// ], -// ReadSsm: [ -// // Required to be able to try to do app-consistent snapshots -// // of EC2 Windows (VSS snapshots). -// { -// actions: ["ssm:GetParameters", "ssm:GetParameter"], -// resources: [ -// `arn:aws:ssm:*:${Aws.ACCOUNT_ID}:parameter/elastio/*`, -// `arn:aws:ssm:*::parameter/aws/*`, -// ], -// }, -// ], -// ReadIam: [ -// { -// actions: ["iam:GetInstanceProfile", "iam:SimulatePrincipalPolicy"], -// resources: ["*"], -// }, -// ], -// WriteCloudformation: [ -// { -// actions: [ -// "cloudformation:CreateStack", -// "cloudformation:UpdateStack", -// ], -// resources: [ -// `arn:aws:cloudformation:*:${Aws.ACCOUNT_ID}:stack/elastio-*/*`, -// ], -// conditions: { -// StringEquals: { -// ["cloudformation:RoleArn"]: -// assetRegionStackDeployerRole.roleArn, -// }, -// }, -// }, -// { -// // Allows running cloudformation:CreateStack/UpdateStack -// // on behalf of `assetRegionStackDeployer` role. -// actions: ["iam:PassRole"], -// resources: [assetRegionStackDeployerRole.roleArn], -// }, -// { -// actions: ["cloudformation:TagResource"], -// resources: [ -// `arn:aws:cloudformation:*:${Aws.ACCOUNT_ID}:stack/elastio-*/*`, -// ], -// conditions: { -// StringLike: { -// "ec2:CreateAction": "*", -// }, -// }, -// }, -// { -// actions: [ -// // We need to delete the asset region stack -// "cloudformation:DeleteStack", -// "cloudformation:TagResource", -// "cloudformation:UntagResource", -// ], -// resources: [ -// `arn:aws:cloudformation:*:${Aws.ACCOUNT_ID}:stack/elastio-*/*`, -// ], -// conditions: condition.hasResourceTag("elastio:resource"), -// }, -// ], -// ReadS3: [ -// { -// actions: [ -// // These are actually used by the scan job at the time of this writing -// "s3:ListBucket", -// "s3:GetObjectVersion", -// "s3:GetObject", -// "s3:GetBucketTagging", - -// // These permissions were used at the time when we supported native Elastio -// // backups and restores. These are readonly, and they are left here for -// // the future when we might need them or for easier debugging. -// "s3:GetReplicationConfiguration", -// "s3:GetMetricsConfiguration", -// "s3:GetLifecycleConfiguration", -// "s3:GetInventoryConfiguration", -// "s3:GetIntelligentTieringConfiguration", -// "s3:GetEncryptionConfiguration", -// "s3:GetBucketWebsite", -// "s3:GetBucketVersioning", -// "s3:GetBucketRequestPayment", -// "s3:GetBucketPublicAccessBlock", -// "s3:GetBucketPolicy", -// "s3:GetBucketOwnershipControls", -// "s3:GetBucketObjectLockConfiguration", -// "s3:GetBucketNotification", -// "s3:GetBucketLogging", -// "s3:GetBucketLocation", -// "s3:GetBucketAcl", -// "s3:GetAnalyticsConfiguration", -// "s3:GetAccelerateConfiguration", -// ], -// resources: ["*"], -// }, -// ], -// ReadSqs: [ -// { -// // Read the S3 changelog SQS queue -// actions: ["sqs:ReceiveMessage", "sqs:DeleteMessage"], -// resources: ["*"], -// conditions: condition.hasResourceTag("elastio:resource"), -// }, -// ], -// ReadDrs: [ -// { -// actions: [ -// "drs:DescribeRecoverySnapshots", -// "drs:DescribeSourceServers", -// "drs:ListTagsForResource", -// ], -// resources: ["*"], -// }, -// ], -// // Required by `ModifySnapshotAttributeRequest`, on encrypted snapshots -// ReadKms: [allowKms()], -// }; - -// const statements = _.mergeWith( -// inventoryIamPolicy, -// cloudConnectorStatements, -// (dest, _src, key) => { -// if (dest !== undefined) { -// throw new Error( -// `Duplicate policy statements in cloud connector policy at ${key}`, -// ); -// } -// }, -// ); - -// this.createRole({ -// id: "cloudConnector", -// name: "CloudConnector", -// description: -// "Allows Elastio Cloud Connector to access the assets in this account", -// assumedBy: new iam.ArnPrincipal( -// `arn:aws:iam::${cloudConnectorAccountId.valueAsString}:role/` + -// iamResourceNamesPrefix.valueAsString + -// "ElastioCloudConnectorBastion" + -// iamResourceNamesSuffix.valueAsString, -// ), -// externalIds: [cloudConnectorRoleExternalId.valueAsString], -// statements, -// }); - -// new CfnOutput(this, "cfnTemplateVersion", { -// value: version.asset_account, -// }); +generate({ + connectorAccountId: "123456789012", + connectorRoleExternalId: "external-id", +}); diff --git a/codegen/src/asset-account/inputs.ts b/codegen/src/asset-account/inputs.ts new file mode 100644 index 0000000..3108ced --- /dev/null +++ b/codegen/src/asset-account/inputs.ts @@ -0,0 +1,122 @@ +import { type } from "arktype"; +import { collapse } from "../common/string"; + +export type Inputs = typeof inputs.inferOut; +export type InputsIn = typeof inputs.inferIn; + +const IamPolicyArn = type(/^arn:aws:iam::[^:]*:policy\/.*/); + +export const inputs = type({ + connectorAccountId: type(/^\d{12}$/).configure({ + title: "Cloud connector AWS account ID", + description: collapse` + The ID of the Elastio Connector's account that should scan assets + in this account. It will be trusted to assume the role in this + account to read the assets and create snapshots. + `, + }), + + connectorRoleExternalId: type("string").configure({ + title: "Connector IAM role external ID", + description: collapse` + The secret token generated specifically for this account that + authenticates the source Elastio Connector to assume the + ElastioCloudConnector role in this account + `, + actual: "", + }), + + disableDeploymentNotification: type("boolean") + .configure({ + title: "Deployment Notification", + description: collapse` + Send a deployment notification to the Elastio Connector + account. This is required for the connector to be able to + discover the assets in this account. + `, + }) + .default(false), + + deploymentNotificationToken: type("string") + .configure({ + title: "Deployment notification token", + description: collapse` + Token sent to the SNS topic to authenticate the deployment notification. + `, + actual: "", + }) + .optional(), + + deploymentNotificationSnsTopicArn: type("string") + .configure({ + title: "Deployment notification SNS topic ARN", + description: collapse` + ARN of the Elastio tenant SNS topic where to publish a notification about a + completed stack deployment. + `, + }) + .optional(), + + iamResourceNamesPrefix: type("string") + .configure({ + title: "IAM resource names prefix", + description: + "Add a custom prefix to names of all IAM resources deployed by this stack", + }) + .default(""), + + iamResourceNamesSuffix: type("string") + .configure({ + title: "IAM resource names suffix", + description: + "Add a custom suffix to names of all IAM resources deployed by this stack", + }) + .default(""), + + globalManagedPolicies: IamPolicyArn.array() + .configure({ + title: "Global IAM managed policies ARNs", + description: + "IAM managed policies ARNs to attach to all Elastio IAM roles", + }) + .narrow((list, ctx) => { + const unique = new Set(list); + return ( + unique.size === list.length || + ctx.reject("list contains duplicate items") + ); + }) + .default(() => []), + + globalPermissionBoundary: IamPolicyArn.configure({ + title: "Global IAM permission boundary policy ARN", + description: collapse` + The ARN of the IAM managed policy to use as a + permission boundary for all Elastio IAM roles + `, + }).optional(), + + encryptWithCmk: type("boolean") + .configure({ + title: "Encrypt data with customer-managed KMS keys", + description: collapse` + Provision additional customer-managed KMS keys to encrypt + Lambda environment variables, DynamoDB tables, S3. Note that + by default data is encrypted with AWS-managed keys. Enable this + option only if your compliance requirements mandate the usage of CMKs. + If this option is disabled Elastio creates only 1 CMK per region where + the Elastio Connector stack is deployed. If this option is enabled then + Elastio creates 1 KMS key per AWS account and 2 KMS keys per every AWS + region where Elastio is deployed in your AWS Account + `, + }) + .default(false), + + lambdaTracing: type("boolean") + .configure({ + title: "Enable AWS X-Ray tracing for Lambda functions", + description: + "This increases the cost of the stack. Enable only if needed", + }) + .default(false), +}); diff --git a/codegen/src/asset-account/mir/cloud-connector.ts b/codegen/src/asset-account/mir/cloud-connector.ts new file mode 100644 index 0000000..2d17b3a --- /dev/null +++ b/codegen/src/asset-account/mir/cloud-connector.ts @@ -0,0 +1,343 @@ +import * as iam from "../../common/iam"; +import * as inventory from "../../common/inventory"; +import { Inputs } from "../inputs"; +import _ from "lodash"; +import { IamRole } from "./resource"; + +export function cloudConnectorRole(inputs: Inputs): IamRole { + const otherStatements: Record = { + WriteEc2: [ + // Create and copy snapshots to the cloud connector account + { + Action: [ + "ec2:CreateSnapshot", + "ec2:CreateSnapshots", + "ec2:CopySnapshot", + ], + Resource: ["*"], + }, + { + Action: [ + "ec2:DeleteSnapshot", + + // This is used in AWS Backup restore test scenario + "ec2:StopInstances", + "ec2:TerminateInstances", + + // Allows additional tags for elastio resources. + // It's used, for example, to add new tag on a + // snapshot which indicates that it's clean or infected + "ec2:CreateTags", + "ec2:DeleteTags", + ], + Resource: ["*"], + Condition: iam.hasResourceTag("elastio:resource"), + }, + + { + Action: ["ec2:ModifySnapshotAttribute"], + Resource: ["*"], + + Condition: { + // Needed to add createVolumePermission for the connector account. + StringEquals: { + "ec2:Add/userId": inputs.connectorAccountId, + // Even though EC2 IAM reference says there are two more + // things that could be used in the condition: + // "ec2:Attribute" and "ec2:Attribute/${AttributeName}", + // none of them are actually present when the request + // is evaluated. This can be seen by adding a condition like + // "ec2:Attribute": "createVolumePermission" and observing + // an error with an encoded authorization message, that + // shows the real evaluation context when decoded. + }, + }, + }, + + // This is used in AWS Backup restore test scenario to mutate + // the created temporary instance/volumes + { + Action: [ + "ec2:ModifyInstanceAttribute", + "ec2:CreateTags", + "ec2:DeleteTags", + ], + Resource: ["*"], + Condition: iam.hasResourceTag("awsbackup-restore-test"), + }, + + // Allow assigning tags when creating new resources + { + Action: ["ec2:CreateTags"], + Resource: ["arn:aws:ec2:*:*:volume/*", "arn:aws:ec2:*::snapshot/*"], + Condition: { + StringLike: { + "ec2:CreateAction": "*", + }, + }, + }, + { + Action: ["ssm:SendCommand"], + Resource: [ + "arn:aws:ssm:*:*:document/AWSEC2-CreateVssSnapshot", + "arn:aws:ec2:*:*:instance/*", + ], + }, + { + Action: [ + "ssm:GetConnectionStatus", + "ssm:GetCommandInvocation", + "ssm:ListCommands", + ], + Resource: ["*"], + }, + ], + + ReadEbs: [ + { + Action: ["ebs:ListSnapshotBlocks", "ebs:GetSnapshotBlock"], + Resource: ["*"], + }, + ], + + ReadSsm: [ + // Required to be able to try to do app-consistent snapshots + // of EC2 Windows (VSS snapshots). + { + Action: ["ssm:GetParameters", "ssm:GetParameter"], + Resource: [ + `arn:aws:ssm:*:{{account_id}}:parameter/elastio/*`, + `arn:aws:ssm:*::parameter/aws/*`, + ], + }, + ], + + ReadIam: [ + { + Action: ["iam:GetInstanceProfile", "iam:SimulatePrincipalPolicy"], + Resource: ["*"], + }, + ], + + WriteCloudformation: [ + { + Action: ["cloudformation:CreateStack", "cloudformation:UpdateStack"], + Resource: [`arn:aws:cloudformation:*:{{account_id}}:stack/elastio-*/*`], + Condition: { + StringEquals: { + ["cloudformation:RoleArn"]: + "{{aws_iam_role.asset_region_stack_deployer.arn}}", + }, + }, + }, + { + // Allows running cloudformation:CreateStack/UpdateStack + // on behalf of `asset_region_stack_deployer` role. + Action: ["iam:PassRole"], + Resource: ["{{aws_iam_role.asset_region_stack_deployer.arn}}"], + }, + { + Action: ["cloudformation:TagResource"], + Resource: [`arn:aws:cloudformation:*:{{account_id}}:stack/elastio-*/*`], + Condition: { + StringLike: { + "ec2:CreateAction": "*", + }, + }, + }, + { + Action: [ + // We need to delete the asset region stack + "cloudformation:DeleteStack", + "cloudformation:TagResource", + "cloudformation:UntagResource", + ], + Resource: [`arn:aws:cloudformation:*:{{account_id}}:stack/elastio-*/*`], + Condition: iam.hasResourceTag("elastio:resource"), + }, + ], + + ReadS3: [ + { + Action: [ + // These are actually used by the scan job at the time of this writing + "s3:ListBucket", + "s3:GetObjectVersion", + "s3:GetObject", + "s3:GetBucketTagging", + + // These permissions were used at the time when we supported native Elastio + // backups and restores. These are readonly, and they are left here for + // the future when we might need them or for easier debugging. + "s3:GetReplicationConfiguration", + "s3:GetMetricsConfiguration", + "s3:GetLifecycleConfiguration", + "s3:GetInventoryConfiguration", + "s3:GetIntelligentTieringConfiguration", + "s3:GetEncryptionConfiguration", + "s3:GetBucketWebsite", + "s3:GetBucketVersioning", + "s3:GetBucketRequestPayment", + "s3:GetBucketPublicAccessBlock", + "s3:GetBucketPolicy", + "s3:GetBucketOwnershipControls", + "s3:GetBucketObjectLockConfiguration", + "s3:GetBucketNotification", + "s3:GetBucketLogging", + "s3:GetBucketLocation", + "s3:GetBucketAcl", + "s3:GetAnalyticsConfiguration", + "s3:GetAccelerateConfiguration", + ], + Resource: ["*"], + }, + ], + + ReadSqs: [ + { + // Read the S3 changelog SQS queue + Action: ["sqs:ReceiveMessage", "sqs:DeleteMessage"], + Resource: ["*"], + Condition: iam.hasResourceTag("elastio:resource"), + }, + ], + + ReadDrs: [ + { + Action: [ + "drs:DescribeRecoverySnapshots", + "drs:DescribeSourceServers", + "drs:ListTagsForResource", + ], + Resource: ["*"], + }, + ], + + ReadKms: [allowKms()], + }; + + const statements = _.mergeWith( + inventory.policy, + otherStatements, + (dest, _src, key) => { + if (dest !== undefined) { + throw new Error( + `Duplicate policy statements in cloud connector policy at ${key}`, + ); + } + }, + ); + + return { + type: "aws_iam_role", + name: "ElastioCloudConnector", + description: + "Allows Elastio Cloud Connector to access the assets in this account", + + assumeRolePolicy: { + Action: ["sts:AssumeRole"], + + Principal: { + AWS: + `arn:aws:iam::${inputs.connectorAccountId}:role/` + + inputs.iamResourceNamesPrefix + + "ElastioCloudConnectorBastion" + + inputs.iamResourceNamesSuffix, + }, + + Condition: { + StringEquals: { + "sts:ExternalId": inputs.connectorRoleExternalId, + }, + }, + }, + + statements, + }; +} + +/** + * KMS actions needed to decrypt the data encrypted with the KMS key. + * This is used to decrypt data stored on EBS volumes and S3 buckets + * encrypted with a custom customer-managed KMS, for example. + * + * We explicitly add these permissions to our roles' IAM policies. Then we expect + * users to add a special tag `elastio:authorize` to their KMS keys to grant Elastio + * access to their KMS keys. The users also need to setup a KMS key resource-based + * policy that enables the AWS account where Elastio is deployed managing access + * via IAM policies through a KMS key policy like this: + * + * ```json + * { + * "Sid": "Enable IAM User Permissions", + * "Effect": "Allow", + * "Principal": { + * "AWS": "arn:aws:iam::111122223333:root" + * }, + * "Action": "kms:*", + * "Resource": "*" + * } + * ``` + * This is taken from [AWS docs](https://docs.aws.amazon.com/kms/latest/developerguide/key-policy-default.html#key-policy-default-allow-root-enable-iam). + * + * Note that KMS resource-based policies similar to IAM role trust policies (which + * also qualify as resource-based policies) are exceptional in their behavior. + * See the excerpt from the AWS docs below about KMS key resource-based policies: + * + * > When the principal in a key policy statement is the account principal, + * > the policy statement doesn't give any IAM principal permission to use the + * > KMS key. Instead, it allows the account to use IAM policies to delegate + * > the permissions specified in the policy statement. This default key policy + * > statement allows the account to use IAM policies to delegate permission for + * > all actions (kms:*) on the KMS key. + * + * # Cross account scenario + * + * Note that IAM acts differently when cross-account access is involved in a general case. + * However, KMS with its exceptional behavior already acts similar to the cross-account + * scenario even within the same account, because it requires the resource-based policy + * to allow the direct principal to use the KMS key, or allow the entire root account + * principal and require the IAM role to have the identity-based policy that allows + * the KMS action. + * + * See details in the [issue comment](https://github.com/elastio/elastio/issues/9220#issuecomment-2110983526) + */ +export function allowKms(): iam.PolicyStatement { + return { + Action: [ + // These actions are needed to reencrypt the volumes that were encrypted + // by the KMS key. + "kms:ReEncryptFrom", + "kms:ReEncryptTo", + "kms:CreateGrant", + "kms:Encrypt", + + // This action is needed to describe the KMS key. + // Needed only for some cases. For example, when we want to backup and EBS volume + // that was created from a snapshot of the root volume of an EC2 instance. + // These calls are made by the ebs.amazonaws.com and not by our code. + "kms:DescribeKey", + + // GenerateDataKeyWithoutPlaintext in particular is required in case when + // we create a volume from an unencrypted snapshot but there is a default + // KMS encryption key set in EBS for the volume. + "kms:GenerateDataKey", + "kms:GenerateDataKeyWithoutPlaintext", + + // This is required when reading S3 buckets encrypted with a KMS key + "kms:Decrypt", + ], + + Resource: ["*"], + + // Users need to put a special tag `elastio:authorize` on their KMS keys to + // allow Elastio to use them for decrypting their data. We also allow + // access to Elastio-owned keys marked with the `elastio:resource` tag. + Condition: { + StringLike: { + ...iam.hasResourceTag("elastio:authorize").StringLike, + ...iam.hasResourceTag("elastio:resource").StringLike, + }, + }, + }; +} diff --git a/codegen/src/asset-account/mir/index.ts b/codegen/src/asset-account/mir/index.ts new file mode 100644 index 0000000..3ed26fb --- /dev/null +++ b/codegen/src/asset-account/mir/index.ts @@ -0,0 +1,83 @@ +import type { Resource } from "./resource"; +import { Inputs } from "../inputs"; +import { cloudConnectorRole } from "./cloud-connector"; + +export { Resource }; + +const version = "0.35.13"; + +export function resources(inputs: Inputs): Record { + return { + inventory_event_target: { + type: "aws_iam_role", + name: "ElastioInventoryEventTarget", + description: + "Role assumed by EventBridge to send events to the Elastio Connector", + + assumeRolePolicy: { + Action: "sts:AssumeRole", + Principal: { + Service: "events.amazonaws.com", + }, + }, + statements: { + SendEventsToConnectorAccount: [ + { + Action: ["events:PutEvents"], + Resource: [ + `arn:aws:events:*:${inputs.connectorAccountId}:event-bus/elastio-*`, + ], + }, + ], + }, + }, + + asset_region_stack_deployer: { + type: "aws_iam_role", + name: "ElastioAssetRegionStackDeployer", + description: "Used by Cloudformation to deploy region-level stack", + assumeRolePolicy: { + Action: "sts:AssumeRole", + Principal: { + Service: "cloudformation.amazonaws.com", + }, + }, + statements: { + ManageElastioEventBridgeRules: [ + { + Action: [ + "events:DescribeRule", + "events:ListTargetsByRule", + "events:ListTagsForResource", + "events:PutRule", + "events:PutTargets", + "events:RemoveTargets", + "events:DeleteRule", + "events:EnableRule", + "events:DisableRule", + ], + Resource: [`arn:aws:events:*:{{account_id}}:rule/elastio-*`], + }, + { + Action: [ + // Allows assigning of `inventory_event_target` role + // to the event targets + "iam:PassRole", + ], + Resource: "{{aws_iam_role.inventory_event_target.arn}}", + }, + ], + }, + }, + + cloud_connector: cloudConnectorRole(inputs), + }; + + // new ssm.StringParameter(this, "assetAccountStackName", { + // parameterName: "/elastio/asset/account/stack-name", + // description: + // "The name of the Asset Account CloudFormation stack used " + + // "for discovery by the Cloud Connector", + // stringValue: Aws.STACK_NAME, + // }); +} diff --git a/codegen/src/asset-account/mir/resource.ts b/codegen/src/asset-account/mir/resource.ts new file mode 100644 index 0000000..408656f --- /dev/null +++ b/codegen/src/asset-account/mir/resource.ts @@ -0,0 +1,18 @@ +import * as iam from "../../common/iam"; + +export type Resource = IamRole | SsmParameter; + +export type IamRole = { + type: "aws_iam_role"; + name: string; + description: string; + assumeRolePolicy: iam.PolicyStatement; + statements: Record; +}; + +type SsmParameter = { + type: "aws_ssm_parameter"; + name: string; + description: string; + value: string; +}; diff --git a/codegen/src/asset-account/orchestrator/cloudformation.ts b/codegen/src/asset-account/orchestrator/cloudformation.ts new file mode 100644 index 0000000..e69de29 diff --git a/codegen/src/asset-account/orchestrator/terraform.ts b/codegen/src/asset-account/orchestrator/terraform.ts new file mode 100644 index 0000000..3457731 --- /dev/null +++ b/codegen/src/asset-account/orchestrator/terraform.ts @@ -0,0 +1,87 @@ +import type { Resource } from "../mir"; +import _ from "lodash"; +import * as hclTools from "@cdktf/hcl-tools"; +import * as iam from "../../common/iam"; +import * as prettier from "prettier"; + +async function literal(value: unknown): Promise { + const json = JSON.stringify(value, null, 2); + const pretty = await prettier.format(json, { parser: "json" }); + return pretty.trim(); +} + +async function jsonencode(value: unknown): Promise { + return `jsonencode(\n${await literal(value)}\n)`; +} + +function policyDocument(statements: iam.PolicyStatement[]) { + return { + Version: "2012-10-17", + Statement: statements.map((statement) => ({ + ...statement, + Effect: statement.Effect ?? "Allow", + })), + }; +} + +export async function generate(resources: Record) { + const parts = []; + + for (const [id, resource] of Object.entries(resources)) { + switch (resource.type) { + case "aws_iam_role": { + parts.push( + `resource "aws_iam_role" "${id}" { + name = "${resource.name}" + assume_role_policy = ${await jsonencode(policyDocument([resource.assumeRolePolicy]))} + }`, + ); + + const statements = Object.entries(resource.statements); + + if (statements.length === 0) { + continue; + } + + const policies = _.mapValues( + resource.statements, + (statement) => policyDocument(statement).Statement, + ); + + parts.push( + `resource "aws_iam_role_policy" "${id}" { + role = aws_iam_role.${id}.name + name = each.key + policy = jsonencode( + { + "Version": "2012-10-17", + "Statement": each.value + } + ) + for_each = ${await literal(policies)} + }`, + ); + } + case "aws_ssm_parameter": { + } + } + } + + parts.push(` + data "aws_caller_identity" "current" {} + locals { + account_id = data.aws_caller_identity.current.account_id + } + `); + + const content = parts + .join("\n\n") + .replaceAll("{{account_id}}", "${local.account_id}") + .replaceAll(/\{\{(.*)\}\}/g, "${$1}"); + + console.log(await hclTools.format(content)); +} + +function camelCaseToSnakeCase(str: string): string { + return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase(); +} diff --git a/codegen/src/common/iam.ts b/codegen/src/common/iam.ts index e06ebb8..4c4f3d0 100644 --- a/codegen/src/common/iam.ts +++ b/codegen/src/common/iam.ts @@ -9,23 +9,23 @@ export interface Policy { statements: PolicyStatement[]; } -export interface PolicyStatement { +export type PolicyStatement = { /** * If not specified then `Allow` is assumed. */ Effect?: "Deny"; Action: Action | Action[]; - Principal?: Principal; - Resource: string | string[]; Condition?: Record; + Principal?: Principal; + Resource?: string | string[]; /** * Statement ID usually used as a description of the statement. */ Sid?: string; -} +}; -type Principal = +export type Principal = | "*" | { AWS: string | string[]; @@ -46,13 +46,16 @@ export type Action = | `${iam.AwsEbsActions}` | `${iam.AwsEc2Actions}` | `${iam.AwsElasticfilesystemActions}` + | `${iam.AwsEventsActions}` | `${iam.AwsFsxActions}` | `${iam.AwsIamActions}` | `${iam.AwsKmsActions}` | `${iam.AwsLambdaActions}` | `${iam.AwsLogsActions}` | `${iam.AwsS3Actions}` - | `${iam.AwsSsmActions}`; + | `${iam.AwsSqsActions}` + | `${iam.AwsSsmActions}` + | `${iam.AwsStsActions}`; type KnownTag = // A simple tag that customers can add to their resource for Elastio to diff --git a/codegen/src/common/inputs.ts b/codegen/src/common/inputs.ts deleted file mode 100644 index d25d5cc..0000000 --- a/codegen/src/common/inputs.ts +++ /dev/null @@ -1,76 +0,0 @@ -// The order of keys and values matters! This is the order in which - -import { InputType } from "zlib"; - -// the groups will be displayed in the CloudFormation UI and in the docs. -export const group = { - configurable: "Configurable parameters", - internal: "Non-configurable (internal) parameters", - experimental: "Experimental parameters", -}; - -type BoolInput = { - type: "bool"; - default?: null | boolean; -}; - -type StringInput = { - type: "string"; - default?: null | string; - allowedValues?: string[]; - - /** - * The allowedPattern is a regular expression that the input value must - * match. The regular expression must be very conservative in its syntax. - * It must be compatible with both the CFN (Java) and Terraform (Go) - * regular expression engines. - */ - allowedPattern?: string; -}; - -type NumberInput = { - type: "number"; - default?: null | number; -}; - -type StringSetInput = { - type: "set(string)"; - default?: null | string[]; - - /** - * Allowed pattern for a single item in the string set. - * - * The allowedPattern is a regular expression that the input value must - * match. The regular expression must be very conservative in its syntax. - * It must be compatible with both the CFN (Java) and Terraform (Go) - * regular expression engines. - */ - allowedPattern?: string; -}; - -export type Input = (BoolInput | StringInput | NumberInput | StringSetInput) & { - /** - * The description is required at the type level to make a developer to - * pay attention to documenting the input parameter. The description will - * be visible to the end users, so keep it in mind that they will likely - * read it. - */ - description?: string; - - /** - * Whether the parameter is user configurable. If false, the parameter - * must not be modified by the end users. A special warning will be inserted - * into the description of the parameter to prevent the form changing the - * value of the parameter. The value of the parameter is expected to be set - * by the Elastio Tenant or the Cloud Connector itself (for asset region stack, - * for example). - * - * Default: false - */ - group?: keyof typeof group; - - /** - * Set how the parameter is displayed in the CloudFormation UI. - */ - displayName: string; -}; diff --git a/codegen/src/common/inventory.ts b/codegen/src/common/inventory.ts index d8f9449..b3a9ab4 100644 --- a/codegen/src/common/inventory.ts +++ b/codegen/src/common/inventory.ts @@ -4,158 +4,182 @@ import * as iam from "./iam"; * Permissions for reading inventory shared across lambas, background jobs, * ElastioTenant (Connector Account) and CloudConnector (Asset Account) roles. */ -export const policy: Record = { - ReadBackupInventory: { - Action: [ - // Vaults - "backup:ListBackupVaults", - "backup:DescribeBackupVault", - - // Recovery points - "backup:ListRecoveryPointsByResource", - "backup:DescribeRecoveryPoint", - "backup:ListRecoveryPointsByBackupVault", - "backup:GetRecoveryPointRestoreMetadata", - - // Common for all resources - "backup:ListTags", - - // Misc. This won't be used at the time of this writing, but - // may come in handy in the future? - "backup:ListProtectedResources", - "backup:ListProtectedResourcesByBackupVault", - ], - Resource: "*", - }, - - ReadEfsInventory: { - Action: [ - "elasticfilesystem:DescribeFileSystems", - "elasticfilesystem:ListTagsForResource", - "elasticfilesystem:DescribeTags", - ], - Resource: "*", - }, - - ReadFsxInventory: { - Action: [ - "fsx:DescribeVolumes", - "fsx:DescribeBackups", - "fsx:DescribeFileSystems", - "fsx:DescribeStorageVirtualMachines", - "fsx:ListTagsForResource", - ], - Resource: "*", - }, - - ReadEbsInventory: { - Action: [ - // Volumes - "ec2:DescribeVolumeStatus", - "ec2:DescribeVolumes", - - // Snapshots - "ec2:DescribeSnapshots", - "ec2:DescribeSnapshotAttribute", - - // Common for all resources - "ec2:DescribeTags", - - // Used for cost estimation - "ebs:ListSnapshotBlocks", - "ebs:ListChangedBlocks", - ], - Resource: "*", - }, - - ReadEc2Inventory: { - Action: [ - "ec2:DescribeInstances", - "ec2:DescribeImages", - "ec2:DescribeHosts", - "ssm:DescribeInstanceInformation", - ], - Resource: "*", - }, - - ReadVpcInventory: { - Action: [ - // Used for network config troubleshooting - "ec2:DescribeAvailabilityZones", - "ec2:DescribeSecurityGroups", - "ec2:DescribeSubnets", - "ec2:DescribeVpcs", - "ec2:DescribeRouteTables", - "ec2:DescribeNatGateways", - "ec2:DescribeVpcEndpoints", - ], - Resource: "*", - }, - - ReadS3Inventory: { - Action: [ - "s3:ListAllMyBuckets", - "s3:GetBucketLocation", - "s3:GetBucketTagging", - "s3:GetBucketObjectLockConfiguration", - "s3:GetBucketAcl", - "s3:GetBucketVersioning", - "s3:GetBucketPolicy", - "s3:GetBucketLogging", - "s3:ListBucket", - ], - Resource: "*", - }, - - ReadAccountsInventory: { - Action: ["iam:ListAccountAliases", "ec2:DescribeRegions"], - Resource: "*", - }, - - ReadKmsInventory: { - Action: ["kms:DescribeKey"], - Resource: "*", - }, - - ReadCloudformationInventory: { - Action: [ - // Required for discovering Elastio CFN stack version by blue stack - // Also used by used by the `accs` service to validate the status - // of the asset account CFN stack in cross-account scenario (this policy - // is reused when defining permissions for the connector role in asset account) - "cloudformation:DescribeStacks", - "cloudformation:DescribeStackSet", - "cloudformation:ListStacks", - ], - Resource: "*", - }, - - ReadBatchInventory: { - Action: [ - // Just for debugging - "batch:DescribeComputeEnvironments", - "batch:DescribeJobDefinitions", - "batch:DescribeJobQueues", - "batch:DescribeJobs", - "batch:DescribeSchedulingPolicies", - - "batch:GetJobQueueSnapshot", - - "batch:ListJobs", - "batch:ListSchedulingPolicies", - "batch:ListTagsForResource", - ], - Resource: "*", - }, - - ReadDrsInventory: { - Action: [ - "drs:DescribeRecoverySnapshots", - "drs:DescribeSourceServers", - "drs:ListTagsForResource", - ], - Resource: "*", - }, +export const policy: Record = { + ReadBackupInventory: [ + { + Action: [ + // Vaults + "backup:ListBackupVaults", + "backup:DescribeBackupVault", + + // Recovery points + "backup:ListRecoveryPointsByResource", + "backup:DescribeRecoveryPoint", + "backup:ListRecoveryPointsByBackupVault", + "backup:GetRecoveryPointRestoreMetadata", + + // Common for all resources + "backup:ListTags", + + // Misc. This won't be used at the time of this writing, but + // may come in handy in the future? + "backup:ListProtectedResources", + "backup:ListProtectedResourcesByBackupVault", + ], + Resource: "*", + }, + ], + + ReadEfsInventory: [ + { + Action: [ + "elasticfilesystem:DescribeFileSystems", + "elasticfilesystem:ListTagsForResource", + "elasticfilesystem:DescribeTags", + ], + Resource: "*", + }, + ], + + ReadFsxInventory: [ + { + Action: [ + "fsx:DescribeVolumes", + "fsx:DescribeBackups", + "fsx:DescribeFileSystems", + "fsx:DescribeStorageVirtualMachines", + "fsx:ListTagsForResource", + ], + Resource: "*", + }, + ], + + ReadEbsInventory: [ + { + Action: [ + // Volumes + "ec2:DescribeVolumeStatus", + "ec2:DescribeVolumes", + + // Snapshots + "ec2:DescribeSnapshots", + "ec2:DescribeSnapshotAttribute", + + // Common for all resources + "ec2:DescribeTags", + + // Used for cost estimation + "ebs:ListSnapshotBlocks", + "ebs:ListChangedBlocks", + ], + Resource: "*", + }, + ], + + ReadEc2Inventory: [ + { + Action: [ + "ec2:DescribeInstances", + "ec2:DescribeImages", + "ec2:DescribeHosts", + "ssm:DescribeInstanceInformation", + ], + Resource: "*", + }, + ], + + ReadVpcInventory: [ + { + Action: [ + // Used for network config troubleshooting + "ec2:DescribeAvailabilityZones", + "ec2:DescribeSecurityGroups", + "ec2:DescribeSubnets", + "ec2:DescribeVpcs", + "ec2:DescribeRouteTables", + "ec2:DescribeNatGateways", + "ec2:DescribeVpcEndpoints", + ], + Resource: "*", + }, + ], + + ReadS3Inventory: [ + { + Action: [ + "s3:ListAllMyBuckets", + "s3:GetBucketLocation", + "s3:GetBucketTagging", + "s3:GetBucketObjectLockConfiguration", + "s3:GetBucketAcl", + "s3:GetBucketVersioning", + "s3:GetBucketPolicy", + "s3:GetBucketLogging", + "s3:ListBucket", + ], + Resource: "*", + }, + ], + + ReadAccountsInventory: [ + { + Action: ["iam:ListAccountAliases", "ec2:DescribeRegions"], + Resource: "*", + }, + ], + + ReadKmsInventory: [ + { + Action: ["kms:DescribeKey"], + Resource: "*", + }, + ], + + ReadCloudformationInventory: [ + { + Action: [ + // Required for discovering Elastio CFN stack version by blue stack + // Also used by used by the `accs` service to validate the status + // of the asset account CFN stack in cross-account scenario (this policy + // is reused when defining permissions for the connector role in asset account) + "cloudformation:DescribeStacks", + "cloudformation:DescribeStackSet", + "cloudformation:ListStacks", + ], + Resource: "*", + }, + ], + + ReadBatchInventory: [ + { + Action: [ + // Just for debugging + "batch:DescribeComputeEnvironments", + "batch:DescribeJobDefinitions", + "batch:DescribeJobQueues", + "batch:DescribeJobs", + "batch:DescribeSchedulingPolicies", + + "batch:GetJobQueueSnapshot", + + "batch:ListJobs", + "batch:ListSchedulingPolicies", + "batch:ListTagsForResource", + ], + Resource: "*", + }, + ], + + ReadDrsInventory: [ + { + Action: [ + "drs:DescribeRecoverySnapshots", + "drs:DescribeSourceServers", + "drs:ListTagsForResource", + ], + Resource: "*", + }, + ], }; /** @@ -169,6 +193,6 @@ type InventoryIamAction = Extract< `${string}:${InventoryIamActionPrefix}${string}` >; -interface InventoryIamPolicyStatement extends iam.PolicyStatement { +type InventoryIamPolicyStatement = Omit & { Action: InventoryIamAction | InventoryIamAction[]; -} +}; From d081311c155caedcc7110cfc1e6151605b15492b Mon Sep 17 00:00:00 2001 From: Veetaha Date: Fri, 11 Apr 2025 13:19:02 +0000 Subject: [PATCH 3/5] Start adding CFN --- codegen/package-lock.json | 14 ++- codegen/package.json | 3 +- codegen/src/asset-account/index.ts | 43 ++++--- .../src/asset-account/mir/cloud-connector.ts | 14 +-- codegen/src/asset-account/mir/index.ts | 8 +- .../orchestrator/cloudformation.ts | 107 ++++++++++++++++++ .../asset-account/orchestrator/terraform.ts | 42 +++++-- .../asset-account/{inputs.ts => params.ts} | 79 +++++++------ codegen/src/common/inventory.ts | 81 +------------ codegen/src/main.ts | 90 ++------------- codegen/src/tf-modules-docs.ts | 79 +++++++++++++ 11 files changed, 331 insertions(+), 229 deletions(-) rename codegen/src/asset-account/{inputs.ts => params.ts} (86%) create mode 100644 codegen/src/tf-modules-docs.ts diff --git a/codegen/package-lock.json b/codegen/package-lock.json index 2d7d7d1..064f001 100644 --- a/codegen/package-lock.json +++ b/codegen/package-lock.json @@ -13,7 +13,8 @@ "constructs": "^10.4.2", "fs-extra": "^11.3.0", "lodash": "^4.17.21", - "prettier": "^3.5.3" + "prettier": "^3.5.3", + "yaml": "^2.7.1" }, "devDependencies": { "@types/lodash": "^4.17.16", @@ -1042,6 +1043,17 @@ "engines": { "node": ">= 10.0.0" } + }, + "node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } } } } diff --git a/codegen/package.json b/codegen/package.json index 617546c..1a0ee02 100644 --- a/codegen/package.json +++ b/codegen/package.json @@ -17,6 +17,7 @@ "constructs": "^10.4.2", "fs-extra": "^11.3.0", "lodash": "^4.17.21", - "prettier": "^3.5.3" + "prettier": "^3.5.3", + "yaml": "^2.7.1" } } diff --git a/codegen/src/asset-account/index.ts b/codegen/src/asset-account/index.ts index bfa5ec8..5717c7c 100644 --- a/codegen/src/asset-account/index.ts +++ b/codegen/src/asset-account/index.ts @@ -1,23 +1,38 @@ import { ArkErrors } from "arktype"; -import { inputs, InputsIn } from "./inputs"; +import { CloudFormationParams, TerraformParams } from "./params"; import * as mir from "./mir"; -import * as tf from "./orchestrator/terraform"; +import * as terraform from "./orchestrator/terraform"; +import * as cloudformation from "./orchestrator/cloudformation"; -export { inputs }; +export type CloudFormationParams = typeof CloudFormationParams.inferIn; +export type TerraformParams = typeof TerraformParams.inferIn; -export function generate(inputsIn: InputsIn) { - const result = inputs(inputsIn); +export function generateCloudFormation(paramsIn: CloudFormationParams) { + const params = CloudFormationParams(paramsIn); + return generate(params, cloudformation.generate); +} + +export function generateTerraform(paramsIn: TerraformParams) { + const params = TerraformParams(paramsIn); + return generate(params, terraform.generate); +} - if (result instanceof ArkErrors) { - throw new Error(`Invalid inputs: ${result}`); +type Generator

= ( + resources: Record, + params: P, +) => R; + +function generate

>( + params: P | ArkErrors, + generator: F, +): R { + if (params instanceof ArkErrors) { + throw new Error(`Invalid params: ${params}`); } - const resources = mir.resources(result); + const resources = mir.resources(params); - tf.generate(resources); -} + params.tags["elastio:resource"] = "true"; -generate({ - connectorAccountId: "123456789012", - connectorRoleExternalId: "external-id", -}); + return generator(resources, params); +} diff --git a/codegen/src/asset-account/mir/cloud-connector.ts b/codegen/src/asset-account/mir/cloud-connector.ts index 2d17b3a..1cad402 100644 --- a/codegen/src/asset-account/mir/cloud-connector.ts +++ b/codegen/src/asset-account/mir/cloud-connector.ts @@ -1,10 +1,10 @@ import * as iam from "../../common/iam"; import * as inventory from "../../common/inventory"; -import { Inputs } from "../inputs"; +import type { Params } from "."; import _ from "lodash"; import { IamRole } from "./resource"; -export function cloudConnectorRole(inputs: Inputs): IamRole { +export function cloudConnectorRole(params: Params): IamRole { const otherStatements: Record = { WriteEc2: [ // Create and copy snapshots to the cloud connector account @@ -41,7 +41,7 @@ export function cloudConnectorRole(inputs: Inputs): IamRole { Condition: { // Needed to add createVolumePermission for the connector account. StringEquals: { - "ec2:Add/userId": inputs.connectorAccountId, + "ec2:Add/userId": params.connectorAccountId, // Even though EC2 IAM reference says there are two more // things that could be used in the condition: // "ec2:Attribute" and "ec2:Attribute/${AttributeName}", @@ -239,15 +239,15 @@ export function cloudConnectorRole(inputs: Inputs): IamRole { Principal: { AWS: - `arn:aws:iam::${inputs.connectorAccountId}:role/` + - inputs.iamResourceNamesPrefix + + `arn:aws:iam::${params.connectorAccountId}:role/` + + params.iamResourceNamesPrefix + "ElastioCloudConnectorBastion" + - inputs.iamResourceNamesSuffix, + params.iamResourceNamesSuffix, }, Condition: { StringEquals: { - "sts:ExternalId": inputs.connectorRoleExternalId, + "sts:ExternalId": params.connectorRoleExternalId, }, }, }, diff --git a/codegen/src/asset-account/mir/index.ts b/codegen/src/asset-account/mir/index.ts index 3ed26fb..fcc755d 100644 --- a/codegen/src/asset-account/mir/index.ts +++ b/codegen/src/asset-account/mir/index.ts @@ -1,12 +1,16 @@ import type { Resource } from "./resource"; -import { Inputs } from "../inputs"; import { cloudConnectorRole } from "./cloud-connector"; +import { CloudFormationParams, TerraformParams } from "../params"; export { Resource }; +export type Params = + | typeof CloudFormationParams.inferOut + | typeof TerraformParams.inferOut; + const version = "0.35.13"; -export function resources(inputs: Inputs): Record { +export function resources(inputs: Params): Record { return { inventory_event_target: { type: "aws_iam_role", diff --git a/codegen/src/asset-account/orchestrator/cloudformation.ts b/codegen/src/asset-account/orchestrator/cloudformation.ts index e69de29..9fd41ff 100644 --- a/codegen/src/asset-account/orchestrator/cloudformation.ts +++ b/codegen/src/asset-account/orchestrator/cloudformation.ts @@ -0,0 +1,107 @@ +import _ from "lodash"; +import * as hclTools from "@cdktf/hcl-tools"; +import * as iam from "../../common/iam"; +import * as prettier from "prettier"; +import { CloudFormationParams } from "../params"; +import { Resource } from "../mir"; + +async function literal(value: unknown): Promise { + const json = JSON.stringify(value, null, 2); + + const pretty = await prettier.format(json, { parser: "json" }); + return pretty.trim(); +} + +async function jsonencode(value: unknown): Promise { + return `jsonencode(\n${await literal(value)}\n)`; +} + +function policyDocument(statements: iam.PolicyStatement[]) { + return { + Version: "2012-10-17", + Statement: statements.map((statement) => ({ + Effect: statement.Effect ?? "Allow", + ...statement, + })), + }; +} + +export type CloudFormationParams = typeof CloudFormationParams.inferOut; + +export interface CloudFormationProject { + files: Record; +} + +export async function generate( + resources: Record, + params: CloudFormationParams, +): Promise { + const parts = [ + `locals { + tags = ${await literal(params.tags)} + }`, + ]; + + for (const [id, resource] of Object.entries(resources)) { + switch (resource.type) { + case "aws_iam_role": { + parts.push( + `resource "aws_iam_role" ${literal(id)} { + name = ${literal(resource.name)} + tags = local.tags + assume_role_policy = ${await jsonencode(policyDocument([resource.assumeRolePolicy]))} + }`, + ); + + const statements = Object.entries(resource.statements); + + if (statements.length === 0) { + continue; + } + + const policies = _.mapValues( + resource.statements, + (statement) => policyDocument(statement).Statement, + ); + + parts.push( + `resource "aws_iam_role_policy" ${literal(id)} { + role = aws_iam_role.${id}.name + name = each.key + policy = jsonencode( + { + "Version": "2012-10-17", + "Statement": each.value + } + ) + for_each = ${await literal(policies)} + }`, + ); + } + case "aws_ssm_parameter": { + } + } + } + + parts.push(` + data "aws_caller_identity" "current" {} + locals { + account_id = data.aws_caller_identity.current.account_id + } + `); + + const content = parts + .join("\n\n") + .replaceAll("{{account_id}}", "${local.account_id}") + .replaceAll(/\{\{(.*)\}\}/g, "${$1}"); + + const formatted = (await hclTools.format(content)).trim(); + + console.log(formatted); + + return { + files: { + "main.tf": formatted, + }, + }; +} diff --git a/codegen/src/asset-account/orchestrator/terraform.ts b/codegen/src/asset-account/orchestrator/terraform.ts index 3457731..9a75157 100644 --- a/codegen/src/asset-account/orchestrator/terraform.ts +++ b/codegen/src/asset-account/orchestrator/terraform.ts @@ -1,11 +1,13 @@ -import type { Resource } from "../mir"; import _ from "lodash"; import * as hclTools from "@cdktf/hcl-tools"; import * as iam from "../../common/iam"; import * as prettier from "prettier"; +import { TerraformParams } from "../params"; +import { Resource } from "../mir"; async function literal(value: unknown): Promise { const json = JSON.stringify(value, null, 2); + const pretty = await prettier.format(json, { parser: "json" }); return pretty.trim(); } @@ -18,21 +20,35 @@ function policyDocument(statements: iam.PolicyStatement[]) { return { Version: "2012-10-17", Statement: statements.map((statement) => ({ - ...statement, Effect: statement.Effect ?? "Allow", + ...statement, })), }; } -export async function generate(resources: Record) { - const parts = []; +export type TerraformParams = typeof TerraformParams.inferOut; + +export interface TerraformProject { + files: Record; +} + +export async function generate( + resources: Record, + params: TerraformParams, +): Promise { + const parts = [ + `locals { + tags = ${await literal(params.tags)} + }`, + ]; for (const [id, resource] of Object.entries(resources)) { switch (resource.type) { case "aws_iam_role": { parts.push( - `resource "aws_iam_role" "${id}" { - name = "${resource.name}" + `resource "aws_iam_role" ${literal(id)} { + name = ${literal(resource.name)} + tags = local.tags assume_role_policy = ${await jsonencode(policyDocument([resource.assumeRolePolicy]))} }`, ); @@ -49,7 +65,7 @@ export async function generate(resources: Record) { ); parts.push( - `resource "aws_iam_role_policy" "${id}" { + `resource "aws_iam_role_policy" ${literal(id)} { role = aws_iam_role.${id}.name name = each.key policy = jsonencode( @@ -79,9 +95,13 @@ export async function generate(resources: Record) { .replaceAll("{{account_id}}", "${local.account_id}") .replaceAll(/\{\{(.*)\}\}/g, "${$1}"); - console.log(await hclTools.format(content)); -} + const formatted = (await hclTools.format(content)).trim(); + + console.log(formatted); -function camelCaseToSnakeCase(str: string): string { - return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase(); + return { + files: { + "main.tf": formatted, + }, + }; } diff --git a/codegen/src/asset-account/inputs.ts b/codegen/src/asset-account/params.ts similarity index 86% rename from codegen/src/asset-account/inputs.ts rename to codegen/src/asset-account/params.ts index 3108ced..04adf93 100644 --- a/codegen/src/asset-account/inputs.ts +++ b/codegen/src/asset-account/params.ts @@ -1,12 +1,9 @@ import { type } from "arktype"; import { collapse } from "../common/string"; -export type Inputs = typeof inputs.inferOut; -export type InputsIn = typeof inputs.inferIn; - const IamPolicyArn = type(/^arn:aws:iam::[^:]*:policy\/.*/); -export const inputs = type({ +const CommonParams = type({ connectorAccountId: type(/^\d{12}$/).configure({ title: "Cloud connector AWS account ID", description: collapse` @@ -26,36 +23,14 @@ export const inputs = type({ actual: "", }), - disableDeploymentNotification: type("boolean") + tags: type({ "[string]": "string" }) .configure({ - title: "Deployment Notification", + title: "Tags", description: collapse` - Send a deployment notification to the Elastio Connector - account. This is required for the connector to be able to - discover the assets in this account. + Tags to add to all resources deployed by this stack. `, }) - .default(false), - - deploymentNotificationToken: type("string") - .configure({ - title: "Deployment notification token", - description: collapse` - Token sent to the SNS topic to authenticate the deployment notification. - `, - actual: "", - }) - .optional(), - - deploymentNotificationSnsTopicArn: type("string") - .configure({ - title: "Deployment notification SNS topic ARN", - description: collapse` - ARN of the Elastio tenant SNS topic where to publish a notification about a - completed stack deployment. - `, - }) - .optional(), + .default(() => ({})), iamResourceNamesPrefix: type("string") .configure({ @@ -82,8 +57,7 @@ export const inputs = type({ .narrow((list, ctx) => { const unique = new Set(list); return ( - unique.size === list.length || - ctx.reject("list contains duplicate items") + unique.size === list.length || ctx.mustBe("a list without diplicates") ); }) .default(() => []), @@ -95,6 +69,41 @@ export const inputs = type({ permission boundary for all Elastio IAM roles `, }).optional(), +}); + +export const CloudformationParams = CommonParams.merge({ + orchestrator: "'cloudformation'", + + disableDeploymentNotification: type("boolean") + .configure({ + title: "Deployment Notification", + description: collapse` + Send a deployment notification to the Elastio Connector + account. This is required for the connector to be able to + discover the assets in this account. + `, + }) + .default(false), + + deploymentNotificationToken: type("string") + .configure({ + title: "Deployment notification token", + description: collapse` + Token sent to the SNS topic to authenticate the deployment notification. + `, + actual: "", + }) + .optional(), + + deploymentNotificationSnsTopicArn: type("string") + .configure({ + title: "Deployment notification SNS topic ARN", + description: collapse` + ARN of the Elastio tenant SNS topic where to publish a notification about a + completed stack deployment. + `, + }) + .optional(), encryptWithCmk: type("boolean") .configure({ @@ -119,4 +128,8 @@ export const inputs = type({ "This increases the cost of the stack. Enable only if needed", }) .default(false), -}); +}).describe("Params specific to CloudFormation orchestrator"); + +export const TerraformParams = CommonParams.describe( + "Params specific to Terraform orchestrator", +); diff --git a/codegen/src/common/inventory.ts b/codegen/src/common/inventory.ts index b3a9ab4..5551984 100644 --- a/codegen/src/common/inventory.ts +++ b/codegen/src/common/inventory.ts @@ -5,7 +5,7 @@ import * as iam from "./iam"; * ElastioTenant (Connector Account) and CloudConnector (Asset Account) roles. */ export const policy: Record = { - ReadBackupInventory: [ + ReadInventory: [ { Action: [ // Vaults @@ -25,38 +25,17 @@ export const policy: Record = { // may come in handy in the future? "backup:ListProtectedResources", "backup:ListProtectedResourcesByBackupVault", - ], - Resource: "*", - }, - ], - ReadEfsInventory: [ - { - Action: [ "elasticfilesystem:DescribeFileSystems", "elasticfilesystem:ListTagsForResource", "elasticfilesystem:DescribeTags", - ], - Resource: "*", - }, - ], - ReadFsxInventory: [ - { - Action: [ "fsx:DescribeVolumes", "fsx:DescribeBackups", "fsx:DescribeFileSystems", "fsx:DescribeStorageVirtualMachines", "fsx:ListTagsForResource", - ], - Resource: "*", - }, - ], - ReadEbsInventory: [ - { - Action: [ // Volumes "ec2:DescribeVolumeStatus", "ec2:DescribeVolumes", @@ -71,26 +50,12 @@ export const policy: Record = { // Used for cost estimation "ebs:ListSnapshotBlocks", "ebs:ListChangedBlocks", - ], - Resource: "*", - }, - ], - ReadEc2Inventory: [ - { - Action: [ "ec2:DescribeInstances", "ec2:DescribeImages", "ec2:DescribeHosts", "ssm:DescribeInstanceInformation", - ], - Resource: "*", - }, - ], - ReadVpcInventory: [ - { - Action: [ // Used for network config troubleshooting "ec2:DescribeAvailabilityZones", "ec2:DescribeSecurityGroups", @@ -99,14 +64,7 @@ export const policy: Record = { "ec2:DescribeRouteTables", "ec2:DescribeNatGateways", "ec2:DescribeVpcEndpoints", - ], - Resource: "*", - }, - ], - ReadS3Inventory: [ - { - Action: [ "s3:ListAllMyBuckets", "s3:GetBucketLocation", "s3:GetBucketTagging", @@ -116,28 +74,11 @@ export const policy: Record = { "s3:GetBucketPolicy", "s3:GetBucketLogging", "s3:ListBucket", - ], - Resource: "*", - }, - ], - ReadAccountsInventory: [ - { - Action: ["iam:ListAccountAliases", "ec2:DescribeRegions"], - Resource: "*", - }, - ], + "iam:ListAccountAliases", + "ec2:DescribeRegions", + "kms:DescribeKey", - ReadKmsInventory: [ - { - Action: ["kms:DescribeKey"], - Resource: "*", - }, - ], - - ReadCloudformationInventory: [ - { - Action: [ // Required for discovering Elastio CFN stack version by blue stack // Also used by used by the `accs` service to validate the status // of the asset account CFN stack in cross-account scenario (this policy @@ -145,14 +86,7 @@ export const policy: Record = { "cloudformation:DescribeStacks", "cloudformation:DescribeStackSet", "cloudformation:ListStacks", - ], - Resource: "*", - }, - ], - ReadBatchInventory: [ - { - Action: [ // Just for debugging "batch:DescribeComputeEnvironments", "batch:DescribeJobDefinitions", @@ -165,14 +99,7 @@ export const policy: Record = { "batch:ListJobs", "batch:ListSchedulingPolicies", "batch:ListTagsForResource", - ], - Resource: "*", - }, - ], - ReadDrsInventory: [ - { - Action: [ "drs:DescribeRecoverySnapshots", "drs:DescribeSourceServers", "drs:ListTagsForResource", diff --git a/codegen/src/main.ts b/codegen/src/main.ts index 9041e66..00bb2ff 100644 --- a/codegen/src/main.ts +++ b/codegen/src/main.ts @@ -1,89 +1,13 @@ -/** - * This script generates IAM policies from TypeScript files in the `policies` directory. - * We use TypeScript for this because it's easier to maintain shared code for policies - * this way and we also get nice compile-time checks from TypeScript for the IAM actions - * used in the policies. This codegen logic may also be used to support other kinds of - * orchestration tools like CloudFormation, CDK, etc. - */ - -import * as fs from "node:fs/promises"; -import * as path from "node:path"; -import { fileURLToPath } from "node:url"; -import { Policy } from "./common/iam"; - -const scriptPath = fileURLToPath(import.meta.url); -const scriptDir = path.dirname(scriptPath); -const iamPoliciesTfModulePath = path.join( - path.join(scriptDir, "../../iam-policies/terraform"), -); - -async function writePolicy(policyName: string, policy: Policy) { - const policyDocument = { - Version: "2012-10-17", - Statement: policy.statements.map((statement) => ({ - ...statement, - Effect: statement.Effect ?? "Allow", - })), - }; - - const policyDefinition = { - Description: policy.description, - PolicyDocument: policyDocument, - }; - - const policyDocumentJson = JSON.stringify(policyDefinition, null, 2); - - const policyOutputPath = path.join( - iamPoliciesTfModulePath, - "policies", - `${policyName}.json`, - ); - - await fs.writeFile(policyOutputPath, policyDocumentJson); -} +import * as tfModulesDocs from './tf-modules-docs'; +import * as assetAccount from './asset-account'; async function main() { - const policiesDir = path.join(scriptDir, "policies"); - const policyFiles = await fs.readdir(policiesDir); - const policyNames = policyFiles.map((file) => path.basename(file, ".ts")); - - const policies = await Promise.all( - policyNames.map(async (policyName) => { - const policyPath = path.join(policiesDir, `${policyName}.ts`); - const module: { default: Policy } = await import(policyPath); - const policy = module.default; - - await writePolicy(policyName, policy); - - return [policyName, policy] as const; - }), - ); - - const policiesMdTable = policies - .map(([policyName, policy]) => { - const name = `[\`${policyName}\`][${policyName}]`; - return `| ${name} | ${policy.description} |`; - }) - .join("\n"); - - const links = policies - .map( - ([policyName]) => - `[${policyName}]: ../../codegen/src/policies/${policyName}.ts`, - ) - .join("\n"); - - const readmePath = path.join(iamPoliciesTfModulePath, "README.md"); - const readme = await fs.readFile(readmePath, "utf-8"); - - const docs = `| Policy | Description |\n| --- | --- |\n${policiesMdTable}\n\n${links}`; - - const policiesDocs = readme.replace( - /(.*)/s, - `\n${docs}\n`, - ); + await tfModulesDocs.generate(); - await fs.writeFile(readmePath, policiesDocs); + await assetAccount.generate({ + connectorAccountId: "123456789012", + connectorRoleExternalId: "external-id", + }); } main(); diff --git a/codegen/src/tf-modules-docs.ts b/codegen/src/tf-modules-docs.ts new file mode 100644 index 0000000..a714c58 --- /dev/null +++ b/codegen/src/tf-modules-docs.ts @@ -0,0 +1,79 @@ +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import { Policy } from "./common/iam"; + +const scriptPath = fileURLToPath(import.meta.url); +const scriptDir = path.dirname(scriptPath); +const iamPoliciesTfModulePath = path.join( + path.join(scriptDir, "../../iam-policies/terraform"), +); + +async function writePolicy(policyName: string, policy: Policy) { + const policyDocument = { + Version: "2012-10-17", + Statement: policy.statements.map((statement) => ({ + ...statement, + Effect: statement.Effect ?? "Allow", + })), + }; + + const policyDefinition = { + Description: policy.description, + PolicyDocument: policyDocument, + }; + + const policyDocumentJson = JSON.stringify(policyDefinition, null, 2); + + const policyOutputPath = path.join( + iamPoliciesTfModulePath, + "policies", + `${policyName}.json`, + ); + + await fs.writeFile(policyOutputPath, policyDocumentJson); +} + +async function generate() { + const policiesDir = path.join(scriptDir, "policies"); + const policyFiles = await fs.readdir(policiesDir); + const policyNames = policyFiles.map((file) => path.basename(file, ".ts")); + + const policies = await Promise.all( + policyNames.map(async (policyName) => { + const policyPath = path.join(policiesDir, `${policyName}.ts`); + const module: { default: Policy } = await import(policyPath); + const policy = module.default; + + await writePolicy(policyName, policy); + + return [policyName, policy] as const; + }), + ); + + const policiesMdTable = policies + .map(([policyName, policy]) => { + const name = `[\`${policyName}\`][${policyName}]`; + return `| ${name} | ${policy.description} |`; + }) + .join("\n"); + + const links = policies + .map( + ([policyName]) => + `[${policyName}]: ../../codegen/src/policies/${policyName}.ts`, + ) + .join("\n"); + + const readmePath = path.join(iamPoliciesTfModulePath, "README.md"); + const readme = await fs.readFile(readmePath, "utf-8"); + + const docs = `| Policy | Description |\n| --- | --- |\n${policiesMdTable}\n\n${links}`; + + const policiesDocs = readme.replace( + /(.*)/s, + `\n${docs}\n`, + ); + + await fs.writeFile(readmePath, policiesDocs); +} From bd6b8965947324b4007c89a808a6a78ebd9566d8 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Fri, 11 Apr 2025 14:22:02 +0000 Subject: [PATCH 4/5] Fix typos --- codegen/src/asset-account/mir/index.ts | 2 +- codegen/src/asset-account/params.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codegen/src/asset-account/mir/index.ts b/codegen/src/asset-account/mir/index.ts index fcc755d..a294789 100644 --- a/codegen/src/asset-account/mir/index.ts +++ b/codegen/src/asset-account/mir/index.ts @@ -39,7 +39,7 @@ export function resources(inputs: Params): Record { asset_region_stack_deployer: { type: "aws_iam_role", name: "ElastioAssetRegionStackDeployer", - description: "Used by Cloudformation to deploy region-level stack", + description: "Used by CloudFormation to deploy region-level stack", assumeRolePolicy: { Action: "sts:AssumeRole", Principal: { diff --git a/codegen/src/asset-account/params.ts b/codegen/src/asset-account/params.ts index 04adf93..cb9a8d8 100644 --- a/codegen/src/asset-account/params.ts +++ b/codegen/src/asset-account/params.ts @@ -71,7 +71,7 @@ const CommonParams = type({ }).optional(), }); -export const CloudformationParams = CommonParams.merge({ +export const CloudFormationParams = CommonParams.merge({ orchestrator: "'cloudformation'", disableDeploymentNotification: type("boolean") From ba5c42c44f8f6aa89504481ba45acffa79693824 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Fri, 11 Apr 2025 14:40:27 +0000 Subject: [PATCH 5/5] Remove aws-cdk-lib --- codegen/package-lock.json | 395 -------------------------------------- codegen/package.json | 1 - 2 files changed, 396 deletions(-) diff --git a/codegen/package-lock.json b/codegen/package-lock.json index 064f001..a306280 100644 --- a/codegen/package-lock.json +++ b/codegen/package-lock.json @@ -8,7 +8,6 @@ "dependencies": { "@cdktf/hcl-tools": "^0.20.11", "arktype": "^2.1.19", - "aws-cdk-lib": "^2.189.0", "aws-iam-policy-types": "^1.0.2", "constructs": "^10.4.2", "fs-extra": "^11.3.0", @@ -36,51 +35,6 @@ "resolved": "https://registry.npmjs.org/@ark/util/-/util-0.45.9.tgz", "integrity": "sha512-0WYNAb8aRGp7dNt6xIvIrRzL7V1XL3u3PK2vcklhtTrdaP235DjC9qJhzidrxtWr68mA5ySSjUrgrXk622bKkw==" }, - "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.231", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.231.tgz", - "integrity": "sha512-vPqD/K2pK/ALhU5r5Nafdc2nLB+LJKxNyxUmQnLsazU6AWDJfkqjHQx8m3J4Cjl2C3chQkIRMdzSDuXIlo43GA==" - }, - "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", - "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==" - }, - "node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "41.2.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-41.2.0.tgz", - "integrity": "sha512-JaulVS6z9y5+u4jNmoWbHZRs9uGOnmn/ktXygNWKNu1k6lF3ad4so3s18eRu15XCbUIomxN9WPYT6Ehh7hzONw==", - "bundleDependencies": [ - "jsonschema", - "semver" - ], - "dependencies": { - "jsonschema": "~1.4.1", - "semver": "^7.7.1" - }, - "engines": { - "node": ">= 14.15.0" - } - }, - "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { - "version": "1.4.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { - "version": "7.7.1", - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@cdktf/hcl-tools": { "version": "0.20.11", "resolved": "https://registry.npmjs.org/@cdktf/hcl-tools/-/hcl-tools-0.20.11.tgz", @@ -510,355 +464,6 @@ "@ark/util": "0.45.9" } }, - "node_modules/aws-cdk-lib": { - "version": "2.189.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.189.0.tgz", - "integrity": "sha512-B5Uha7uRntOAyuKfU0eFtxij3ZVTzGAbetw5qaXlURa68wsWpKlU72/OyKugB6JYkhjCZkSTVVBxd1pVTosxEw==", - "bundleDependencies": [ - "@balena/dockerignore", - "case", - "fs-extra", - "ignore", - "jsonschema", - "minimatch", - "punycode", - "semver", - "table", - "yaml", - "mime-types" - ], - "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.229", - "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", - "@aws-cdk/cloud-assembly-schema": "^41.0.0", - "@balena/dockerignore": "^1.0.2", - "case": "1.6.3", - "fs-extra": "^11.3.0", - "ignore": "^5.3.2", - "jsonschema": "^1.5.0", - "mime-types": "^2.1.35", - "minimatch": "^3.1.2", - "punycode": "^2.3.1", - "semver": "^7.7.1", - "table": "^6.9.0", - "yaml": "1.10.2" - }, - "engines": { - "node": ">= 14.15.0" - }, - "peerDependencies": { - "constructs": "^10.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { - "version": "1.0.2", - "inBundle": true, - "license": "Apache-2.0" - }, - "node_modules/aws-cdk-lib/node_modules/ajv": { - "version": "8.17.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/aws-cdk-lib/node_modules/ansi-regex": { - "version": "5.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/ansi-styles": { - "version": "4.3.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/aws-cdk-lib/node_modules/astral-regex": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/balanced-match": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/brace-expansion": { - "version": "1.1.11", - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/aws-cdk-lib/node_modules/case": { - "version": "1.6.3", - "inBundle": true, - "license": "(MIT OR GPL-3.0-or-later)", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/color-convert": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/color-name": { - "version": "1.1.4", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/concat-map": { - "version": "0.0.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/emoji-regex": { - "version": "8.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { - "version": "3.1.3", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/fast-uri": { - "version": "3.0.6", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "inBundle": true, - "license": "BSD-3-Clause" - }, - "node_modules/aws-cdk-lib/node_modules/fs-extra": { - "version": "11.3.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/aws-cdk-lib/node_modules/graceful-fs": { - "version": "4.2.11", - "inBundle": true, - "license": "ISC" - }, - "node_modules/aws-cdk-lib/node_modules/ignore": { - "version": "5.3.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/jsonfile": { - "version": "6.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/aws-cdk-lib/node_modules/jsonschema": { - "version": "1.5.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { - "version": "4.4.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/mime-db": { - "version": "1.52.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/aws-cdk-lib/node_modules/mime-types": { - "version": "2.1.35", - "inBundle": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/aws-cdk-lib/node_modules/minimatch": { - "version": "3.1.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/aws-cdk-lib/node_modules/punycode": { - "version": "2.3.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/aws-cdk-lib/node_modules/require-from-string": { - "version": "2.0.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.7.1", - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/aws-cdk-lib/node_modules/slice-ansi": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/aws-cdk-lib/node_modules/string-width": { - "version": "4.2.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/strip-ansi": { - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/table": { - "version": "6.9.0", - "inBundle": true, - "license": "BSD-3-Clause", - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/universalify": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/yaml": { - "version": "1.10.2", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, "node_modules/aws-iam-policy-types": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/aws-iam-policy-types/-/aws-iam-policy-types-1.0.2.tgz", diff --git a/codegen/package.json b/codegen/package.json index 1a0ee02..4110cbf 100644 --- a/codegen/package.json +++ b/codegen/package.json @@ -12,7 +12,6 @@ "dependencies": { "@cdktf/hcl-tools": "^0.20.11", "arktype": "^2.1.19", - "aws-cdk-lib": "^2.189.0", "aws-iam-policy-types": "^1.0.2", "constructs": "^10.4.2", "fs-extra": "^11.3.0",