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/.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 53f9a46..a306280 100644
--- a/codegen/package-lock.json
+++ b/codegen/package-lock.json
@@ -6,14 +6,40 @@
"packages": {
"": {
"dependencies": {
- "aws-iam-policy-types": "^1.0.2"
+ "@cdktf/hcl-tools": "^0.20.11",
+ "arktype": "^2.1.19",
+ "aws-iam-policy-types": "^1.0.2",
+ "constructs": "^10.4.2",
+ "fs-extra": "^11.3.0",
+ "lodash": "^4.17.21",
+ "prettier": "^3.5.3",
+ "yaml": "^2.7.1"
},
"devDependencies": {
- "@types/node": "^22.13.13",
+ "@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/@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",
@@ -414,13 +440,28 @@
"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.13",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz",
- "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==",
+ "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-iam-policy-types": {
@@ -434,6 +475,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",
@@ -474,6 +520,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",
@@ -500,6 +559,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",
@@ -529,9 +623,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",
@@ -542,10 +636,29 @@
}
},
"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"
+ }
+ },
+ "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 dd6500f..4110cbf 100644
--- a/codegen/package.json
+++ b/codegen/package.json
@@ -4,11 +4,19 @@
"start": "tsc --noEmit && tsx ./src/main.ts"
},
"devDependencies": {
- "@types/node": "^22.13.13",
+ "@types/lodash": "^4.17.16",
+ "@types/node": "^22.14.0",
"tsx": "^4.19.3",
- "typescript": "^5.8.2"
+ "typescript": "^5.8.3"
},
"dependencies": {
- "aws-iam-policy-types": "^1.0.2"
+ "@cdktf/hcl-tools": "^0.20.11",
+ "arktype": "^2.1.19",
+ "aws-iam-policy-types": "^1.0.2",
+ "constructs": "^10.4.2",
+ "fs-extra": "^11.3.0",
+ "lodash": "^4.17.21",
+ "prettier": "^3.5.3",
+ "yaml": "^2.7.1"
}
}
diff --git a/codegen/src/asset-account/index.ts b/codegen/src/asset-account/index.ts
new file mode 100644
index 0000000..5717c7c
--- /dev/null
+++ b/codegen/src/asset-account/index.ts
@@ -0,0 +1,38 @@
+import { ArkErrors } from "arktype";
+import { CloudFormationParams, TerraformParams } from "./params";
+import * as mir from "./mir";
+import * as terraform from "./orchestrator/terraform";
+import * as cloudformation from "./orchestrator/cloudformation";
+
+export type CloudFormationParams = typeof CloudFormationParams.inferIn;
+export type TerraformParams = typeof TerraformParams.inferIn;
+
+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);
+}
+
+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(params);
+
+ params.tags["elastio:resource"] = "true";
+
+ return generator(resources, params);
+}
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..1cad402
--- /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 type { Params } from ".";
+import _ from "lodash";
+import { IamRole } from "./resource";
+
+export function cloudConnectorRole(params: Params): 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": 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}",
+ // 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::${params.connectorAccountId}:role/` +
+ params.iamResourceNamesPrefix +
+ "ElastioCloudConnectorBastion" +
+ params.iamResourceNamesSuffix,
+ },
+
+ Condition: {
+ StringEquals: {
+ "sts:ExternalId": params.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..a294789
--- /dev/null
+++ b/codegen/src/asset-account/mir/index.ts
@@ -0,0 +1,87 @@
+import type { Resource } from "./resource";
+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: Params): 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..9fd41ff
--- /dev/null
+++ 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
new file mode 100644
index 0000000..9a75157
--- /dev/null
+++ b/codegen/src/asset-account/orchestrator/terraform.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 { 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();
+}
+
+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 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" ${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/params.ts b/codegen/src/asset-account/params.ts
new file mode 100644
index 0000000..cb9a8d8
--- /dev/null
+++ b/codegen/src/asset-account/params.ts
@@ -0,0 +1,135 @@
+import { type } from "arktype";
+import { collapse } from "../common/string";
+
+const IamPolicyArn = type(/^arn:aws:iam::[^:]*:policy\/.*/);
+
+const CommonParams = 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: "",
+ }),
+
+ tags: type({ "[string]": "string" })
+ .configure({
+ title: "Tags",
+ description: collapse`
+ Tags to add to all resources deployed by this stack.
+ `,
+ })
+ .default(() => ({})),
+
+ 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.mustBe("a list without diplicates")
+ );
+ })
+ .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(),
+});
+
+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({
+ 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),
+}).describe("Params specific to CloudFormation orchestrator");
+
+export const TerraformParams = CommonParams.describe(
+ "Params specific to Terraform orchestrator",
+);
diff --git a/codegen/src/iam.ts b/codegen/src/common/iam.ts
similarity index 86%
rename from codegen/src/iam.ts
rename to codegen/src/common/iam.ts
index 911bb82..4c4f3d0 100644
--- a/codegen/src/iam.ts
+++ b/codegen/src/common/iam.ts
@@ -9,23 +9,23 @@ export interface Policy {
statements: PolicyStatement[];
}
-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[];
@@ -37,18 +37,25 @@ 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.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/inventory.ts b/codegen/src/common/inventory.ts
new file mode 100644
index 0000000..5551984
--- /dev/null
+++ b/codegen/src/common/inventory.ts
@@ -0,0 +1,125 @@
+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 = {
+ ReadInventory: [
+ {
+ 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",
+
+ "elasticfilesystem:DescribeFileSystems",
+ "elasticfilesystem:ListTagsForResource",
+ "elasticfilesystem:DescribeTags",
+
+ "fsx:DescribeVolumes",
+ "fsx:DescribeBackups",
+ "fsx:DescribeFileSystems",
+ "fsx:DescribeStorageVirtualMachines",
+ "fsx:ListTagsForResource",
+
+ // Volumes
+ "ec2:DescribeVolumeStatus",
+ "ec2:DescribeVolumes",
+
+ // Snapshots
+ "ec2:DescribeSnapshots",
+ "ec2:DescribeSnapshotAttribute",
+
+ // Common for all resources
+ "ec2:DescribeTags",
+
+ // Used for cost estimation
+ "ebs:ListSnapshotBlocks",
+ "ebs:ListChangedBlocks",
+
+ "ec2:DescribeInstances",
+ "ec2:DescribeImages",
+ "ec2:DescribeHosts",
+ "ssm:DescribeInstanceInformation",
+
+ // Used for network config troubleshooting
+ "ec2:DescribeAvailabilityZones",
+ "ec2:DescribeSecurityGroups",
+ "ec2:DescribeSubnets",
+ "ec2:DescribeVpcs",
+ "ec2:DescribeRouteTables",
+ "ec2:DescribeNatGateways",
+ "ec2:DescribeVpcEndpoints",
+
+ "s3:ListAllMyBuckets",
+ "s3:GetBucketLocation",
+ "s3:GetBucketTagging",
+ "s3:GetBucketObjectLockConfiguration",
+ "s3:GetBucketAcl",
+ "s3:GetBucketVersioning",
+ "s3:GetBucketPolicy",
+ "s3:GetBucketLogging",
+ "s3:ListBucket",
+
+ "iam:ListAccountAliases",
+ "ec2:DescribeRegions",
+ "kms:DescribeKey",
+
+ // 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",
+
+ // Just for debugging
+ "batch:DescribeComputeEnvironments",
+ "batch:DescribeJobDefinitions",
+ "batch:DescribeJobQueues",
+ "batch:DescribeJobs",
+ "batch:DescribeSchedulingPolicies",
+
+ "batch:GetJobQueueSnapshot",
+
+ "batch:ListJobs",
+ "batch:ListSchedulingPolicies",
+ "batch:ListTagsForResource",
+
+ "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}`
+>;
+
+type InventoryIamPolicyStatement = Omit & {
+ 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..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 "./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/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.",
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);
+}