diff --git a/.changeset/afraid-flies-repair.md b/.changeset/afraid-flies-repair.md new file mode 100644 index 0000000000..9076679019 --- /dev/null +++ b/.changeset/afraid-flies-repair.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend': minor +--- + +Re-export some plugin-types from submodule export @aws-amplify/backend/types/platform diff --git a/.changeset/brave-shirts-push.md b/.changeset/brave-shirts-push.md new file mode 100644 index 0000000000..5b528ef3ef --- /dev/null +++ b/.changeset/brave-shirts-push.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-function': patch +--- + +Set default function memory to 512 diff --git a/.changeset/breezy-eyes-appear.md b/.changeset/breezy-eyes-appear.md new file mode 100644 index 0000000000..78f91424a6 --- /dev/null +++ b/.changeset/breezy-eyes-appear.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-data': minor +--- + +plumb function access definition from schema into IAM policies attached to the functions diff --git a/.changeset/chatty-icons-mix.md b/.changeset/chatty-icons-mix.md new file mode 100644 index 0000000000..6cd88329e7 --- /dev/null +++ b/.changeset/chatty-icons-mix.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-storage': patch +--- + +fix cogntio identity placeholder value in IAM policy diff --git a/.changeset/clean-pets-join.md b/.changeset/clean-pets-join.md new file mode 100644 index 0000000000..6fa70cf799 --- /dev/null +++ b/.changeset/clean-pets-join.md @@ -0,0 +1,6 @@ +--- +'@aws-amplify/model-generator': minor +'@aws-amplify/backend-cli': patch +--- + +Rename target format type and prop in model gen package diff --git a/.changeset/empty-emus-thank.md b/.changeset/empty-emus-thank.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/empty-emus-thank.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/famous-eels-kiss.md b/.changeset/famous-eels-kiss.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/famous-eels-kiss.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/famous-glasses-know.md b/.changeset/famous-glasses-know.md new file mode 100644 index 0000000000..eab784ba5c --- /dev/null +++ b/.changeset/famous-glasses-know.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-data': minor +--- + +remove allowListedRoleNames from defineData diff --git a/.changeset/fluffy-books-dance.md b/.changeset/fluffy-books-dance.md new file mode 100644 index 0000000000..2657dab900 --- /dev/null +++ b/.changeset/fluffy-books-dance.md @@ -0,0 +1,6 @@ +--- +'@aws-amplify/backend-data': patch +'@aws-amplify/backend': patch +--- + +backend-data: add js resolver support diff --git a/.changeset/four-peaches-pull.md b/.changeset/four-peaches-pull.md new file mode 100644 index 0000000000..b46cf3d812 --- /dev/null +++ b/.changeset/four-peaches-pull.md @@ -0,0 +1,6 @@ +--- +'@aws-amplify/integration-tests': patch +'@aws-amplify/backend-data': patch +--- + +backend-data: add support for first-class defineFunction diff --git a/.changeset/good-wasps-sin.md b/.changeset/good-wasps-sin.md new file mode 100644 index 0000000000..27ac93fc47 --- /dev/null +++ b/.changeset/good-wasps-sin.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-auth': minor +--- + +attach policy & ssm params to acces userpool from auth resource diff --git a/.changeset/great-timers-invent.md b/.changeset/great-timers-invent.md new file mode 100644 index 0000000000..888724f2d8 --- /dev/null +++ b/.changeset/great-timers-invent.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-storage': minor +--- + +Implement deny-by-default behavior on access rules diff --git a/.changeset/late-worms-rule.md b/.changeset/late-worms-rule.md new file mode 100644 index 0000000000..84a9fb9a44 --- /dev/null +++ b/.changeset/late-worms-rule.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend': patch +--- + +add aspect on root stack to valid role trust policies diff --git a/.changeset/little-baboons-tan.md b/.changeset/little-baboons-tan.md new file mode 100644 index 0000000000..95839b1ace --- /dev/null +++ b/.changeset/little-baboons-tan.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-function': patch +--- + +store attribution string in funciton stack diff --git a/.changeset/lucky-tigers-carry.md b/.changeset/lucky-tigers-carry.md new file mode 100644 index 0000000000..374a227b51 --- /dev/null +++ b/.changeset/lucky-tigers-carry.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-data': minor +--- + +feat: enable destructive schema updates in amplify sandbox diff --git a/.changeset/modern-files-arrive.md b/.changeset/modern-files-arrive.md new file mode 100644 index 0000000000..d3ff04646d --- /dev/null +++ b/.changeset/modern-files-arrive.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/client-config': minor +--- + +fix(client-config): add legacy analytics configuration key diff --git a/.changeset/modern-moons-fail.md b/.changeset/modern-moons-fail.md new file mode 100644 index 0000000000..d69d6be921 --- /dev/null +++ b/.changeset/modern-moons-fail.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-cli': patch +--- + +Update text to match sandbox default behavior diff --git a/.changeset/modern-terms-stare.md b/.changeset/modern-terms-stare.md new file mode 100644 index 0000000000..f82161ed49 --- /dev/null +++ b/.changeset/modern-terms-stare.md @@ -0,0 +1,6 @@ +--- +'@aws-amplify/integration-tests': patch +'@aws-amplify/backend-function': patch +--- + +Ensure resource access env vars are added to function typed shim files diff --git a/.changeset/new-kings-beg.md b/.changeset/new-kings-beg.md new file mode 100644 index 0000000000..c44b1adf6a --- /dev/null +++ b/.changeset/new-kings-beg.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/cli-core': patch +--- + +add error message for PNPM on windows diff --git a/.changeset/new-meals-destroy.md b/.changeset/new-meals-destroy.md new file mode 100644 index 0000000000..e66a7a7f74 --- /dev/null +++ b/.changeset/new-meals-destroy.md @@ -0,0 +1,7 @@ +--- +'@aws-amplify/cli-core': minor +'@aws-amplify/sandbox': patch +'@aws-amplify/backend-cli': patch +--- + +use `format` to replace `color` and remove `color`. diff --git a/.changeset/odd-shirts-collect.md b/.changeset/odd-shirts-collect.md new file mode 100644 index 0000000000..e67161407e --- /dev/null +++ b/.changeset/odd-shirts-collect.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/auth-construct-alpha': patch +--- + +Fix deployment bug with SAML providers. diff --git a/.changeset/popular-bobcats-provide.md b/.changeset/popular-bobcats-provide.md new file mode 100644 index 0000000000..a21dd3999d --- /dev/null +++ b/.changeset/popular-bobcats-provide.md @@ -0,0 +1,6 @@ +--- +'@aws-amplify/integration-tests': minor +'@aws-amplify/backend-function': minor +--- + +Add dynamic environment variables to function type definition files diff --git a/.changeset/pre.json b/.changeset/pre.json index a133e8886e..0776a5440d 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -27,24 +27,69 @@ "@aws-amplify/sandbox": "0.5.1" }, "changesets": [ + "afraid-flies-repair", "brave-carrots-glow", "brave-pets-clean", + "brave-shirts-push", + "breezy-eyes-appear", "brown-otters-smoke", + "chatty-icons-mix", + "clean-pets-join", + "cyan-steaks-repeat", "eighty-rings-pull", + "empty-emus-thank", + "famous-eels-kiss", + "famous-glasses-know", "five-fireants-shout", + "fluffy-books-dance", "four-donuts-jump", + "four-peaches-pull", "friendly-kids-lick", "giant-feet-sing", + "good-wasps-sin", + "great-timers-invent", "khaki-panthers-grin", + "khaki-pants-sniff", + "late-worms-rule", "lemon-peas-sin", "light-cougars-give", + "little-baboons-tan", + "little-books-press", "loud-sheep-occur", + "lucky-tigers-carry", + "lucky-trainers-matter", "mean-frogs-visit", "mighty-experts-compare", + "modern-files-arrive", + "modern-moons-fail", + "modern-terms-stare", + "new-kings-beg", + "new-meals-destroy", + "odd-shirts-collect", + "polite-kiwis-brake", + "popular-bobcats-provide", + "pretty-cups-jog", + "pretty-lobsters-prove", + "proud-bags-dream", + "proud-feet-hide", + "quiet-pets-scream", + "quiet-shirts-hug", + "rich-onions-learn", + "rude-toys-visit", + "serious-maps-wait", + "short-bulldogs-punch", + "short-olives-bow", + "shy-horses-act", + "silver-needles-rush", + "small-hotels-do", + "smart-crews-serve", "smooth-penguins-joke", "smooth-tigers-double", "sour-rice-listen", + "spicy-bulldogs-itch", + "thin-steaks-shave", "three-doors-act", - "tidy-readers-prove" + "tidy-readers-prove", + "two-shirts-type" ] } diff --git a/.changeset/proud-bags-dream.md b/.changeset/proud-bags-dream.md new file mode 100644 index 0000000000..c35aba609b --- /dev/null +++ b/.changeset/proud-bags-dream.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/sandbox': patch +--- + +upgrade @parcel/watcher wo use the latest version diff --git a/.changeset/proud-feet-hide.md b/.changeset/proud-feet-hide.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/proud-feet-hide.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/rich-onions-learn.md b/.changeset/rich-onions-learn.md new file mode 100644 index 0000000000..d12a8b8b8e --- /dev/null +++ b/.changeset/rich-onions-learn.md @@ -0,0 +1,7 @@ +--- +'create-amplify': patch +'@aws-amplify/cli-core': patch +'@aws-amplify/backend-cli': patch +--- + +use printer from cli-core diff --git a/.changeset/rude-toys-visit.md b/.changeset/rude-toys-visit.md new file mode 100644 index 0000000000..ebb2233689 --- /dev/null +++ b/.changeset/rude-toys-visit.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/cli-core': patch +--- + +update PackageManagerControllerFactory to take Operation System platform information optionally diff --git a/.changeset/serious-maps-wait.md b/.changeset/serious-maps-wait.md new file mode 100644 index 0000000000..ae0eb4a134 --- /dev/null +++ b/.changeset/serious-maps-wait.md @@ -0,0 +1,11 @@ +--- +'@aws-amplify/backend-deployer': patch +'@aws-amplify/platform-core': minor +'@aws-amplify/backend-auth': patch +'@aws-amplify/backend-data': patch +'@aws-amplify/cli-core': patch +'@aws-amplify/sandbox': patch +'@aws-amplify/backend-cli': patch +--- + +require "resolution" in AmplifyUserError options diff --git a/.changeset/short-olives-bow.md b/.changeset/short-olives-bow.md new file mode 100644 index 0000000000..8663b21577 --- /dev/null +++ b/.changeset/short-olives-bow.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-storage': minor +--- + +Add "list" to available storage resource actions diff --git a/.changeset/shy-horses-act.md b/.changeset/shy-horses-act.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/shy-horses-act.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/small-hotels-do.md b/.changeset/small-hotels-do.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/small-hotels-do.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/smart-crews-serve.md b/.changeset/smart-crews-serve.md new file mode 100644 index 0000000000..1d002ce758 --- /dev/null +++ b/.changeset/smart-crews-serve.md @@ -0,0 +1,6 @@ +--- +'@aws-amplify/integration-tests': patch +'@aws-amplify/backend-function': patch +--- + +Ensure typed shim files contain only the function name diff --git a/.changeset/spicy-bulldogs-itch.md b/.changeset/spicy-bulldogs-itch.md new file mode 100644 index 0000000000..586cdf7de8 --- /dev/null +++ b/.changeset/spicy-bulldogs-itch.md @@ -0,0 +1,6 @@ +--- +'@aws-amplify/backend-storage': minor +'@aws-amplify/backend-auth': minor +--- + +Enable auth group access to storage and change syntax for specifying owner-based access diff --git a/.changeset/thin-steaks-shave.md b/.changeset/thin-steaks-shave.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/thin-steaks-shave.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/two-shirts-type.md b/.changeset/two-shirts-type.md new file mode 100644 index 0000000000..b648cf6396 --- /dev/null +++ b/.changeset/two-shirts-type.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/deployed-backend-client': minor +--- + +Add listBackends method to return a list of stacks for sandbox and branch deployments diff --git a/.eslint_dictionary.json b/.eslint_dictionary.json index a05c850890..54e92fd56c 100644 --- a/.eslint_dictionary.json +++ b/.eslint_dictionary.json @@ -11,6 +11,7 @@ "argv", "arn", "arns", + "backends", "birthdate", "bundler", "cdk", @@ -126,6 +127,7 @@ "subcommand", "subcommands", "submodule", + "subpath", "syncable", "timestamps", "tmpdir", diff --git a/.github/actions/install_with_cache/action.yml b/.github/actions/install_with_cache/action.yml index 24a0fe867e..cc6ec48b6a 100644 --- a/.github/actions/install_with_cache/action.yml +++ b/.github/actions/install_with_cache/action.yml @@ -10,7 +10,7 @@ runs: path: | node_modules packages/**/node_modules - key: ${{ runner.os }}-${{ hashFiles('package-lock.json') }} + key: ${{ runner.os }}-${{ hashFiles('package-lock.json') }}-v2 # only install if cache miss - if: steps.npm-cache.outputs.cache-hit != 'true' shell: bash diff --git a/.github/workflows/health_checks.yml b/.github/workflows/health_checks.yml index 33300bb895..857940ab29 100644 --- a/.github/workflows/health_checks.yml +++ b/.github/workflows/health_checks.yml @@ -16,7 +16,8 @@ jobs: # Windows install must happen on the same worker size as subsequent jobs. # Larger workers use different drive (C: instead of D:) to check out project and NPM installation # creates file system links that include drive letter. - os: [ubuntu-latest, macos-latest, amplify-backend_windows-latest_8-core] + # Changing between standard and custom workers requires full install cache invalidation + os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # version 3.6.0 @@ -35,7 +36,7 @@ jobs: - build strategy: matrix: - os: [ubuntu-latest, macos-latest, amplify-backend_windows-latest_8-core] + os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # version 3.6.0 @@ -131,12 +132,7 @@ jobs: # will finish running other test matrices even if one fails fail-fast: false matrix: - os: - [ - amplify-backend_ubuntu-latest_4-core, - macos-latest-xl, - amplify-backend_windows-latest_8-core, - ] + os: [ubuntu-latest, macos-latest-xl, windows-latest] runs-on: ${{ matrix.os }} timeout-minutes: 25 needs: @@ -170,12 +166,9 @@ jobs: # will finish running other test matrices even if one fails fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, amplify-backend_windows-latest_8-core] + os: [ubuntu-latest, macos-latest, windows-latest] pkg-manager: [npm, yarn-classic, yarn-modern, pnpm] node-version: [20] - exclude: - - os: amplify-backend_windows-latest_8-core - pkg-manager: pnpm env: PACKAGE_MANAGER: ${{ matrix.pkg-manager }} runs-on: ${{ matrix.os }} @@ -305,7 +298,9 @@ jobs: update_or_publish_versions: if: ${{ github.event_name == 'push' && github.ref_name == 'main' }} needs: - - build + - run_package_manager_e2e_tests + - test_with_coverage + - run_e2e_tests runs-on: ubuntu-latest steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # version 3.6.0 diff --git a/.gitignore b/.gitignore index 972d472104..60b72325bf 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,4 @@ e2e-tests concurrent_workspace_script_cache.json testDir -.amplify +/.amplify diff --git a/PROJECT_ARCHITECTURE.md b/PROJECT_ARCHITECTURE.md index a21ae0306f..53128ab1ba 100644 --- a/PROJECT_ARCHITECTURE.md +++ b/PROJECT_ARCHITECTURE.md @@ -63,7 +63,7 @@ The following diagram has a basic dependency graph of the package structure. Thi ![Simple dependency graph](markdown-assets/simple-dependency-graph.png) -At the root, the customer project declares a dependency on `@aws-amplify/cli` and `@aws-amplify/backend`. The cli package depends on several packages for handling various subcommands. +At the root, the customer project declares a dependency on `@aws-amplify/backend-cli` and `@aws-amplify/backend`. The backend-cli package depends on several packages for handling various subcommands. The backend package depends on several feature vertical packages (auth, data, storage, functions). The feature vertical packages implement interfaces defined in plugin-types which the backend package also depends on. diff --git a/markdown-assets/simple-dependency-graph.png b/markdown-assets/simple-dependency-graph.png index 2b8fd74814..4b2fc419a2 100644 Binary files a/markdown-assets/simple-dependency-graph.png and b/markdown-assets/simple-dependency-graph.png differ diff --git a/package-lock.json b/package-lock.json index 5717e2a65b..bd2229e7b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1244,17 +1244,17 @@ } }, "node_modules/@aws-amplify/data-schema": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema/-/data-schema-0.13.2.tgz", - "integrity": "sha512-N30C14Vd7D/0+GmCZKNDYOi/9gPSOH8GDR9AUCigee0RJLfYHvI0IAuOsGR1/06wZutN9stwYU8jsTm58jNHBA==", + "version": "0.13.15", + "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema/-/data-schema-0.13.15.tgz", + "integrity": "sha512-7rzbNPHVIMqra6do7kX8NEKiLh9ED8SvYbTwE5GwXsguzPhTIP9qjfss5n/Oy83I0KqiG2np26wG7gkM/1WBLw==", "dependencies": { "@aws-amplify/data-schema-types": "*" } }, "node_modules/@aws-amplify/data-schema-types": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema-types/-/data-schema-types-0.7.2.tgz", - "integrity": "sha512-X3AE95rfeeT8NmUwI4PfWlijn3354OpIxIGigzTpPTmRwg/OAKQE7cB9HL1ITfKNh4hrCbPMdl8I7jePSwpNGQ==", + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema-types/-/data-schema-types-0.7.8.tgz", + "integrity": "sha512-ze4Rwlx2V/Snr8o0Yzb2SisIhPmWlWLEFXRYQ8N+RxFX0/gTbBielfR78D2eKVk1ufBEB3RAFCXJFs7KuW9MHg==", "dependencies": { "rxjs": "^7.8.1" } @@ -3896,51 +3896,51 @@ } }, "node_modules/@aws-sdk/client-iam": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-iam/-/client-iam-3.515.0.tgz", - "integrity": "sha512-gjesiRmg6wj8xhKIjif8NFyfTmkFmg9xWCC1Br34GKvFqq/dtyr1tE0vbrJxQW8J6H2MQDbTKrNJdS6/eQG1tw==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-iam/-/client-iam-3.523.0.tgz", + "integrity": "sha512-ghiGVdklDJIrwDbIGRogPeDOiPyzBdIKMKSN3fM6fPm0q0KjcEuvUrDxxytkrKOMgi9yNnCDCukHcZS6KkvtEg==", "dev": true, "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.515.0", - "@aws-sdk/core": "3.513.0", - "@aws-sdk/credential-provider-node": "3.515.0", - "@aws-sdk/middleware-host-header": "3.515.0", - "@aws-sdk/middleware-logger": "3.515.0", - "@aws-sdk/middleware-recursion-detection": "3.515.0", - "@aws-sdk/middleware-user-agent": "3.515.0", - "@aws-sdk/region-config-resolver": "3.515.0", - "@aws-sdk/types": "3.515.0", - "@aws-sdk/util-endpoints": "3.515.0", - "@aws-sdk/util-user-agent-browser": "3.515.0", - "@aws-sdk/util-user-agent-node": "3.515.0", - "@smithy/config-resolver": "^2.1.1", - "@smithy/core": "^1.3.2", - "@smithy/fetch-http-handler": "^2.4.1", - "@smithy/hash-node": "^2.1.1", - "@smithy/invalid-dependency": "^2.1.1", - "@smithy/middleware-content-length": "^2.1.1", - "@smithy/middleware-endpoint": "^2.4.1", - "@smithy/middleware-retry": "^2.1.1", - "@smithy/middleware-serde": "^2.1.1", - "@smithy/middleware-stack": "^2.1.1", - "@smithy/node-config-provider": "^2.2.1", - "@smithy/node-http-handler": "^2.3.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/url-parser": "^2.1.1", + "@aws-sdk/client-sts": "3.523.0", + "@aws-sdk/core": "3.523.0", + "@aws-sdk/credential-provider-node": "3.523.0", + "@aws-sdk/middleware-host-header": "3.523.0", + "@aws-sdk/middleware-logger": "3.523.0", + "@aws-sdk/middleware-recursion-detection": "3.523.0", + "@aws-sdk/middleware-user-agent": "3.523.0", + "@aws-sdk/region-config-resolver": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-endpoints": "3.523.0", + "@aws-sdk/util-user-agent-browser": "3.523.0", + "@aws-sdk/util-user-agent-node": "3.523.0", + "@smithy/config-resolver": "^2.1.3", + "@smithy/core": "^1.3.4", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/hash-node": "^2.1.3", + "@smithy/invalid-dependency": "^2.1.3", + "@smithy/middleware-content-length": "^2.1.3", + "@smithy/middleware-endpoint": "^2.4.3", + "@smithy/middleware-retry": "^2.1.3", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/node-config-provider": "^2.2.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.1", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", "@smithy/util-base64": "^2.1.1", "@smithy/util-body-length-browser": "^2.1.1", "@smithy/util-body-length-node": "^2.2.1", - "@smithy/util-defaults-mode-browser": "^2.1.1", - "@smithy/util-defaults-mode-node": "^2.2.0", - "@smithy/util-endpoints": "^1.1.1", - "@smithy/util-middleware": "^2.1.1", - "@smithy/util-retry": "^2.1.1", + "@smithy/util-defaults-mode-browser": "^2.1.3", + "@smithy/util-defaults-mode-node": "^2.2.2", + "@smithy/util-endpoints": "^1.1.3", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-retry": "^2.1.3", "@smithy/util-utf8": "^2.1.1", - "@smithy/util-waiter": "^2.1.1", + "@smithy/util-waiter": "^2.1.3", "fast-xml-parser": "4.2.5", "tslib": "^2.5.0" }, @@ -3949,47 +3949,47 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sso": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.515.0.tgz", - "integrity": "sha512-4oGBLW476zmkdN98lAns3bObRNO+DLOfg4MDUSR6l6GYBV/zGAtoy2O/FhwYKgA2L5h2ZtElGopLlk/1Q0ePLw==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.523.0.tgz", + "integrity": "sha512-vob/Tk9bIr6VIyzScBWsKpP92ACI6/aOXBL2BITgvRWl5Umqi1jXFtfssj/N2UJHM4CBMRwxIJ33InfN0gPxZw==", "dev": true, "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/core": "3.513.0", - "@aws-sdk/middleware-host-header": "3.515.0", - "@aws-sdk/middleware-logger": "3.515.0", - "@aws-sdk/middleware-recursion-detection": "3.515.0", - "@aws-sdk/middleware-user-agent": "3.515.0", - "@aws-sdk/region-config-resolver": "3.515.0", - "@aws-sdk/types": "3.515.0", - "@aws-sdk/util-endpoints": "3.515.0", - "@aws-sdk/util-user-agent-browser": "3.515.0", - "@aws-sdk/util-user-agent-node": "3.515.0", - "@smithy/config-resolver": "^2.1.1", - "@smithy/core": "^1.3.2", - "@smithy/fetch-http-handler": "^2.4.1", - "@smithy/hash-node": "^2.1.1", - "@smithy/invalid-dependency": "^2.1.1", - "@smithy/middleware-content-length": "^2.1.1", - "@smithy/middleware-endpoint": "^2.4.1", - "@smithy/middleware-retry": "^2.1.1", - "@smithy/middleware-serde": "^2.1.1", - "@smithy/middleware-stack": "^2.1.1", - "@smithy/node-config-provider": "^2.2.1", - "@smithy/node-http-handler": "^2.3.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/url-parser": "^2.1.1", + "@aws-sdk/core": "3.523.0", + "@aws-sdk/middleware-host-header": "3.523.0", + "@aws-sdk/middleware-logger": "3.523.0", + "@aws-sdk/middleware-recursion-detection": "3.523.0", + "@aws-sdk/middleware-user-agent": "3.523.0", + "@aws-sdk/region-config-resolver": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-endpoints": "3.523.0", + "@aws-sdk/util-user-agent-browser": "3.523.0", + "@aws-sdk/util-user-agent-node": "3.523.0", + "@smithy/config-resolver": "^2.1.3", + "@smithy/core": "^1.3.4", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/hash-node": "^2.1.3", + "@smithy/invalid-dependency": "^2.1.3", + "@smithy/middleware-content-length": "^2.1.3", + "@smithy/middleware-endpoint": "^2.4.3", + "@smithy/middleware-retry": "^2.1.3", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/node-config-provider": "^2.2.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.1", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", "@smithy/util-base64": "^2.1.1", "@smithy/util-body-length-browser": "^2.1.1", "@smithy/util-body-length-node": "^2.2.1", - "@smithy/util-defaults-mode-browser": "^2.1.1", - "@smithy/util-defaults-mode-node": "^2.2.0", - "@smithy/util-endpoints": "^1.1.1", - "@smithy/util-middleware": "^2.1.1", - "@smithy/util-retry": "^2.1.1", + "@smithy/util-defaults-mode-browser": "^2.1.3", + "@smithy/util-defaults-mode-node": "^2.2.2", + "@smithy/util-endpoints": "^1.1.3", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-retry": "^2.1.3", "@smithy/util-utf8": "^2.1.1", "tslib": "^2.5.0" }, @@ -3998,48 +3998,48 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.515.0.tgz", - "integrity": "sha512-zACa8LNlPUdlNUBqQRf5a3MfouLNtcBfm84v2c8M976DwJrMGONPe1QjyLLsD38uESQiXiVQRruj/b000iMXNw==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.523.0.tgz", + "integrity": "sha512-OktkdiuJ5DtYgNrJlo53Tf7pJ+UWfOt7V7or0ije6MysLP18GwlTkbg2UE4EUtfOxt/baXxHMlExB1vmRtlATw==", "dev": true, "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.515.0", - "@aws-sdk/core": "3.513.0", - "@aws-sdk/middleware-host-header": "3.515.0", - "@aws-sdk/middleware-logger": "3.515.0", - "@aws-sdk/middleware-recursion-detection": "3.515.0", - "@aws-sdk/middleware-user-agent": "3.515.0", - "@aws-sdk/region-config-resolver": "3.515.0", - "@aws-sdk/types": "3.515.0", - "@aws-sdk/util-endpoints": "3.515.0", - "@aws-sdk/util-user-agent-browser": "3.515.0", - "@aws-sdk/util-user-agent-node": "3.515.0", - "@smithy/config-resolver": "^2.1.1", - "@smithy/core": "^1.3.2", - "@smithy/fetch-http-handler": "^2.4.1", - "@smithy/hash-node": "^2.1.1", - "@smithy/invalid-dependency": "^2.1.1", - "@smithy/middleware-content-length": "^2.1.1", - "@smithy/middleware-endpoint": "^2.4.1", - "@smithy/middleware-retry": "^2.1.1", - "@smithy/middleware-serde": "^2.1.1", - "@smithy/middleware-stack": "^2.1.1", - "@smithy/node-config-provider": "^2.2.1", - "@smithy/node-http-handler": "^2.3.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/url-parser": "^2.1.1", + "@aws-sdk/client-sts": "3.523.0", + "@aws-sdk/core": "3.523.0", + "@aws-sdk/middleware-host-header": "3.523.0", + "@aws-sdk/middleware-logger": "3.523.0", + "@aws-sdk/middleware-recursion-detection": "3.523.0", + "@aws-sdk/middleware-user-agent": "3.523.0", + "@aws-sdk/region-config-resolver": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-endpoints": "3.523.0", + "@aws-sdk/util-user-agent-browser": "3.523.0", + "@aws-sdk/util-user-agent-node": "3.523.0", + "@smithy/config-resolver": "^2.1.3", + "@smithy/core": "^1.3.4", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/hash-node": "^2.1.3", + "@smithy/invalid-dependency": "^2.1.3", + "@smithy/middleware-content-length": "^2.1.3", + "@smithy/middleware-endpoint": "^2.4.3", + "@smithy/middleware-retry": "^2.1.3", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/node-config-provider": "^2.2.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.1", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", "@smithy/util-base64": "^2.1.1", "@smithy/util-body-length-browser": "^2.1.1", "@smithy/util-body-length-node": "^2.2.1", - "@smithy/util-defaults-mode-browser": "^2.1.1", - "@smithy/util-defaults-mode-node": "^2.2.0", - "@smithy/util-endpoints": "^1.1.1", - "@smithy/util-middleware": "^2.1.1", - "@smithy/util-retry": "^2.1.1", + "@smithy/util-defaults-mode-browser": "^2.1.3", + "@smithy/util-defaults-mode-node": "^2.2.2", + "@smithy/util-endpoints": "^1.1.3", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-retry": "^2.1.3", "@smithy/util-utf8": "^2.1.1", "tslib": "^2.5.0" }, @@ -4047,51 +4047,51 @@ "node": ">=14.0.0" }, "peerDependencies": { - "@aws-sdk/credential-provider-node": "^3.515.0" + "@aws-sdk/credential-provider-node": "^3.523.0" } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sts": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.515.0.tgz", - "integrity": "sha512-ScYuvaIDgip3atOJIA1FU2n0gJkEdveu1KrrCPathoUCV5zpK8qQmO/n+Fj/7hKFxeKdFbB+4W4CsJWYH94nlg==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.523.0.tgz", + "integrity": "sha512-ggAkL8szaJkqD8oOsS68URJ9XMDbLA/INO/NPZJqv9BhmftecJvfy43uUVWGNs6n4YXNzfF0Y+zQ3DT0fZkv9g==", "dev": true, "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/core": "3.513.0", - "@aws-sdk/middleware-host-header": "3.515.0", - "@aws-sdk/middleware-logger": "3.515.0", - "@aws-sdk/middleware-recursion-detection": "3.515.0", - "@aws-sdk/middleware-user-agent": "3.515.0", - "@aws-sdk/region-config-resolver": "3.515.0", - "@aws-sdk/types": "3.515.0", - "@aws-sdk/util-endpoints": "3.515.0", - "@aws-sdk/util-user-agent-browser": "3.515.0", - "@aws-sdk/util-user-agent-node": "3.515.0", - "@smithy/config-resolver": "^2.1.1", - "@smithy/core": "^1.3.2", - "@smithy/fetch-http-handler": "^2.4.1", - "@smithy/hash-node": "^2.1.1", - "@smithy/invalid-dependency": "^2.1.1", - "@smithy/middleware-content-length": "^2.1.1", - "@smithy/middleware-endpoint": "^2.4.1", - "@smithy/middleware-retry": "^2.1.1", - "@smithy/middleware-serde": "^2.1.1", - "@smithy/middleware-stack": "^2.1.1", - "@smithy/node-config-provider": "^2.2.1", - "@smithy/node-http-handler": "^2.3.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/url-parser": "^2.1.1", + "@aws-sdk/core": "3.523.0", + "@aws-sdk/middleware-host-header": "3.523.0", + "@aws-sdk/middleware-logger": "3.523.0", + "@aws-sdk/middleware-recursion-detection": "3.523.0", + "@aws-sdk/middleware-user-agent": "3.523.0", + "@aws-sdk/region-config-resolver": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-endpoints": "3.523.0", + "@aws-sdk/util-user-agent-browser": "3.523.0", + "@aws-sdk/util-user-agent-node": "3.523.0", + "@smithy/config-resolver": "^2.1.3", + "@smithy/core": "^1.3.4", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/hash-node": "^2.1.3", + "@smithy/invalid-dependency": "^2.1.3", + "@smithy/middleware-content-length": "^2.1.3", + "@smithy/middleware-endpoint": "^2.4.3", + "@smithy/middleware-retry": "^2.1.3", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/node-config-provider": "^2.2.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.1", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", "@smithy/util-base64": "^2.1.1", "@smithy/util-body-length-browser": "^2.1.1", "@smithy/util-body-length-node": "^2.2.1", - "@smithy/util-defaults-mode-browser": "^2.1.1", - "@smithy/util-defaults-mode-node": "^2.2.0", - "@smithy/util-endpoints": "^1.1.1", - "@smithy/util-middleware": "^2.1.1", - "@smithy/util-retry": "^2.1.1", + "@smithy/util-defaults-mode-browser": "^2.1.3", + "@smithy/util-defaults-mode-node": "^2.2.2", + "@smithy/util-endpoints": "^1.1.3", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-retry": "^2.1.3", "@smithy/util-utf8": "^2.1.1", "fast-xml-parser": "4.2.5", "tslib": "^2.5.0" @@ -4100,18 +4100,35 @@ "node": ">=14.0.0" }, "peerDependencies": { - "@aws-sdk/credential-provider-node": "^3.515.0" + "@aws-sdk/credential-provider-node": "^3.523.0" + } + }, + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/core": { + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.523.0.tgz", + "integrity": "sha512-JHa3ngEWkTzZ2YTn6EavcADC8gv6zZU4U9WBAleClh6ioXH0kGMBawZje3y0F0mKyLTfLhFqFUlCV5sngI/Qcw==", + "dev": true, + "dependencies": { + "@smithy/core": "^1.3.4", + "@smithy/protocol-http": "^3.2.1", + "@smithy/signature-v4": "^2.1.3", + "@smithy/smithy-client": "^2.4.1", + "@smithy/types": "^2.10.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.515.0.tgz", - "integrity": "sha512-45vxdyqhTAaUMERYVWOziG3K8L2TV9G4ryQS/KZ84o7NAybE9GMdoZRVmGHAO7mJJ1wQiYCM/E+i5b3NW9JfNA==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.523.0.tgz", + "integrity": "sha512-Y6DWdH6/OuMDoNKVzZlNeBc6f1Yjk1lYMjANKpIhMbkRCvLJw/PYZKOZa8WpXbTYdgg9XLjKybnLIb3ww3uuzA==", "dev": true, "dependencies": { - "@aws-sdk/types": "3.515.0", - "@smithy/property-provider": "^2.1.1", - "@smithy/types": "^2.9.1", + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4119,19 +4136,19 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.515.0.tgz", - "integrity": "sha512-Ba6FXK77vU4WyheiamNjEuTFmir0eAXuJGPO27lBaA8g+V/seXGHScsbOG14aQGDOr2P02OPwKGZrWWA7BFpfQ==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.523.0.tgz", + "integrity": "sha512-6YUtePbn3UFpY9qfVwHFWIVnFvVS5vsbGxxkTO02swvZBvVG4sdG0Xj0AbotUNQNY9QTCN7WkhwIrd50rfDQ9Q==", "dev": true, "dependencies": { - "@aws-sdk/types": "3.515.0", - "@smithy/fetch-http-handler": "^2.4.1", - "@smithy/node-http-handler": "^2.3.1", - "@smithy/property-provider": "^2.1.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/util-stream": "^2.1.1", + "@aws-sdk/types": "3.523.0", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/property-provider": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.1", + "@smithy/types": "^2.10.1", + "@smithy/util-stream": "^2.1.3", "tslib": "^2.5.0" }, "engines": { @@ -4139,21 +4156,21 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.515.0.tgz", - "integrity": "sha512-ouDlNZdv2TKeVEA/YZk2+XklTXyAAGdbWnl4IgN9ItaodWI+lZjdIoNC8BAooVH+atIV/cZgoGTGQL7j2TxJ9A==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.523.0.tgz", + "integrity": "sha512-dRch5Ts67FFRZY5r9DpiC3PM6BVHv1tRcy1b26hoqfFkxP9xYH3dsTSPBog1azIqaJa2GcXqEvKCqhghFTt4Xg==", "dev": true, "dependencies": { - "@aws-sdk/client-sts": "3.515.0", - "@aws-sdk/credential-provider-env": "3.515.0", - "@aws-sdk/credential-provider-process": "3.515.0", - "@aws-sdk/credential-provider-sso": "3.515.0", - "@aws-sdk/credential-provider-web-identity": "3.515.0", - "@aws-sdk/types": "3.515.0", - "@smithy/credential-provider-imds": "^2.2.1", - "@smithy/property-provider": "^2.1.1", - "@smithy/shared-ini-file-loader": "^2.3.1", - "@smithy/types": "^2.9.1", + "@aws-sdk/client-sts": "3.523.0", + "@aws-sdk/credential-provider-env": "3.523.0", + "@aws-sdk/credential-provider-process": "3.523.0", + "@aws-sdk/credential-provider-sso": "3.523.0", + "@aws-sdk/credential-provider-web-identity": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@smithy/credential-provider-imds": "^2.2.3", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4161,22 +4178,22 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.515.0.tgz", - "integrity": "sha512-Y4kHSpbxksiCZZNcvsiKUd8Fb2XlyUuONEwqWFNL82ZH6TCCjBGS31wJQCSxBHqYcOL3tiORUEJkoO7uS30uQA==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.523.0.tgz", + "integrity": "sha512-0aW5ylA8pZmvv/8qA/+iel4acEyzSlHRiaHYL3L0qu9SSoe2a92+RHjrxKl6+Sb55eA2mRfQjaN8oOa5xiYyKA==", "dev": true, "dependencies": { - "@aws-sdk/credential-provider-env": "3.515.0", - "@aws-sdk/credential-provider-http": "3.515.0", - "@aws-sdk/credential-provider-ini": "3.515.0", - "@aws-sdk/credential-provider-process": "3.515.0", - "@aws-sdk/credential-provider-sso": "3.515.0", - "@aws-sdk/credential-provider-web-identity": "3.515.0", - "@aws-sdk/types": "3.515.0", - "@smithy/credential-provider-imds": "^2.2.1", - "@smithy/property-provider": "^2.1.1", - "@smithy/shared-ini-file-loader": "^2.3.1", - "@smithy/types": "^2.9.1", + "@aws-sdk/credential-provider-env": "3.523.0", + "@aws-sdk/credential-provider-http": "3.523.0", + "@aws-sdk/credential-provider-ini": "3.523.0", + "@aws-sdk/credential-provider-process": "3.523.0", + "@aws-sdk/credential-provider-sso": "3.523.0", + "@aws-sdk/credential-provider-web-identity": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@smithy/credential-provider-imds": "^2.2.3", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4184,15 +4201,15 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.515.0.tgz", - "integrity": "sha512-pSjiOA2FM63LHRKNDvEpBRp80FVGT0Mw/gzgbqFXP+sewk0WVonYbEcMDTJptH3VsLPGzqH/DQ1YL/aEIBuXFQ==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.523.0.tgz", + "integrity": "sha512-f0LP9KlFmMvPWdKeUKYlZ6FkQAECUeZMmISsv6NKtvPCI9e4O4cLTeR09telwDK8P0HrgcRuZfXM7E30m8re0Q==", "dev": true, "dependencies": { - "@aws-sdk/types": "3.515.0", - "@smithy/property-provider": "^2.1.1", - "@smithy/shared-ini-file-loader": "^2.3.1", - "@smithy/types": "^2.9.1", + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4200,17 +4217,17 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.515.0.tgz", - "integrity": "sha512-j7vUkiSmuhpBvZYoPTRTI4ePnQbiZMFl6TNhg9b9DprC1zHkucsZnhRhqjOVlrw/H6J4jmcPGcHHTZ5WQNI5xQ==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.523.0.tgz", + "integrity": "sha512-/VfOJuI8ImV//W4gr+yieF/4shzWAzWYeaaNu7hv161C5YW7/OoCygwRVHSnF4KKeUGQZomZWwml5zHZ57f8xQ==", "dev": true, "dependencies": { - "@aws-sdk/client-sso": "3.515.0", - "@aws-sdk/token-providers": "3.515.0", - "@aws-sdk/types": "3.515.0", - "@smithy/property-provider": "^2.1.1", - "@smithy/shared-ini-file-loader": "^2.3.1", - "@smithy/types": "^2.9.1", + "@aws-sdk/client-sso": "3.523.0", + "@aws-sdk/token-providers": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4218,15 +4235,15 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.515.0.tgz", - "integrity": "sha512-66+2g4z3fWwdoGReY8aUHvm6JrKZMTRxjuizljVmMyOBttKPeBYXvUTop/g3ZGUx1f8j+C5qsGK52viYBvtjuQ==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.523.0.tgz", + "integrity": "sha512-EyBwVoTNZrhLRIHly3JnLzy86deT2hHGoxSCrT3+cVcF1Pq3FPp6n9fUkHd6Yel+wFrjpXCRggLddPvajUoXtQ==", "dev": true, "dependencies": { - "@aws-sdk/client-sts": "3.515.0", - "@aws-sdk/types": "3.515.0", - "@smithy/property-provider": "^2.1.1", - "@smithy/types": "^2.9.1", + "@aws-sdk/client-sts": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4234,14 +4251,14 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.515.0.tgz", - "integrity": "sha512-I1MwWPzdRKM1luvdDdjdGsDjNVPhj9zaIytEchjTY40NcKOg+p2evLD2y69ozzg8pyXK63r8DdvDGOo9QPuh0A==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.523.0.tgz", + "integrity": "sha512-4g3q7Ta9sdD9TMUuohBAkbx/e3I/juTqfKi7TPgP+8jxcYX72MOsgemAMHuP6CX27eyj4dpvjH+w4SIVDiDSmg==", "dev": true, "dependencies": { - "@aws-sdk/types": "3.515.0", - "@smithy/protocol-http": "^3.1.1", - "@smithy/types": "^2.9.1", + "@aws-sdk/types": "3.523.0", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4249,13 +4266,13 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-logger": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.515.0.tgz", - "integrity": "sha512-qXomJzg2m/5seQOxHi/yOXOKfSjwrrJSmEmfwJKJyQgdMbBcjz3Cz0H/1LyC6c5hHm6a/SZgSTzDAbAoUmyL+Q==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.523.0.tgz", + "integrity": "sha512-PeDNJNhfiaZx54LBaLTXzUaJ9LXFwDFFIksipjqjvxMafnoVcQwKbkoPUWLe5ytT4nnL1LogD3s55mERFUsnwg==", "dev": true, "dependencies": { - "@aws-sdk/types": "3.515.0", - "@smithy/types": "^2.9.1", + "@aws-sdk/types": "3.523.0", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4263,14 +4280,14 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.515.0.tgz", - "integrity": "sha512-dokHLbTV3IHRIBrw9mGoxcNTnQsjlm7TpkJhPdGT9T4Mq399EyQo51u6IsVMm07RXLl2Zw7u+u9p+qWBFzmFRA==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.523.0.tgz", + "integrity": "sha512-nZ3Vt7ehfSDYnrcg/aAfjjvpdE+61B3Zk68i6/hSUIegT3IH9H1vSW67NDKVp+50hcEfzWwM2HMPXxlzuyFyrw==", "dev": true, "dependencies": { - "@aws-sdk/types": "3.515.0", - "@smithy/protocol-http": "^3.1.1", - "@smithy/types": "^2.9.1", + "@aws-sdk/types": "3.523.0", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4278,15 +4295,15 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.515.0.tgz", - "integrity": "sha512-nOqZjGA/GkjuJ5fUshec9Fv6HFd7ovOTxMJbw3MfAhqXuVZ6dKF41lpVJ4imNsgyFt3shUg9WDY8zGFjlYMB3g==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.523.0.tgz", + "integrity": "sha512-5OoKkmAPNaxLgJuS65gByW1QknGvvXdqzrIMXLsm9LjbsphTOscyvT439qk3Jf08TL4Zlw2x+pZMG7dZYuMAhQ==", "dev": true, "dependencies": { - "@aws-sdk/types": "3.515.0", - "@aws-sdk/util-endpoints": "3.515.0", - "@smithy/protocol-http": "^3.1.1", - "@smithy/types": "^2.9.1", + "@aws-sdk/types": "3.523.0", + "@aws-sdk/util-endpoints": "3.523.0", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4294,16 +4311,16 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.515.0.tgz", - "integrity": "sha512-RIRx9loxMgEAc/r1wPfnfShOuzn4RBi8pPPv6/jhhITEeMnJe6enAh2k5y9DdiVDDgCWZgVFSv0YkAIfzAFsnQ==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.523.0.tgz", + "integrity": "sha512-IypIAecBc8b4jM0uVBEj90NYaIsc0vuLdSFyH4LPO7is4rQUet4CkkD+S036NvDdcdxBsQ4hJZBmWrqiizMHhQ==", "dev": true, "dependencies": { - "@aws-sdk/types": "3.515.0", - "@smithy/node-config-provider": "^2.2.1", - "@smithy/types": "^2.9.1", + "@aws-sdk/types": "3.523.0", + "@smithy/node-config-provider": "^2.2.3", + "@smithy/types": "^2.10.1", "@smithy/util-config-provider": "^2.2.1", - "@smithy/util-middleware": "^2.1.1", + "@smithy/util-middleware": "^2.1.3", "tslib": "^2.5.0" }, "engines": { @@ -4311,16 +4328,16 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/token-providers": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.515.0.tgz", - "integrity": "sha512-MQuf04rIcTXqwDzmyHSpFPF1fKEzRl64oXtCRUF3ddxTdK6wxXkePfK6wNCuL+GEbEcJAoCtIGIRpzGPJvQjHA==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.523.0.tgz", + "integrity": "sha512-m3sPEnLuGV3JY9A8ytcz90SogVtjxEyIxUDFeswxY4C5wP/36yOq3ivenRu07dH+QIJnBhsQdjnHwJfrIetG6g==", "dev": true, "dependencies": { - "@aws-sdk/client-sso-oidc": "3.515.0", - "@aws-sdk/types": "3.515.0", - "@smithy/property-provider": "^2.1.1", - "@smithy/shared-ini-file-loader": "^2.3.1", - "@smithy/types": "^2.9.1", + "@aws-sdk/client-sso-oidc": "3.523.0", + "@aws-sdk/types": "3.523.0", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4328,12 +4345,12 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/types": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.515.0.tgz", - "integrity": "sha512-B3gUpiMlpT6ERaLvZZ61D0RyrQPsFYDkCncLPVkZOKkCOoFU46zi1o6T5JcYiz8vkx1q9RGloQ5exh79s5pU/w==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.523.0.tgz", + "integrity": "sha512-AqGIu4u+SxPiUuNBp2acCVcq80KDUFjxe6e3cMTvKWTzCbrVk1AXv0dAaJnCmdkWIha6zJDWxpIk/aL4EGhZ9A==", "dev": true, "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4341,14 +4358,14 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-endpoints": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.515.0.tgz", - "integrity": "sha512-UJi+jdwcGFV/F7d3+e2aQn5yZOVpDiAgfgNhPnEtgV0WozJ5/ZUeZBgWvSc/K415N4A4D/9cbBc7+I+35qzcDQ==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.523.0.tgz", + "integrity": "sha512-f4qe4AdafjAZoVGoVt69Jb2rXCgo306OOobSJ/f4bhQ0zgAjGELKJATNRRe0J7P28+ffmSxeuYwM3r4gDkD/QA==", "dev": true, "dependencies": { - "@aws-sdk/types": "3.515.0", - "@smithy/types": "^2.9.1", - "@smithy/util-endpoints": "^1.1.1", + "@aws-sdk/types": "3.523.0", + "@smithy/types": "^2.10.1", + "@smithy/util-endpoints": "^1.1.3", "tslib": "^2.5.0" }, "engines": { @@ -4356,26 +4373,26 @@ } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.515.0.tgz", - "integrity": "sha512-pTWQb0JCafTmLHLDv3Qqs/nAAJghcPdGQIBpsCStb0YEzg3At/dOi2AIQ683yYnXmeOxLXJDzmlsovfVObJScw==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.523.0.tgz", + "integrity": "sha512-6ZRNdGHX6+HQFqTbIA5+i8RWzxFyxsZv8D3soRfpdyWIKkzhSz8IyRKXRciwKBJDaC7OX2jzGE90wxRQft27nA==", "dev": true, "dependencies": { - "@aws-sdk/types": "3.515.0", - "@smithy/types": "^2.9.1", + "@aws-sdk/types": "3.523.0", + "@smithy/types": "^2.10.1", "bowser": "^2.11.0", "tslib": "^2.5.0" } }, "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.515.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.515.0.tgz", - "integrity": "sha512-A/KJ+/HTohHyVXLH+t/bO0Z2mPrQgELbQO8tX+B2nElo8uklj70r5cT7F8ETsI9oOy+HDVpiL5/v45ZgpUOiPg==", + "version": "3.523.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.523.0.tgz", + "integrity": "sha512-tW7vliJ77EsE8J1bzFpDYCiUyrw2NTcem+J5ddiWD4HA/xNQUyX0CMOXMBZCBA31xLTIchyz0LkZHlDsmB9LUw==", "dev": true, "dependencies": { - "@aws-sdk/types": "3.515.0", - "@smithy/node-config-provider": "^2.2.1", - "@smithy/types": "^2.9.1", + "@aws-sdk/types": "3.523.0", + "@smithy/node-config-provider": "^2.2.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -10262,48 +10279,308 @@ "path-parse": "^1.0.6" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", - "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", - "dev": true, - "dependencies": { - "eslint-scope": "5.1.1" + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 8" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 8" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 8" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/@pkgjs/parseargs": { @@ -10562,11 +10839,11 @@ "dev": true }, "node_modules/@smithy/abort-controller": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.1.tgz", - "integrity": "sha512-1+qdrUqLhaALYL0iOcN43EP6yAXXQ2wWZ6taf4S2pNGowmOc5gx+iMQv+E42JizNJjB0+gEadOXeV1Bf7JWL1Q==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.3.tgz", + "integrity": "sha512-c2aYH2Wu1RVE3rLlVgg2kQOBJGM0WbjReQi5DnPTm2Zb7F0gk7J2aeQeaX2u/lQZoHl6gv8Oac7mt9alU3+f4A==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -10591,14 +10868,14 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.1.1.tgz", - "integrity": "sha512-lxfLDpZm+AWAHPFZps5JfDoO9Ux1764fOgvRUBpHIO8HWHcSN1dkgsago1qLRVgm1BZ8RCm8cgv99QvtaOWIhw==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.1.4.tgz", + "integrity": "sha512-AW2WUZmBAzgO3V3ovKtsUbI3aBNMeQKFDumoqkNxaVDWF/xfnxAWqBKDr/NuG7c06N2Rm4xeZLPiJH/d+na0HA==", "dependencies": { - "@smithy/node-config-provider": "^2.2.1", - "@smithy/types": "^2.9.1", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/types": "^2.10.1", "@smithy/util-config-provider": "^2.2.1", - "@smithy/util-middleware": "^2.1.1", + "@smithy/util-middleware": "^2.1.3", "tslib": "^2.5.0" }, "engines": { @@ -10606,17 +10883,17 @@ } }, "node_modules/@smithy/core": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.2.tgz", - "integrity": "sha512-tYDmTp0f2TZVE18jAOH1PnmkngLQ+dOGUlMd1u67s87ieueNeyqhja6z/Z4MxhybEiXKOWFOmGjfTZWFxljwJw==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.5.tgz", + "integrity": "sha512-Rrc+e2Jj6Gu7Xbn0jvrzZlSiP2CZocIOfZ9aNUA82+1sa6GBnxqL9+iZ9EKHeD9aqD1nU8EK4+oN2EiFpSv7Yw==", "dependencies": { - "@smithy/middleware-endpoint": "^2.4.1", - "@smithy/middleware-retry": "^2.1.1", - "@smithy/middleware-serde": "^2.1.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/util-middleware": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.4", + "@smithy/middleware-retry": "^2.1.4", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", + "@smithy/util-middleware": "^2.1.3", "tslib": "^2.5.0" }, "engines": { @@ -10624,14 +10901,14 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.2.1.tgz", - "integrity": "sha512-7XHjZUxmZYnONheVQL7j5zvZXga+EWNgwEAP6OPZTi7l8J4JTeNh9aIOfE5fKHZ/ee2IeNOh54ZrSna+Vc6TFA==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.2.4.tgz", + "integrity": "sha512-DdatjmBZQnhGe1FhI8gO98f7NmvQFSDiZTwC3WMvLTCKQUY+Y1SVkhJqIuLu50Eb7pTheoXQmK+hKYUgpUWsNA==", "dependencies": { - "@smithy/node-config-provider": "^2.2.1", - "@smithy/property-provider": "^2.1.1", - "@smithy/types": "^2.9.1", - "@smithy/url-parser": "^2.1.1", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/property-provider": "^2.1.3", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", "tslib": "^2.5.0" }, "engines": { @@ -10639,12 +10916,12 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.1.1.tgz", - "integrity": "sha512-E8KYBxBIuU4c+zrpR22VsVrOPoEDzk35bQR3E+xm4k6Pa6JqzkDOdMyf9Atac5GPNKHJBdVaQ4JtjdWX2rl/nw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.1.3.tgz", + "integrity": "sha512-rGlCVuwSDv6qfKH4/lRxFjcZQnIE0LZ3D4lkMHg7ZSltK9rA74r0VuGSvWVQ4N/d70VZPaniFhp4Z14QYZsa+A==", "dependencies": { "@aws-crypto/crc32": "3.0.0", - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "@smithy/util-hex-encoding": "^2.1.1", "tslib": "^2.5.0" } @@ -10701,13 +10978,13 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.4.1.tgz", - "integrity": "sha512-VYGLinPsFqH68lxfRhjQaSkjXM7JysUOJDTNjHBuN/ykyRb2f1gyavN9+VhhPTWCy32L4yZ2fdhpCs/nStEicg==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.4.3.tgz", + "integrity": "sha512-Fn/KYJFo6L5I4YPG8WQb2hOmExgRmNpVH5IK2zU3JKrY5FKW7y9ar5e0BexiIC9DhSKqKX+HeWq/Y18fq7Dkpw==", "dependencies": { - "@smithy/protocol-http": "^3.1.1", - "@smithy/querystring-builder": "^2.1.1", - "@smithy/types": "^2.9.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/querystring-builder": "^2.1.3", + "@smithy/types": "^2.10.1", "@smithy/util-base64": "^2.1.1", "tslib": "^2.5.0" } @@ -10724,11 +11001,11 @@ } }, "node_modules/@smithy/hash-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.1.1.tgz", - "integrity": "sha512-Qhoq0N8f2OtCnvUpCf+g1vSyhYQrZjhSwvJ9qvR8BUGOtTXiyv2x1OD2e6jVGmlpC4E4ax1USHoyGfV9JFsACg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.1.3.tgz", + "integrity": "sha512-FsAPCUj7VNJIdHbSxMd5uiZiF20G2zdSDgrgrDrHqIs/VMxK85Vqk5kMVNNDMCZmMezp6UKnac0B4nAyx7HJ9g==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "@smithy/util-buffer-from": "^2.1.1", "@smithy/util-utf8": "^2.1.1", "tslib": "^2.5.0" @@ -10751,11 +11028,11 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.1.1.tgz", - "integrity": "sha512-7WTgnKw+VPg8fxu2v9AlNOQ5yaz6RA54zOVB4f6vQuR0xFKd+RzlCpt0WidYTsye7F+FYDIaS/RnJW4pxjNInw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.1.3.tgz", + "integrity": "sha512-wkra7d/G4CbngV4xsjYyAYOvdAhahQje/WymuQdVEnXFExJopEu7fbL5AEAlBPgWHXwu94VnCSG00gVzRfExyg==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" } }, @@ -10781,12 +11058,12 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.1.1.tgz", - "integrity": "sha512-rSr9ezUl9qMgiJR0UVtVOGEZElMdGFyl8FzWEF5iEKTlcWxGr2wTqGfDwtH3LAB7h+FPkxqv4ZU4cpuCN9Kf/g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.1.3.tgz", + "integrity": "sha512-aJduhkC+dcXxdnv5ZpM3uMmtGmVFKx412R1gbeykS5HXDmRU6oSsyy2SoHENCkfOGKAQOjVE2WVqDJibC0d21g==", "dependencies": { - "@smithy/protocol-http": "^3.1.1", - "@smithy/types": "^2.9.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -10794,16 +11071,16 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.4.1.tgz", - "integrity": "sha512-XPZTb1E2Oav60Ven3n2PFx+rX9EDsU/jSTA8VDamt7FXks67ekjPY/XrmmPDQaFJOTUHJNKjd8+kZxVO5Ael4Q==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.4.4.tgz", + "integrity": "sha512-4yjHyHK2Jul4JUDBo2sTsWY9UshYUnXeb/TAK/MTaPEb8XQvDmpwSFnfIRDU45RY1a6iC9LCnmJNg/yHyfxqkw==", "dependencies": { - "@smithy/middleware-serde": "^2.1.1", - "@smithy/node-config-provider": "^2.2.1", - "@smithy/shared-ini-file-loader": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/url-parser": "^2.1.1", - "@smithy/util-middleware": "^2.1.1", + "@smithy/middleware-serde": "^2.1.3", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/shared-ini-file-loader": "^2.3.4", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", + "@smithy/util-middleware": "^2.1.3", "tslib": "^2.5.0" }, "engines": { @@ -10811,17 +11088,17 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.1.1.tgz", - "integrity": "sha512-eMIHOBTXro6JZ+WWzZWd/8fS8ht5nS5KDQjzhNMHNRcG5FkNTqcKpYhw7TETMYzbLfhO5FYghHy1vqDWM4FLDA==", - "dependencies": { - "@smithy/node-config-provider": "^2.2.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/service-error-classification": "^2.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/util-middleware": "^2.1.1", - "@smithy/util-retry": "^2.1.1", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.1.4.tgz", + "integrity": "sha512-Cyolv9YckZTPli1EkkaS39UklonxMd08VskiuMhURDjC0HHa/AD6aK/YoD21CHv9s0QLg0WMLvk9YeLTKkXaFQ==", + "dependencies": { + "@smithy/node-config-provider": "^2.2.4", + "@smithy/protocol-http": "^3.2.1", + "@smithy/service-error-classification": "^2.1.3", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", + "@smithy/util-middleware": "^2.1.3", + "@smithy/util-retry": "^2.1.3", "tslib": "^2.5.0", "uuid": "^8.3.2" }, @@ -10838,11 +11115,11 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.1.1.tgz", - "integrity": "sha512-D8Gq0aQBeE1pxf3cjWVkRr2W54t+cdM2zx78tNrVhqrDykRA7asq8yVJij1u5NDtKzKqzBSPYh7iW0svUKg76g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.1.3.tgz", + "integrity": "sha512-s76LId+TwASrHhUa9QS4k/zeXDUAuNuddKklQzRgumbzge5BftVXHXIqL4wQxKGLocPwfgAOXWx+HdWhQk9hTg==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -10850,11 +11127,11 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.1.1.tgz", - "integrity": "sha512-KPJhRlhsl8CjgGXK/DoDcrFGfAqoqvuwlbxy+uOO4g2Azn1dhH+GVfC3RAp+6PoL5PWPb+vt6Z23FP+Mr6qeCw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.1.3.tgz", + "integrity": "sha512-opMFufVQgvBSld/b7mD7OOEBxF6STyraVr1xel1j0abVILM8ALJvRoFbqSWHGmaDlRGIiV9Q5cGbWi0sdiEaLQ==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -10862,13 +11139,13 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.1.tgz", - "integrity": "sha512-epzK3x1xNxA9oJgHQ5nz+2j6DsJKdHfieb+YgJ7ATWxzNcB7Hc+Uya2TUck5MicOPhDV8HZImND7ZOecVr+OWg==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.4.tgz", + "integrity": "sha512-nqazHCp8r4KHSFhRQ+T0VEkeqvA0U+RhehBSr1gunUuNW3X7j0uDrWBxB2gE9eutzy6kE3Y7L+Dov/UXT871vg==", "dependencies": { - "@smithy/property-provider": "^2.1.1", - "@smithy/shared-ini-file-loader": "^2.3.1", - "@smithy/types": "^2.9.1", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.4", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -10876,14 +11153,14 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.3.1.tgz", - "integrity": "sha512-gLA8qK2nL9J0Rk/WEZSvgin4AppvuCYRYg61dcUo/uKxvMZsMInL5I5ZdJTogOvdfVug3N2dgI5ffcUfS4S9PA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.4.1.tgz", + "integrity": "sha512-HCkb94soYhJMxPCa61wGKgmeKpJ3Gftx1XD6bcWEB2wMV1L9/SkQu/6/ysKBnbOzWRE01FGzwrTxucHypZ8rdg==", "dependencies": { - "@smithy/abort-controller": "^2.1.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/querystring-builder": "^2.1.1", - "@smithy/types": "^2.9.1", + "@smithy/abort-controller": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/querystring-builder": "^2.1.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -10891,11 +11168,11 @@ } }, "node_modules/@smithy/property-provider": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.1.tgz", - "integrity": "sha512-FX7JhhD/o5HwSwg6GLK9zxrMUrGnb3PzNBrcthqHKBc3dH0UfgEAU24xnJ8F0uow5mj17UeBEOI6o3CF2k7Mhw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.3.tgz", + "integrity": "sha512-bMz3se+ySKWNrgm7eIiQMa2HO/0fl2D0HvLAdg9pTMcpgp4SqOAh6bz7Ik6y7uQqSrk4rLjIKgbQ6yzYgGehCQ==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -10903,11 +11180,11 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.1.1.tgz", - "integrity": "sha512-6ZRTSsaXuSL9++qEwH851hJjUA0OgXdQFCs+VDw4tGH256jQ3TjYY/i34N4vd24RV3nrjNsgd1yhb57uMoKbzQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.2.1.tgz", + "integrity": "sha512-KLrQkEw4yJCeAmAH7hctE8g9KwA7+H2nSJwxgwIxchbp/L0B5exTdOQi9D5HinPLlothoervGmhpYKelZ6AxIA==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -10915,11 +11192,11 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.1.1.tgz", - "integrity": "sha512-C/ko/CeEa8jdYE4gt6nHO5XDrlSJ3vdCG0ZAc6nD5ZIE7LBp0jCx4qoqp7eoutBu7VrGMXERSRoPqwi1WjCPbg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.1.3.tgz", + "integrity": "sha512-kFD3PnNqKELe6m9GRHQw/ftFFSZpnSeQD4qvgDB6BQN6hREHELSosVFUMPN4M3MDKN2jAwk35vXHLoDrNfKu0A==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "@smithy/util-uri-escape": "^2.1.1", "tslib": "^2.5.0" }, @@ -10928,11 +11205,11 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.1.1.tgz", - "integrity": "sha512-H4+6jKGVhG1W4CIxfBaSsbm98lOO88tpDWmZLgkJpt8Zkk/+uG0FmmqMuCAc3HNM2ZDV+JbErxr0l5BcuIf/XQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.1.3.tgz", + "integrity": "sha512-3+CWJoAqcBMR+yvz6D+Fc5VdoGFtfenW6wqSWATWajrRMGVwJGPT3Vy2eb2bnMktJc4HU4bpjeovFa566P3knQ==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -10940,22 +11217,22 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.1.tgz", - "integrity": "sha512-txEdZxPUgM1PwGvDvHzqhXisrc5LlRWYCf2yyHfvITWioAKat7srQvpjMAvgzf0t6t7j8yHrryXU9xt7RZqFpw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.3.tgz", + "integrity": "sha512-iUrpSsem97bbXHHT/v3s7vaq8IIeMo6P6cXdeYHrx0wOJpMeBGQF7CB0mbJSiTm3//iq3L55JiEm8rA7CTVI8A==", "dependencies": { - "@smithy/types": "^2.9.1" + "@smithy/types": "^2.10.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.1.tgz", - "integrity": "sha512-2E2kh24igmIznHLB6H05Na4OgIEilRu0oQpYXo3LCNRrawHAcfDKq9004zJs+sAMt2X5AbY87CUCJ7IpqpSgdw==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.4.tgz", + "integrity": "sha512-CiZmPg9GeDKbKmJGEFvJBsJcFnh0AQRzOtQAzj1XEa8N/0/uSN/v1LYzgO7ry8hhO8+9KB7+DhSW0weqBra4Aw==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -10963,15 +11240,15 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.1.1.tgz", - "integrity": "sha512-Hb7xub0NHuvvQD3YwDSdanBmYukoEkhqBjqoxo+bSdC0ryV9cTfgmNjuAQhTPYB6yeU7hTR+sPRiFMlxqv6kmg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.1.3.tgz", + "integrity": "sha512-Jq4iPPdCmJojZTsPePn4r1ULShh6ONkokLuxp1Lnk4Sq7r7rJp4HlA1LbPBq4bD64TIzQezIpr1X+eh5NYkNxw==", "dependencies": { - "@smithy/eventstream-codec": "^2.1.1", + "@smithy/eventstream-codec": "^2.1.3", "@smithy/is-array-buffer": "^2.1.1", - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "@smithy/util-hex-encoding": "^2.1.1", - "@smithy/util-middleware": "^2.1.1", + "@smithy/util-middleware": "^2.1.3", "@smithy/util-uri-escape": "^2.1.1", "@smithy/util-utf8": "^2.1.1", "tslib": "^2.5.0" @@ -10981,15 +11258,15 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.3.1.tgz", - "integrity": "sha512-YsTdU8xVD64r2pLEwmltrNvZV6XIAC50LN6ivDopdt+YiF/jGH6PY9zUOu0CXD/d8GMB8gbhnpPsdrjAXHS9QA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.4.2.tgz", + "integrity": "sha512-ntAFYN51zu3N3mCd95YFcFi/8rmvm//uX+HnK24CRbI6k5Rjackn0JhgKz5zOx/tbNvOpgQIwhSX+1EvEsBLbA==", "dependencies": { - "@smithy/middleware-endpoint": "^2.4.1", - "@smithy/middleware-stack": "^2.1.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/types": "^2.9.1", - "@smithy/util-stream": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.4", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", + "@smithy/util-stream": "^2.1.3", "tslib": "^2.5.0" }, "engines": { @@ -10997,9 +11274,9 @@ } }, "node_modules/@smithy/types": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.9.1.tgz", - "integrity": "sha512-vjXlKNXyprDYDuJ7UW5iobdmyDm6g8dDG+BFUncAg/3XJaN45Gy5RWWWUVgrzIK7S4R1KWgIX5LeJcfvSI24bw==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", "dependencies": { "tslib": "^2.5.0" }, @@ -11008,12 +11285,12 @@ } }, "node_modules/@smithy/url-parser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.1.1.tgz", - "integrity": "sha512-qC9Bv8f/vvFIEkHsiNrUKYNl8uKQnn4BdhXl7VzQRP774AwIjiSMMwkbT+L7Fk8W8rzYVifzJNYxv1HwvfBo3Q==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.1.3.tgz", + "integrity": "sha512-X1NRA4WzK/ihgyzTpeGvI9Wn45y8HmqF4AZ/FazwAv8V203Ex+4lXqcYI70naX9ETqbqKVzFk88W6WJJzCggTQ==", "dependencies": { - "@smithy/querystring-parser": "^2.1.1", - "@smithy/types": "^2.9.1", + "@smithy/querystring-parser": "^2.1.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" } }, @@ -11072,13 +11349,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.1.1.tgz", - "integrity": "sha512-lqLz/9aWRO6mosnXkArtRuQqqZBhNpgI65YDpww4rVQBuUT7qzKbDLG5AmnQTCiU4rOquaZO/Kt0J7q9Uic7MA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.1.4.tgz", + "integrity": "sha512-J6XAVY+/g7jf03QMnvqPyU+8jqGrrtXoKWFVOS+n1sz0Lg8HjHJ1ANqaDN+KTTKZRZlvG8nU5ZrJOUL6VdwgcQ==", "dependencies": { - "@smithy/property-provider": "^2.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", + "@smithy/property-provider": "^2.1.3", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", "bowser": "^2.11.0", "tslib": "^2.5.0" }, @@ -11087,16 +11364,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.2.0.tgz", - "integrity": "sha512-iFJp/N4EtkanFpBUtSrrIbtOIBf69KNuve03ic1afhJ9/korDxdM0c6cCH4Ehj/smI9pDCfVv+bqT3xZjF2WaA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.2.3.tgz", + "integrity": "sha512-ttUISrv1uVOjTlDa3nznX33f0pthoUlP+4grhTvOzcLhzArx8qHB94/untGACOG3nlf8vU20nI2iWImfzoLkYA==", "dependencies": { - "@smithy/config-resolver": "^2.1.1", - "@smithy/credential-provider-imds": "^2.2.1", - "@smithy/node-config-provider": "^2.2.1", - "@smithy/property-provider": "^2.1.1", - "@smithy/smithy-client": "^2.3.1", - "@smithy/types": "^2.9.1", + "@smithy/config-resolver": "^2.1.4", + "@smithy/credential-provider-imds": "^2.2.4", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/property-provider": "^2.1.3", + "@smithy/smithy-client": "^2.4.2", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -11104,12 +11381,12 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.1.1.tgz", - "integrity": "sha512-sI4d9rjoaekSGEtq3xSb2nMjHMx8QXcz2cexnVyRWsy4yQ9z3kbDpX+7fN0jnbdOp0b3KSTZJZ2Yb92JWSanLw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.1.4.tgz", + "integrity": "sha512-/qAeHmK5l4yQ4/bCIJ9p49wDe9rwWtOzhPHblu386fwPNT3pxmodgcs9jDCV52yK9b4rB8o9Sj31P/7Vzka1cg==", "dependencies": { - "@smithy/node-config-provider": "^2.2.1", - "@smithy/types": "^2.9.1", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -11128,11 +11405,11 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.1.tgz", - "integrity": "sha512-mKNrk8oz5zqkNcbcgAAepeJbmfUW6ogrT2Z2gDbIUzVzNAHKJQTYmH9jcy0jbWb+m7ubrvXKb6uMjkSgAqqsFA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.3.tgz", + "integrity": "sha512-/+2fm7AZ2ozl5h8wM++ZP0ovE9/tiUUAHIbCfGfb3Zd3+Dyk17WODPKXBeJ/TnK5U+x743QmA0xHzlSm8I/qhw==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -11140,12 +11417,12 @@ } }, "node_modules/@smithy/util-retry": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.1.1.tgz", - "integrity": "sha512-Mg+xxWPTeSPrthpC5WAamJ6PW4Kbo01Fm7lWM1jmGRvmrRdsd3192Gz2fBXAMURyXpaNxyZf6Hr/nQ4q70oVEA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.1.3.tgz", + "integrity": "sha512-Kbvd+GEMuozbNUU3B89mb99tbufwREcyx2BOX0X2+qHjq6Gvsah8xSDDgxISDwcOHoDqUWO425F0Uc/QIRhYkg==", "dependencies": { - "@smithy/service-error-classification": "^2.1.1", - "@smithy/types": "^2.9.1", + "@smithy/service-error-classification": "^2.1.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -11153,13 +11430,13 @@ } }, "node_modules/@smithy/util-stream": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.1.1.tgz", - "integrity": "sha512-J7SMIpUYvU4DQN55KmBtvaMc7NM3CZ2iWICdcgaovtLzseVhAqFRYqloT3mh0esrFw+3VEK6nQFteFsTqZSECQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.1.3.tgz", + "integrity": "sha512-HvpEQbP8raTy9n86ZfXiAkf3ezp1c3qeeO//zGqwZdrfaoOpGKQgF2Sv1IqZp7wjhna7pvczWaGUHjcOPuQwKw==", "dependencies": { - "@smithy/fetch-http-handler": "^2.4.1", - "@smithy/node-http-handler": "^2.3.1", - "@smithy/types": "^2.9.1", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/types": "^2.10.1", "@smithy/util-base64": "^2.1.1", "@smithy/util-buffer-from": "^2.1.1", "@smithy/util-hex-encoding": "^2.1.1", @@ -11314,12 +11591,12 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-2.1.1.tgz", - "integrity": "sha512-kYy6BLJJNif+uqNENtJqWdXcpqo1LS+nj1AfXcDhOpqpSHJSAkVySLyZV9fkmuVO21lzGoxjvd1imGGJHph/IA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-2.1.3.tgz", + "integrity": "sha512-3R0wNFAQQoH9e4m+bVLDYNOst2qNxtxFgq03WoNHWTBOqQT3jFnOBRj1W51Rf563xDA5kwqjziksxn6RKkHB+Q==", "dependencies": { - "@smithy/abort-controller": "^2.1.1", - "@smithy/types": "^2.9.1", + "@smithy/abort-controller": "^2.1.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -14644,6 +14921,17 @@ "node": ">=8" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", @@ -19143,6 +19431,14 @@ "tslib": "^2.0.3" } }, + "node_modules/node-addon-api": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", + "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", + "engines": { + "node": "^16 || ^18 || >= 20" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -19178,16 +19474,6 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/node-gyp-build": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", - "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -23301,7 +23587,7 @@ }, "packages/auth-construct": { "name": "@aws-amplify/auth-construct-alpha", - "version": "0.6.0-beta.2", + "version": "0.6.0-beta.4", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", @@ -23316,18 +23602,18 @@ }, "packages/backend": { "name": "@aws-amplify/backend", - "version": "0.13.0-beta.3", + "version": "0.13.0-beta.5", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-auth": "^0.5.0-beta.2", - "@aws-amplify/backend-data": "^0.10.0-beta.2", - "@aws-amplify/backend-function": "^0.8.0-beta.1", + "@aws-amplify/backend-auth": "^0.5.0-beta.4", + "@aws-amplify/backend-data": "^0.10.0-beta.4", + "@aws-amplify/backend-function": "^0.8.0-beta.3", "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", "@aws-amplify/backend-output-storage": "^0.4.0-beta.1", "@aws-amplify/backend-secret": "^0.4.5-beta.1", - "@aws-amplify/backend-storage": "^0.6.0-beta.1", - "@aws-amplify/client-config": "^0.8.1-beta.2", - "@aws-amplify/data-schema": "^0.13.2", + "@aws-amplify/backend-storage": "^0.6.0-beta.3", + "@aws-amplify/client-config": "^0.9.0-beta.3", + "@aws-amplify/data-schema": "^0.13.15", "@aws-amplify/platform-core": "^0.5.0-beta.1", "@aws-amplify/plugin-types": "^0.9.0-beta.0", "@aws-sdk/client-amplify": "^3.465.0" @@ -23343,10 +23629,10 @@ }, "packages/backend-auth": { "name": "@aws-amplify/backend-auth", - "version": "0.5.0-beta.2", + "version": "0.5.0-beta.4", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.2", + "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.4", "@aws-amplify/backend-output-storage": "^0.4.0-beta.1", "@aws-amplify/plugin-types": "^0.9.0-beta.0" }, @@ -23361,18 +23647,18 @@ }, "packages/backend-data": { "name": "@aws-amplify/backend-data", - "version": "0.10.0-beta.2", + "version": "0.10.0-beta.4", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", "@aws-amplify/backend-output-storage": "^0.4.0-beta.1", "@aws-amplify/data-construct": "^1.4.1", - "@aws-amplify/data-schema-types": "^0.7.2", + "@aws-amplify/data-schema-types": "^0.7.8", "@aws-amplify/plugin-types": "^0.9.0-beta.0" }, "devDependencies": { "@aws-amplify/backend-platform-test-stubs": "^0.3.3-beta.0", - "@aws-amplify/data-schema": "^0.13.2", + "@aws-amplify/data-schema": "^0.13.15", "@aws-amplify/platform-core": "^0.5.0-beta.1" }, "peerDependencies": { @@ -23397,7 +23683,7 @@ }, "packages/backend-function": { "name": "@aws-amplify/backend-function", - "version": "0.8.0-beta.1", + "version": "0.8.0-beta.3", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", @@ -23464,7 +23750,7 @@ }, "packages/backend-storage": { "name": "@aws-amplify/backend-storage", - "version": "0.6.0-beta.1", + "version": "0.6.0-beta.3", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", @@ -23482,19 +23768,19 @@ }, "packages/cli": { "name": "@aws-amplify/backend-cli", - "version": "0.12.0-beta.3", + "version": "0.12.0-beta.5", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-deployer": "^0.5.1-beta.1", "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", "@aws-amplify/backend-secret": "^0.4.5-beta.1", - "@aws-amplify/cli-core": "^0.4.1-beta.0", - "@aws-amplify/client-config": "^0.8.1-beta.2", + "@aws-amplify/cli-core": "^0.5.0-beta.1", + "@aws-amplify/client-config": "^0.9.0-beta.3", "@aws-amplify/deployed-backend-client": "^0.4.0-beta.2", "@aws-amplify/form-generator": "^0.8.0-beta.1", "@aws-amplify/model-generator": "^0.4.1-beta.2", "@aws-amplify/platform-core": "^0.5.0-beta.1", - "@aws-amplify/sandbox": "^0.5.2-beta.2", + "@aws-amplify/sandbox": "^0.5.2-beta.4", "@aws-sdk/credential-provider-ini": "^3.465.0", "@aws-sdk/credential-providers": "^3.465.0", "@aws-sdk/region-config-resolver": "^3.465.0", @@ -23523,9 +23809,10 @@ }, "packages/cli-core": { "name": "@aws-amplify/cli-core", - "version": "0.4.1-beta.0", + "version": "0.5.0-beta.1", "license": "Apache-2.0", "dependencies": { + "@aws-amplify/platform-core": "^0.5.0-beta.1", "@inquirer/prompts": "^3.0.0", "execa": "^8.0.1", "kleur": "^4.1.5" @@ -23643,7 +23930,7 @@ }, "packages/client-config": { "name": "@aws-amplify/client-config", - "version": "0.8.1-beta.2", + "version": "0.9.0-beta.3", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", @@ -23661,10 +23948,10 @@ } }, "packages/create-amplify": { - "version": "0.6.1-beta.2", + "version": "0.7.0-beta.3", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/cli-core": "^0.4.1-beta.0", + "@aws-amplify/cli-core": "^0.5.0-beta.1", "@aws-amplify/platform-core": "^0.5.0-beta.1", "@aws-amplify/plugin-types": "^0.9.0-beta.0", "execa": "^8.0.1", @@ -23834,17 +24121,18 @@ }, "packages/integration-tests": { "name": "@aws-amplify/integration-tests", - "version": "0.4.4-beta.0", + "version": "0.5.0-beta.2", "license": "Apache-2.0", "devDependencies": { - "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.2", - "@aws-amplify/backend": "^0.13.0-beta.3", + "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.4", + "@aws-amplify/backend": "^0.13.0-beta.5", "@aws-amplify/backend-secret": "^0.4.5-beta.1", - "@aws-amplify/client-config": "^0.8.1-beta.2", - "@aws-amplify/data-schema": "^0.13.2", + "@aws-amplify/client-config": "^0.9.0-beta.3", + "@aws-amplify/data-schema": "^0.13.15", "@aws-amplify/platform-core": "^0.5.0-beta.1", "@aws-sdk/client-amplify": "^3.465.0", "@aws-sdk/client-cloudformation": "^3.465.0", + "@aws-sdk/client-iam": "^3.465.0", "@aws-sdk/client-lambda": "^3.465.0", "@aws-sdk/client-s3": "^3.465.0", "@aws-sdk/credential-providers": "^3.465.0", @@ -24421,19 +24709,19 @@ }, "packages/sandbox": { "name": "@aws-amplify/sandbox", - "version": "0.5.2-beta.2", + "version": "0.5.2-beta.4", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-deployer": "^0.5.1-beta.1", "@aws-amplify/backend-secret": "^0.4.5-beta.1", - "@aws-amplify/cli-core": "^0.4.1-beta.0", - "@aws-amplify/client-config": "^0.8.1-beta.2", + "@aws-amplify/cli-core": "^0.5.0-beta.1", + "@aws-amplify/client-config": "^0.9.0-beta.3", "@aws-amplify/deployed-backend-client": "^0.4.0-beta.2", "@aws-amplify/platform-core": "^0.5.0-beta.1", "@aws-sdk/client-cloudformation": "^3.465.0", "@aws-sdk/credential-providers": "^3.465.0", "@aws-sdk/types": "^3.465.0", - "@parcel/watcher": "2.1.0", + "@parcel/watcher": "^2.4.1", "debounce-promise": "^3.1.2", "glob": "^10.2.7", "open": "^9.1.0", @@ -24446,30 +24734,6 @@ "peerDependencies": { "aws-cdk": "^2.127.0" } - }, - "packages/sandbox/node_modules/@parcel/watcher": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.1.0.tgz", - "integrity": "sha512-8s8yYjd19pDSsBpbkOHnT6Z2+UJSuLQx61pCFM0s5wSRvKCEMDjd/cHY3/GI1szHIWbpXpsJdg3V6ISGGx9xDw==", - "hasInstallScript": true, - "dependencies": { - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^3.2.1", - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "packages/sandbox/node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" } } } diff --git a/packages/auth-construct/CHANGELOG.md b/packages/auth-construct/CHANGELOG.md index b1b2dcf00f..559131e387 100644 --- a/packages/auth-construct/CHANGELOG.md +++ b/packages/auth-construct/CHANGELOG.md @@ -1,5 +1,23 @@ # @aws-amplify/auth-construct-alpha +## 0.6.0-beta.5 + +### Patch Changes + +- @aws-amplify/backend-output-storage@0.4.0-beta.2 + +## 0.6.0-beta.4 + +### Patch Changes + +- 1d444df: Fix deployment bug with SAML providers. + +## 0.6.0-beta.3 + +### Patch Changes + +- c54625f: Update frontend config to output OIDC provider names instead of just 'OIDC'. + ## 0.6.0-beta.2 ### Minor Changes diff --git a/packages/auth-construct/package.json b/packages/auth-construct/package.json index 3f7cbf5e7e..8f4ebb1a25 100644 --- a/packages/auth-construct/package.json +++ b/packages/auth-construct/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/auth-construct-alpha", - "version": "0.6.0-beta.2", + "version": "0.6.0-beta.5", "type": "commonjs", "publishConfig": { "access": "public" @@ -19,7 +19,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", - "@aws-amplify/backend-output-storage": "^0.4.0-beta.1", + "@aws-amplify/backend-output-storage": "^0.4.0-beta.2", "@aws-amplify/plugin-types": "^0.9.0-beta.0", "@aws-sdk/util-arn-parser": "^3.465.0" }, diff --git a/packages/auth-construct/src/construct.test.ts b/packages/auth-construct/src/construct.test.ts index 7f14baf75c..d0e09c93ce 100644 --- a/packages/auth-construct/src/construct.test.ts +++ b/packages/auth-construct/src/construct.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, it, mock } from 'node:test'; import { AmplifyAuth } from './construct.js'; import { App, SecretValue, Stack } from 'aws-cdk-lib'; -import { Match, Template } from 'aws-cdk-lib/assertions'; +import { Template } from 'aws-cdk-lib/assertions'; import assert from 'node:assert'; import { BackendOutputEntry, @@ -1399,25 +1399,6 @@ void describe('Auth construct', () => { ProviderName: oidcProviderName, ProviderType: 'OIDC', }); - template.hasResourceProperties('AWS::Cognito::IdentityPool', { - OpenIdConnectProviderARNs: [ - Match.objectEquals({ - 'Fn::Join': [ - '', - [ - 'arn:aws:iam:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':oidc-provider/cognito-idp.', - { Ref: 'AWS::Region' }, - '.amazonaws.com/', - { Ref: 'testMyOidcProviderOidcIDP837BDEAD' }, - ], - ], - }), - ], - }); }); void it('oidc defaults to GET for oidc method', () => { const app = new App(); @@ -1482,25 +1463,6 @@ void describe('Auth construct', () => { ProviderName: oidcProviderName, ProviderType: 'OIDC', }); - template.hasResourceProperties('AWS::Cognito::IdentityPool', { - OpenIdConnectProviderARNs: [ - Match.objectEquals({ - 'Fn::Join': [ - '', - [ - 'arn:aws:iam:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':oidc-provider/cognito-idp.', - { Ref: 'AWS::Region' }, - '.amazonaws.com/', - { Ref: 'testMyOidcProviderOidcIDP837BDEAD' }, - ], - ], - }), - ], - }); }); void it('supports oidc and phone', () => { const app = new App(); @@ -1531,25 +1493,6 @@ void describe('Auth construct', () => { 'AWS::Cognito::UserPoolIdentityProvider', ExpectedOidcIDPProperties ); - template.hasResourceProperties('AWS::Cognito::IdentityPool', { - OpenIdConnectProviderARNs: [ - Match.objectEquals({ - 'Fn::Join': [ - '', - [ - 'arn:aws:iam:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':oidc-provider/cognito-idp.', - { Ref: 'AWS::Region' }, - '.amazonaws.com/', - { Ref: 'testMyOidcProviderOidcIDP837BDEAD' }, - ], - ], - }), - ], - }); }); void it('supports multiple oidc providers', () => { const app = new App(); @@ -1590,40 +1533,6 @@ void describe('Auth construct', () => { 'AWS::Cognito::UserPoolIdentityProvider', ExpectedOidcIDPProperties2 ); - template.hasResourceProperties('AWS::Cognito::IdentityPool', { - OpenIdConnectProviderARNs: [ - Match.objectEquals({ - 'Fn::Join': [ - '', - [ - 'arn:aws:iam:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':oidc-provider/cognito-idp.', - { Ref: 'AWS::Region' }, - '.amazonaws.com/', - { Ref: 'testMyOidcProviderOidcIDP837BDEAD' }, - ], - ], - }), - Match.objectEquals({ - 'Fn::Join': [ - '', - [ - 'arn:aws:iam:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':oidc-provider/cognito-idp.', - { Ref: 'AWS::Region' }, - '.amazonaws.com/', - { Ref: 'testMyOidcProvider2OidcIDP43D7B07B' }, - ], - ], - }), - ], - }); }); void it('supports saml and email', () => { const app = new App(); @@ -1653,23 +1562,6 @@ void describe('Auth construct', () => { 'AWS::Cognito::UserPoolIdentityProvider', ExpectedSAMLIDPProperties ); - template.hasResourceProperties('AWS::Cognito::IdentityPool', { - SamlProviderARNs: [ - Match.objectEquals({ - 'Fn::Join': [ - '', - [ - 'arn:aws:iam:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':saml-provider/', - { Ref: 'testSamlIDP7B98F3F4' }, - ], - ], - }), - ], - }); }); void it('supports saml and phone', () => { const app = new App(); @@ -1699,23 +1591,6 @@ void describe('Auth construct', () => { 'AWS::Cognito::UserPoolIdentityProvider', ExpectedSAMLIDPProperties ); - template.hasResourceProperties('AWS::Cognito::IdentityPool', { - SamlProviderARNs: [ - Match.objectEquals({ - 'Fn::Join': [ - '', - [ - 'arn:aws:iam:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':saml-provider/', - { Ref: 'testSamlIDP7B98F3F4' }, - ], - ], - }), - ], - }); }); void it('supports saml via URL and email', () => { const app = new App(); @@ -1745,23 +1620,6 @@ void describe('Auth construct', () => { 'AWS::Cognito::UserPoolIdentityProvider', ExpectedSAMLIDPViaURLProperties ); - template.hasResourceProperties('AWS::Cognito::IdentityPool', { - SamlProviderARNs: [ - Match.objectEquals({ - 'Fn::Join': [ - '', - [ - 'arn:aws:iam:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':saml-provider/', - { Ref: 'testSamlIDP7B98F3F4' }, - ], - ], - }), - ], - }); }); void it('supports additional oauth settings', () => { diff --git a/packages/auth-construct/src/construct.ts b/packages/auth-construct/src/construct.ts index 29738c499d..2e1287d9db 100644 --- a/packages/auth-construct/src/construct.ts +++ b/packages/auth-construct/src/construct.ts @@ -39,7 +39,6 @@ import { } from '@aws-amplify/backend-output-storage'; import * as path from 'path'; import { coreAttributeNameMap } from './string_maps.js'; -import { build as arnBuilder } from '@aws-sdk/util-arn-parser'; type DefaultRoles = { auth: Role; unAuth: Role }; type IdentityProviderSetupResult = { @@ -316,30 +315,6 @@ export class AmplifyAuth ]; // add other providers identityPool.supportedLoginProviders = providerSetupResult.oAuthMappings; - if (providerSetupResult.oidc) { - const oidcArns = []; - for (const oidcProvider of providerSetupResult.oidc) { - oidcArns.push( - arnBuilder({ - service: 'iam', - region, - accountId: Stack.of(this).account, - resource: `oidc-provider/cognito-idp.${region}.amazonaws.com/${oidcProvider.providerName}`, - }) - ); - } - identityPool.openIdConnectProviderArns = oidcArns; - } - if (providerSetupResult.saml) { - identityPool.samlProviderArns = [ - arnBuilder({ - service: 'iam', - region, - accountId: Stack.of(this).account, - resource: `saml-provider/${providerSetupResult.saml.providerName}`, - }), - ]; - } return { identityPool, identityPoolRoleAttachment, diff --git a/packages/backend-auth/API.md b/packages/backend-auth/API.md index 9bdc2e1b6d..e8322dbd7c 100644 --- a/packages/backend-auth/API.md +++ b/packages/backend-auth/API.md @@ -11,15 +11,23 @@ import { AuthResources } from '@aws-amplify/plugin-types'; import { AuthRoleName } from '@aws-amplify/plugin-types'; import { BackendSecret } from '@aws-amplify/plugin-types'; import { ConstructFactory } from '@aws-amplify/plugin-types'; +import { ConstructFactoryGetInstanceProps } from '@aws-amplify/plugin-types'; import { ExternalProviderOptions } from '@aws-amplify/auth-construct-alpha'; import { FacebookProviderProps } from '@aws-amplify/auth-construct-alpha'; import { FunctionResources } from '@aws-amplify/plugin-types'; import { GoogleProviderProps } from '@aws-amplify/auth-construct-alpha'; import { OidcProviderProps } from '@aws-amplify/auth-construct-alpha'; +import { ResourceAccessAcceptor } from '@aws-amplify/plugin-types'; import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types'; import { ResourceProvider } from '@aws-amplify/plugin-types'; import { TriggerEvent } from '@aws-amplify/auth-construct-alpha'; +// @public +export type ActionIam = 'addUserToGroup' | 'createUser' | 'deleteUser' | 'deleteUserAttributes' | 'disableUser' | 'enableUser' | 'forgetDevice' | 'getDevice' | 'getUser' | 'listDevices' | 'listGroupsForUser' | 'removeUserFromGroup' | 'resetUserPassword' | 'setUserMfaPreference' | 'setUserPassword' | 'setUserSettings' | 'updateDeviceStatus' | 'updateUserAttributes'; + +// @public +export type ActionMeta = 'manageUsers' | 'manageGroupMembership' | 'manageUserDevices' | 'managePasswordRecovery'; + // @public export type AmazonProviderFactoryProps = Omit & { clientId: BackendSecret; @@ -30,6 +38,7 @@ export type AmazonProviderFactoryProps = Omit & { loginWith: Expand; triggers?: Partial>>>; + access?: AuthAccessGenerator; }>; // @public @@ -40,13 +49,35 @@ export type AppleProviderFactoryProps = Omit) => AuthActionBuilder; +}; + +// @public (undocumented) +export type AuthAccessDefinition = { + getResourceAccessAcceptor: (getInstanceProps: ConstructFactoryGetInstanceProps) => ResourceAccessAcceptor; + actions: AuthAction[]; +}; + +// @public (undocumented) +export type AuthAccessGenerator = (allow: AuthAccessBuilder) => AuthAccessDefinition[]; + +// @public (undocumented) +export type AuthAction = ActionIam | ActionMeta; + +// @public (undocumented) +export type AuthActionBuilder = { + to: (actions: AuthAction[]) => AuthAccessDefinition; +}; + // @public export type AuthLoginWithFactoryProps = Omit & { externalProviders?: ExternalProviderSpecificFactoryProps; }; // @public (undocumented) -export type BackendAuth = ResourceProvider & ResourceAccessAcceptorFactory; +export type BackendAuth = ResourceProvider & ResourceAccessAcceptorFactory; // @public export const defineAuth: (props: AmplifyAuthProps) => ConstructFactory; diff --git a/packages/backend-auth/CHANGELOG.md b/packages/backend-auth/CHANGELOG.md index 1045a434a5..eca86d9c22 100644 --- a/packages/backend-auth/CHANGELOG.md +++ b/packages/backend-auth/CHANGELOG.md @@ -1,5 +1,33 @@ # @aws-amplify/backend-auth +## 0.5.0-beta.5 + +### Patch Changes + +- 937086b: require "resolution" in AmplifyUserError options + - @aws-amplify/backend-output-storage@0.4.0-beta.2 + - @aws-amplify/auth-construct-alpha@0.6.0-beta.5 + +## 0.5.0-beta.4 + +### Minor Changes + +- ab05ae0: attach policy & ssm params to acces userpool from auth resource +- f999897: Enable auth group access to storage and change syntax for specifying owner-based access + +### Patch Changes + +- Updated dependencies [1d444df] + - @aws-amplify/auth-construct-alpha@0.6.0-beta.4 + +## 0.5.0-beta.3 + +### Patch Changes + +- aee7501: limit defineAuth call to one +- Updated dependencies [c54625f] + - @aws-amplify/auth-construct-alpha@0.6.0-beta.3 + ## 0.5.0-beta.2 ### Minor Changes diff --git a/packages/backend-auth/package.json b/packages/backend-auth/package.json index 9e8e017eeb..072667cfe7 100644 --- a/packages/backend-auth/package.json +++ b/packages/backend-auth/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-auth", - "version": "0.5.0-beta.2", + "version": "0.5.0-beta.5", "type": "module", "publishConfig": { "access": "public" @@ -18,13 +18,13 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.2", - "@aws-amplify/backend-output-storage": "^0.4.0-beta.1", + "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.5", + "@aws-amplify/backend-output-storage": "^0.4.0-beta.2", "@aws-amplify/plugin-types": "^0.9.0-beta.0" }, "devDependencies": { "@aws-amplify/backend-platform-test-stubs": "^0.3.3-beta.0", - "@aws-amplify/platform-core": "^0.5.0-beta.1" + "@aws-amplify/platform-core": "^0.5.0-beta.2" }, "peerDependencies": { "aws-cdk-lib": "^2.127.0", diff --git a/packages/backend-auth/src/access_builder.test.ts b/packages/backend-auth/src/access_builder.test.ts new file mode 100644 index 0000000000..0faa45070e --- /dev/null +++ b/packages/backend-auth/src/access_builder.test.ts @@ -0,0 +1,57 @@ +import { describe, it, mock } from 'node:test'; +import { authAccessBuilder } from './access_builder.js'; +import { + ConstructContainer, + ConstructFactoryGetInstanceProps, + ResourceAccessAcceptorFactory, + ResourceProvider, +} from '@aws-amplify/plugin-types'; +import assert from 'node:assert'; + +void describe('allowAccessBuilder', () => { + const resourceAccessAcceptorMock = mock.fn(); + + const getResourceAccessAcceptorMock = mock.fn( + // allows us to get proper typing on the mock args + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (_: string) => resourceAccessAcceptorMock + ); + + const getConstructFactoryMock = mock.fn( + // this lets us get proper typing on the mock args + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (_: string) => ({ + getInstance: () => + ({ + getResourceAccessAcceptor: getResourceAccessAcceptorMock, + } as unknown as T), + }) + ); + + const stubGetInstanceProps = { + constructContainer: { + getConstructFactory: getConstructFactoryMock, + } as unknown as ConstructContainer, + } as unknown as ConstructFactoryGetInstanceProps; + + void it('builds access definition for resource', () => { + const accessDefinition = authAccessBuilder + .resource({ + getInstance: () => + ({ + getResourceAccessAcceptor: getResourceAccessAcceptorMock, + } as unknown as ResourceProvider & ResourceAccessAcceptorFactory), + }) + .to(['createUser', 'deleteUser', 'setUserPassword']); + + assert.deepStrictEqual(accessDefinition.actions, [ + 'createUser', + 'deleteUser', + 'setUserPassword', + ]); + assert.equal( + accessDefinition.getResourceAccessAcceptor(stubGetInstanceProps), + resourceAccessAcceptorMock + ); + }); +}); diff --git a/packages/backend-auth/src/access_builder.ts b/packages/backend-auth/src/access_builder.ts new file mode 100644 index 0000000000..0d0bf85751 --- /dev/null +++ b/packages/backend-auth/src/access_builder.ts @@ -0,0 +1,11 @@ +import { AuthAccessBuilder } from './types.js'; + +export const authAccessBuilder: AuthAccessBuilder = { + resource: (grantee) => ({ + to: (actions) => ({ + getResourceAccessAcceptor: (getInstanceProps) => + grantee.getInstance(getInstanceProps).getResourceAccessAcceptor(), + actions, + }), + }), +}; diff --git a/packages/backend-auth/src/auth_access_policy_arbiter.test.ts b/packages/backend-auth/src/auth_access_policy_arbiter.test.ts new file mode 100644 index 0000000000..8bdf9d9c6f --- /dev/null +++ b/packages/backend-auth/src/auth_access_policy_arbiter.test.ts @@ -0,0 +1,132 @@ +import { beforeEach, describe, it, mock } from 'node:test'; +import { App, Stack } from 'aws-cdk-lib'; +import { + BackendOutputEntry, + BackendOutputStorageStrategy, + ConstructContainer, + ConstructFactoryGetInstanceProps, + ImportPathVerifier, +} from '@aws-amplify/plugin-types'; +import { + ConstructContainerStub, + ImportPathVerifierStub, + StackResolverStub, +} from '@aws-amplify/backend-platform-test-stubs'; +import { StackMetadataBackendOutputStorageStrategy } from '@aws-amplify/backend-output-storage'; +import { UserPool } from 'aws-cdk-lib/aws-cognito'; +import { AuthAccessPolicyArbiter } from './auth_access_policy_arbiter.js'; +import assert from 'node:assert'; +import { UserPoolAccessPolicyFactory } from './userpool_access_policy_factory.js'; + +void describe('AuthAccessPolicyArbiter', () => { + void describe('arbitratePolicies', () => { + let stack: Stack; + let constructContainer: ConstructContainer; + let outputStorageStrategy: BackendOutputStorageStrategy; + let importPathVerifier: ImportPathVerifier; + let getInstanceProps: ConstructFactoryGetInstanceProps; + + beforeEach(() => { + stack = createStackAndSetContext(); + + constructContainer = new ConstructContainerStub( + new StackResolverStub(stack) + ); + + outputStorageStrategy = new StackMetadataBackendOutputStorageStrategy( + stack + ); + + importPathVerifier = new ImportPathVerifierStub(); + + getInstanceProps = { + constructContainer, + outputStorageStrategy, + importPathVerifier, + }; + }); + + void it('passes expected policy and ssm context to resource access acceptor', () => { + const userpool = new UserPool(stack, 'testUserPool'); + const acceptResourceAccessMock = mock.fn(); + const authAccessPolicyArbiter = new AuthAccessPolicyArbiter( + [ + { + actions: ['manageUsers'], + getResourceAccessAcceptor: () => ({ + identifier: 'testResourceAccessAcceptor', + acceptResourceAccess: acceptResourceAccessMock, + }), + }, + { + actions: ['deleteUser', 'disableUser', 'deleteUserAttributes'], + getResourceAccessAcceptor: () => ({ + identifier: 'testResourceAccessAcceptor', + acceptResourceAccess: acceptResourceAccessMock, + }), + }, + ], + getInstanceProps, + [{ name: 'TEST_USERPOOL_ID', path: 'test/ssm/path/to/userpool/id' }], + new UserPoolAccessPolicyFactory(userpool) + ); + + authAccessPolicyArbiter.arbitratePolicies(); + assert.equal(acceptResourceAccessMock.mock.callCount(), 2); + assert.deepStrictEqual( + acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: [ + 'cognito-idp:AdminConfirmSignUp', + 'cognito-idp:AdminCreateUser', + 'cognito-idp:AdminDeleteUser', + 'cognito-idp:AdminDeleteUserAttributes', + 'cognito-idp:AdminDisableUser', + 'cognito-idp:AdminEnableUser', + 'cognito-idp:AdminGetUser', + 'cognito-idp:AdminListGroupsForUser', + 'cognito-idp:AdminRespondToAuthChallenge', + 'cognito-idp:AdminSetUserMFAPreference', + 'cognito-idp:AdminSetUserSettings', + 'cognito-idp:AdminUpdateUserAttributes', + 'cognito-idp:AdminUserGlobalSignOut', + ], + Effect: 'Allow', + Resource: `${userpool.userPoolArn}`, + }, + ], + Version: '2012-10-17', + } + ); + assert.deepStrictEqual( + acceptResourceAccessMock.mock.calls[1].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: [ + 'cognito-idp:AdminDeleteUser', + 'cognito-idp:AdminDisableUser', + 'cognito-idp:AdminDeleteUserAttributes', + ], + Effect: 'Allow', + Resource: `${userpool.userPoolArn}`, + }, + ], + Version: '2012-10-17', + } + ); + assert.deepStrictEqual( + acceptResourceAccessMock.mock.calls[0].arguments[1], + [{ name: 'TEST_USERPOOL_ID', path: 'test/ssm/path/to/userpool/id' }] + ); + }); + }); +}); + +const createStackAndSetContext = (): Stack => { + const app = new App(); + const stack = new Stack(app); + return stack; +}; diff --git a/packages/backend-auth/src/auth_access_policy_arbiter.ts b/packages/backend-auth/src/auth_access_policy_arbiter.ts new file mode 100644 index 0000000000..ad8b3b690b --- /dev/null +++ b/packages/backend-auth/src/auth_access_policy_arbiter.ts @@ -0,0 +1,58 @@ +import { + ConstructFactoryGetInstanceProps, + SsmEnvironmentEntry, +} from '@aws-amplify/plugin-types'; +import { AuthAccessDefinition } from './types.js'; +import { UserPoolAccessPolicyFactory } from './userpool_access_policy_factory.js'; + +/** + * Middleman between creating bucket policies and attaching those policies to corresponding roles + */ +export class AuthAccessPolicyArbiter { + /** + * Instantiate with context from the auth factory + */ + constructor( + private readonly accessDefinition: AuthAccessDefinition[], + private readonly getInstanceProps: ConstructFactoryGetInstanceProps, + private readonly ssmEnvironmentEntries: SsmEnvironmentEntry[], + private readonly userPoolAccessPolicyFactory: UserPoolAccessPolicyFactory + ) {} + + /** + * Responsible for creating policies corresponding to the definition, + * then invoking the corresponding ResourceAccessAcceptor to accept the policies + */ + arbitratePolicies = () => { + this.accessDefinition.forEach(this.acceptResourceAccess); + }; + + acceptResourceAccess = (accessDefinition: AuthAccessDefinition) => { + const accessAcceptor = accessDefinition.getResourceAccessAcceptor( + this.getInstanceProps + ); + const policy = this.userPoolAccessPolicyFactory.createPolicy( + accessDefinition.actions + ); + + accessAcceptor.acceptResourceAccess(policy, this.ssmEnvironmentEntries); + }; +} + +/** + * + */ +export class AuthAccessPolicyArbiterFactory { + getInstance = ( + accessDefinition: AuthAccessDefinition[], + getInstanceProps: ConstructFactoryGetInstanceProps, + ssmEnvironmentEntries: SsmEnvironmentEntry[], + userpoolAccessPolicyFactory: UserPoolAccessPolicyFactory + ) => + new AuthAccessPolicyArbiter( + accessDefinition, + getInstanceProps, + ssmEnvironmentEntries, + userpoolAccessPolicyFactory + ); +} diff --git a/packages/backend-auth/src/factory.test.ts b/packages/backend-auth/src/factory.test.ts index 5dc3bcc16e..e71f4d1eea 100644 --- a/packages/backend-auth/src/factory.test.ts +++ b/packages/backend-auth/src/factory.test.ts @@ -11,6 +11,7 @@ import { ConstructFactoryGetInstanceProps, FunctionResources, ImportPathVerifier, + ResourceAccessAcceptorFactory, ResourceProvider, } from '@aws-amplify/plugin-types'; import { triggerEvents } from '@aws-amplify/auth-construct-alpha'; @@ -113,6 +114,68 @@ void describe('AmplifyAuthFactory', () => { ); }); + void it('if access is defined, it should attach valid policy to the resource', () => { + const mockAcceptResourceAccess = mock.fn(); + const lambdaResourceStub = { + getInstance: () => ({ + getResourceAccessAcceptor: () => ({ + acceptResourceAccess: mockAcceptResourceAccess, + }), + }), + } as unknown as ConstructFactory< + ResourceProvider & ResourceAccessAcceptorFactory + >; + + resetFactoryCount(); + + authFactory = defineAuth({ + loginWith: { email: true }, + access: (allow) => [ + allow.resource(lambdaResourceStub).to(['managePasswordRecovery']), + allow.resource(lambdaResourceStub).to(['createUser']), + ], + }); + + const backendAuth = authFactory.getInstance(getInstanceProps); + + assert.equal(mockAcceptResourceAccess.mock.callCount(), 2); + assert.ok( + mockAcceptResourceAccess.mock.calls[0].arguments[0] instanceof Policy + ); + assert.deepStrictEqual( + mockAcceptResourceAccess.mock.calls[0].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: [ + 'cognito-idp:AdminResetUserPassword', + 'cognito-idp:AdminSetUserPassword', + ], + Effect: 'Allow', + Resource: backendAuth.resources.userPool.userPoolArn, + }, + ], + Version: '2012-10-17', + } + ); + assert.ok( + mockAcceptResourceAccess.mock.calls[1].arguments[0] instanceof Policy + ); + assert.deepStrictEqual( + mockAcceptResourceAccess.mock.calls[1].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: 'cognito-idp:AdminCreateUser', + Effect: 'Allow', + Resource: backendAuth.resources.userPool.userPoolArn, + }, + ], + Version: '2012-10-17', + } + ); + }); + triggerEvents.forEach((event) => { void it(`resolves ${event} trigger and attaches handler to auth construct`, () => { const funcStub: ConstructFactory> = { diff --git a/packages/backend-auth/src/factory.ts b/packages/backend-auth/src/factory.ts index 529fb7eb17..95d98208d7 100644 --- a/packages/backend-auth/src/factory.ts +++ b/packages/backend-auth/src/factory.ts @@ -1,3 +1,7 @@ +import * as path from 'path'; +import { Policy } from 'aws-cdk-lib/aws-iam'; +import { UserPool, UserPoolOperation } from 'aws-cdk-lib/aws-cognito'; +import { AmplifyUserError } from '@aws-amplify/platform-core'; import { AmplifyAuth, AuthProps, @@ -15,15 +19,18 @@ import { ResourceAccessAcceptorFactory, ResourceProvider, } from '@aws-amplify/plugin-types'; -import * as path from 'path'; -import { AuthLoginWithFactoryProps, Expand } from './types.js'; import { translateToAuthConstructLoginWith } from './translate_auth_props.js'; -import { Policy } from 'aws-cdk-lib/aws-iam'; -import { UserPool, UserPoolOperation } from 'aws-cdk-lib/aws-cognito'; -import { AmplifyUserError } from '@aws-amplify/platform-core'; +import { authAccessBuilder as _authAccessBuilder } from './access_builder.js'; +import { AuthAccessPolicyArbiterFactory } from './auth_access_policy_arbiter.js'; +import { + AuthAccessGenerator, + AuthLoginWithFactoryProps, + Expand, +} from './types.js'; +import { UserPoolAccessPolicyFactory } from './userpool_access_policy_factory.js'; export type BackendAuth = ResourceProvider & - ResourceAccessAcceptorFactory; + ResourceAccessAcceptorFactory; export type AmplifyAuthProps = Expand< Omit & { @@ -40,6 +47,13 @@ export type AmplifyAuthProps = Expand< ConstructFactory> > >; + /** + * !EXPERIMENTAL! + * + * Access control is under active development and is subject to change without notice. + * Use at your own risk and do not use in production + */ + access?: AuthAccessGenerator; } >; @@ -98,12 +112,15 @@ class AmplifyAuthGenerator implements ConstructContainerEntryGenerator { constructor( private readonly props: AmplifyAuthProps, - private readonly getInstanceProps: ConstructFactoryGetInstanceProps + private readonly getInstanceProps: ConstructFactoryGetInstanceProps, + private readonly authAccessBuilder = _authAccessBuilder, + private readonly authAccessPolicyArbiterFactory = new AuthAccessPolicyArbiterFactory() ) {} generateContainerEntry = ({ scope, backendSecretResolver, + ssmEnvironmentEntriesGenerator, }: GenerateContainerEntryProps) => { const authProps: AuthProps = { ...this.props, @@ -126,20 +143,62 @@ class AmplifyAuthGenerator implements ConstructContainerEntryGenerator { const authConstructMixin: BackendAuth = { ...authConstruct, + /** + * Returns a resourceAccessAcceptor for the given role + * @param roleIdentifier Either the auth or unauth role name or the name of a UserPool group + */ getResourceAccessAcceptor: ( - roleName: AuthRoleName + roleIdentifier: AuthRoleName | string ): ResourceAccessAcceptor => ({ - identifier: `${roleName}ResourceAccessAcceptor`, + identifier: `${roleIdentifier}ResourceAccessAcceptor`, acceptResourceAccess: (policy: Policy) => { - const role = authConstruct.resources[roleName]; + const role = roleNameIsAuthRoleName(roleIdentifier) + ? authConstruct.resources[roleIdentifier] + : authConstruct.resources.groups?.[roleIdentifier]?.role; + if (!role) { + throw new AmplifyUserError('InvalidResourceAccessConfig', { + message: `No auth IAM role found for "${roleIdentifier}".`, + resolution: `If you are trying to configure UserPool group access, ensure that the group name is specified correctly.`, + }); + } policy.attachToRole(role); }, }), }; + if (!this.props.access) { + return authConstructMixin; + } + // props.access is the access callback defined by the customer + // here we inject the authAccessBuilder into the callback and run it + // this produces the access definition that will be used to create the auth access policies + const accessDefinition = this.props.access(this.authAccessBuilder); + + const ssmEnvironmentEntries = + ssmEnvironmentEntriesGenerator.generateSsmEnvironmentEntries({ + [`${this.defaultName}_USERPOOL_ID`]: + authConstructMixin.resources.userPool.userPoolId, + }); + + const authPolicyArbiter = this.authAccessPolicyArbiterFactory.getInstance( + accessDefinition, + this.getInstanceProps, + ssmEnvironmentEntries, + new UserPoolAccessPolicyFactory(authConstruct.resources.userPool) + ); + + authPolicyArbiter.arbitratePolicies(); + return authConstructMixin; }; } +const roleNameIsAuthRoleName = (roleName: string): roleName is AuthRoleName => { + return ( + roleName === 'authenticatedUserIamRole' || + roleName === 'unauthenticatedUserIamRole' + ); +}; + /** * Provide the settings that will be used for authentication. */ diff --git a/packages/backend-auth/src/types.ts b/packages/backend-auth/src/types.ts index a42d3657ab..54cddb8b13 100644 --- a/packages/backend-auth/src/types.ts +++ b/packages/backend-auth/src/types.ts @@ -7,7 +7,14 @@ import { GoogleProviderProps, OidcProviderProps, } from '@aws-amplify/auth-construct-alpha'; -import { BackendSecret } from '@aws-amplify/plugin-types'; +import { + BackendSecret, + ConstructFactory, + ConstructFactoryGetInstanceProps, + ResourceAccessAcceptor, + ResourceAccessAcceptorFactory, + ResourceProvider, +} from '@aws-amplify/plugin-types'; /** * This utility allows us to expand nested types in auto complete prompts. @@ -175,3 +182,60 @@ export type AuthLoginWithFactoryProps = Omit< */ externalProviders?: ExternalProviderSpecificFactoryProps; }; + +export type AuthAccessBuilder = { + resource: ( + other: ConstructFactory + ) => AuthActionBuilder; +}; + +export type AuthActionBuilder = { + to: (actions: AuthAction[]) => AuthAccessDefinition; +}; + +export type AuthAccessGenerator = ( + allow: AuthAccessBuilder +) => AuthAccessDefinition[]; + +export type AuthAccessDefinition = { + getResourceAccessAcceptor: ( + getInstanceProps: ConstructFactoryGetInstanceProps + ) => ResourceAccessAcceptor; + + // list of auth actions you can perform on the resource + actions: AuthAction[]; +}; + +export type AuthAction = ActionIam | ActionMeta; + +/** @todo https://github.com/aws-amplify/amplify-backend/issues/1111 */ +export type ActionMeta = + | 'manageUsers' + | 'manageGroupMembership' + | 'manageUserDevices' + | 'managePasswordRecovery'; + +/** + * This maps to Cognito IAM actions. + * @todo https://github.com/aws-amplify/amplify-backend/issues/1111 + * @see https://aws.permissions.cloud/iam/cognito-idp + */ +export type ActionIam = + | 'addUserToGroup' + | 'createUser' + | 'deleteUser' + | 'deleteUserAttributes' + | 'disableUser' + | 'enableUser' + | 'forgetDevice' + | 'getDevice' + | 'getUser' + | 'listDevices' + | 'listGroupsForUser' + | 'removeUserFromGroup' + | 'resetUserPassword' + | 'setUserMfaPreference' + | 'setUserPassword' + | 'setUserSettings' + | 'updateDeviceStatus' + | 'updateUserAttributes'; diff --git a/packages/backend-auth/src/userpool_access_policy_factory.test.ts b/packages/backend-auth/src/userpool_access_policy_factory.test.ts new file mode 100644 index 0000000000..d387828436 --- /dev/null +++ b/packages/backend-auth/src/userpool_access_policy_factory.test.ts @@ -0,0 +1,65 @@ +import { beforeEach, describe, it } from 'node:test'; +import assert from 'node:assert'; +import { App, Stack } from 'aws-cdk-lib'; +import { UserPool } from 'aws-cdk-lib/aws-cognito'; +import { UserPoolAccessPolicyFactory } from './userpool_access_policy_factory.js'; +import { Template } from 'aws-cdk-lib/assertions'; +import { AccountPrincipal, Policy, Role } from 'aws-cdk-lib/aws-iam'; + +void describe('UserPoolAccessPolicyFactory', () => { + let userpool: UserPool; + let stack: Stack; + let factory: UserPoolAccessPolicyFactory; + + beforeEach(() => { + ({ stack, userpool } = createStackAndUserpool()); + factory = new UserPoolAccessPolicyFactory(userpool); + }); + + void it('throws if no permissions are specified', () => { + assert.throws(() => factory.createPolicy([])); + }); + + void it('returns policy with specified iam actions', () => { + const policy = factory.createPolicy([ + 'createUser', + 'updateUserAttributes', + 'deleteUserAttributes', + ]); + + // we have to attach the policy to a role, otherwise CDK erases the policy from the stack + policy.attachToRole( + new Role(stack, 'testRole', { assumedBy: new AccountPrincipal('1234') }) + ); + + assert.ok(policy instanceof Policy); + + const template = Template.fromStack(Stack.of(userpool)); + + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'cognito-idp:AdminCreateUser', + 'cognito-idp:AdminUpdateUserAttributes', + 'cognito-idp:AdminDeleteUserAttributes', + ], + Resource: { + 'Fn::GetAtt': ['testUserpool0DDFA854', 'Arn'], + }, + }, + ], + }, + }); + }); +}); + +const createStackAndUserpool = (): { stack: Stack; userpool: UserPool } => { + const app = new App(); + const stack = new Stack(app); + return { + stack, + userpool: new UserPool(stack, 'testUserpool'), + }; +}; diff --git a/packages/backend-auth/src/userpool_access_policy_factory.ts b/packages/backend-auth/src/userpool_access_policy_factory.ts new file mode 100644 index 0000000000..535a17d950 --- /dev/null +++ b/packages/backend-auth/src/userpool_access_policy_factory.ts @@ -0,0 +1,111 @@ +import { IUserPool } from 'aws-cdk-lib/aws-cognito'; +import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { Stack } from 'aws-cdk-lib'; +import { AmplifyFault, AmplifyUserError } from '@aws-amplify/platform-core'; +import { AuthAction } from './types.js'; + +/** + * Generates IAM policies scoped to a single userpool. + */ +export class UserPoolAccessPolicyFactory { + private readonly namePrefix = 'userpoolAccess'; + private readonly stack: Stack; + + private policyCount = 1; + + /** + * Instantiate with the userpool to generate policies for + */ + constructor(private readonly userpool: IUserPool) { + this.stack = Stack.of(userpool); + } + + createPolicy = (actions: AuthAction[]) => { + if (actions.length === 0) { + throw new AmplifyUserError('EmptyPolicyError', { + message: 'At least one action must be specified.', + resolution: + 'Ensure all resource access rules specify at least one action.', + }); + } + + const policyActions = new Set( + actions.flatMap((action) => iamActionMap[action]) + ); + + if (policyActions.size === 0) { + throw new AmplifyFault('EmptyPolicyFault', { + message: 'Failed to construct valid policy to access UserPool', + }); + } + + const policy = new Policy( + this.stack, + `${this.namePrefix}${this.policyCount++}`, + { + statements: [ + new PolicyStatement({ + actions: [...policyActions], + resources: [this.userpool.userPoolArn], + }), + ], + } + ); + + return policy; + }; +} + +type IamActionMap = { + [action in AuthAction]: string[]; +}; + +const iamActionMap: IamActionMap = { + manageUsers: [ + 'cognito-idp:AdminConfirmSignUp', + 'cognito-idp:AdminCreateUser', + 'cognito-idp:AdminDeleteUser', + 'cognito-idp:AdminDeleteUserAttributes', + 'cognito-idp:AdminDisableUser', + 'cognito-idp:AdminEnableUser', + 'cognito-idp:AdminGetUser', + 'cognito-idp:AdminListGroupsForUser', + 'cognito-idp:AdminRespondToAuthChallenge', + 'cognito-idp:AdminSetUserMFAPreference', + 'cognito-idp:AdminSetUserSettings', + 'cognito-idp:AdminUpdateUserAttributes', + 'cognito-idp:AdminUserGlobalSignOut', + ], + manageGroupMembership: [ + 'cognito-idp:AdminAddUserToGroup', + 'cognito-idp:AdminRemoveUserFromGroup', + ], + manageUserDevices: [ + 'cognito-idp:AdminForgetDevice', + 'cognito-idp:AdminGetDevice', + 'cognito-idp:AdminListDevices', + 'cognito-idp:AdminUpdateDeviceStatus', + ], + managePasswordRecovery: [ + 'cognito-idp:AdminResetUserPassword', + 'cognito-idp:AdminSetUserPassword', + ], + addUserToGroup: ['cognito-idp:AdminAddUserToGroup'], + createUser: ['cognito-idp:AdminCreateUser'], + deleteUser: ['cognito-idp:AdminDeleteUser'], + deleteUserAttributes: ['cognito-idp:AdminDeleteUserAttributes'], + disableUser: ['cognito-idp:AdminDisableUser'], + enableUser: ['cognito-idp:AdminEnableUser'], + forgetDevice: ['cognito-idp:AdminForgetDevice'], + getDevice: ['cognito-idp:AdminGetDevice'], + getUser: ['cognito-idp:AdminGetUser'], + listDevices: ['cognito-idp:AdminListDevices'], + listGroupsForUser: ['cognito-idp:AdminListGroupsForUser'], + removeUserFromGroup: ['cognito-idp:AdminRemoveUserFromGroup'], + resetUserPassword: ['cognito-idp:AdminResetUserPassword'], + setUserMfaPreference: ['cognito-idp:AdminSetUserMFAPreference'], + setUserPassword: ['cognito-idp:AdminSetUserPassword'], + setUserSettings: ['cognito-idp:AdminSetUserSettings'], + updateDeviceStatus: ['cognito-idp:AdminUpdateDeviceStatus'], + updateUserAttributes: ['cognito-idp:AdminUpdateUserAttributes'], +}; diff --git a/packages/backend-data/API.md b/packages/backend-data/API.md index f3da1be476..9ec1d61879 100644 --- a/packages/backend-data/API.md +++ b/packages/backend-data/API.md @@ -21,7 +21,6 @@ export type AuthorizationModes = { apiKeyAuthorizationMode?: ApiKeyAuthorizationModeProps; lambdaAuthorizationMode?: LambdaAuthorizationModeProps; oidcAuthorizationMode?: OIDCAuthorizationModeProps; - allowListedRoleNames?: string[]; }; // @public diff --git a/packages/backend-data/CHANGELOG.md b/packages/backend-data/CHANGELOG.md index 49184e16d7..2d131b8d2f 100644 --- a/packages/backend-data/CHANGELOG.md +++ b/packages/backend-data/CHANGELOG.md @@ -1,5 +1,31 @@ # @aws-amplify/backend-data +## 0.10.0-beta.5 + +### Minor Changes + +- 91dae55: remove allowListedRoleNames from defineData + +### Patch Changes + +- 26cdffd: backend-data: add support for first-class defineFunction +- 937086b: require "resolution" in AmplifyUserError options + - @aws-amplify/backend-output-storage@0.4.0-beta.2 + +## 0.10.0-beta.4 + +### Minor Changes + +- a777488: plumb function access definition from schema into IAM policies attached to the functions +- 268acd8: feat: enable destructive schema updates in amplify sandbox + +## 0.10.0-beta.3 + +### Patch Changes + +- 912034e: limit defineData call to one +- 7857f0a: backend-data: add js resolver support + ## 0.10.0-beta.2 ### Minor Changes diff --git a/packages/backend-data/package.json b/packages/backend-data/package.json index 7ea364a482..8f8ad7d28b 100644 --- a/packages/backend-data/package.json +++ b/packages/backend-data/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-data", - "version": "0.10.0-beta.2", + "version": "0.10.0-beta.5", "type": "module", "publishConfig": { "access": "public" @@ -18,19 +18,19 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-amplify/data-schema": "^0.13.2", + "@aws-amplify/data-schema": "^0.13.15", "@aws-amplify/backend-platform-test-stubs": "^0.3.3-beta.0", - "@aws-amplify/platform-core": "^0.5.0-beta.1" + "@aws-amplify/platform-core": "^0.5.0-beta.2" }, "peerDependencies": { "aws-cdk-lib": "^2.127.0", "constructs": "^10.0.0" }, "dependencies": { - "@aws-amplify/backend-output-storage": "^0.4.0-beta.1", + "@aws-amplify/backend-output-storage": "^0.4.0-beta.2", "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", "@aws-amplify/data-construct": "^1.4.1", "@aws-amplify/plugin-types": "^0.9.0-beta.0", - "@aws-amplify/data-schema-types": "^0.7.2" + "@aws-amplify/data-schema-types": "^0.7.8" } } diff --git a/packages/backend-data/src/app_sync_policy_generator.test.ts b/packages/backend-data/src/app_sync_policy_generator.test.ts new file mode 100644 index 0000000000..969e3227a4 --- /dev/null +++ b/packages/backend-data/src/app_sync_policy_generator.test.ts @@ -0,0 +1,147 @@ +import { beforeEach, describe, it } from 'node:test'; +import { + AppSyncApiAction, + AppSyncPolicyGenerator, +} from './app_sync_policy_generator.js'; +import { App, Stack } from 'aws-cdk-lib'; +import { GraphqlApi } from 'aws-cdk-lib/aws-appsync'; +import { AccountPrincipal, Role } from 'aws-cdk-lib/aws-iam'; +import { Template } from 'aws-cdk-lib/assertions'; + +void describe('AppSyncPolicyGenerator', () => { + let stack: Stack; + let graphqlApi: GraphqlApi; + + beforeEach(() => { + const app = new App(); + stack = new Stack(app, 'testStack'); + graphqlApi = new GraphqlApi(stack, 'testApi', { + name: 'testName', + definition: { + schema: { + bind: () => ({ + apiId: 'testApi', + definition: 'test schema', + }), + }, + }, + }); + }); + const singleActionTestCases: { + action: AppSyncApiAction; + expectedResourceSuffix: string; + }[] = [ + { + action: 'query', + expectedResourceSuffix: 'Query/*', + }, + { + action: 'mutate', + expectedResourceSuffix: 'Mutation/*', + }, + { + action: 'listen', + expectedResourceSuffix: 'Subscription/*', + }, + ]; + + singleActionTestCases.forEach(({ action, expectedResourceSuffix }) => { + void it(`generates policy for ${action} action`, () => { + const policyGenerator = new AppSyncPolicyGenerator(graphqlApi); + + const queryPolicy = policyGenerator.generateGraphqlAccessPolicy([action]); + + // we have to attach the policy to a role, otherwise CDK erases the policy from the stack + queryPolicy.attachToRole( + new Role(stack, 'testRole', { + assumedBy: new AccountPrincipal('1234'), + }) + ); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testApiD6ECAB50', 'Arn'], + }, + `/types/${expectedResourceSuffix}`, + ], + ], + }, + }, + ], + }, + }); + }); + }); + + void it('generates policy for multiple actions', () => { + const policyGenerator = new AppSyncPolicyGenerator(graphqlApi); + + const queryPolicy = policyGenerator.generateGraphqlAccessPolicy([ + 'query', + 'mutate', + 'listen', + ]); + + // we have to attach the policy to a role, otherwise CDK erases the policy from the stack + queryPolicy.attachToRole( + new Role(stack, 'testRole', { + assumedBy: new AccountPrincipal('1234'), + }) + ); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: [ + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testApiD6ECAB50', 'Arn'], + }, + `/types/Query/*`, + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testApiD6ECAB50', 'Arn'], + }, + `/types/Mutation/*`, + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testApiD6ECAB50', 'Arn'], + }, + `/types/Subscription/*`, + ], + ], + }, + ], + }, + ], + }, + }); + }); +}); diff --git a/packages/backend-data/src/app_sync_policy_generator.ts b/packages/backend-data/src/app_sync_policy_generator.ts new file mode 100644 index 0000000000..9962d1922e --- /dev/null +++ b/packages/backend-data/src/app_sync_policy_generator.ts @@ -0,0 +1,47 @@ +import { Stack } from 'aws-cdk-lib'; +import { IGraphqlApi } from 'aws-cdk-lib/aws-appsync'; +import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; + +export type AppSyncApiAction = 'query' | 'mutate' | 'listen'; + +/** + * Generates policies for accessing an AppSync GraphQL API + */ +export class AppSyncPolicyGenerator { + private readonly stack: Stack; + private readonly policyPrefix = 'GraphqlAccessPolicy'; + private policyCount = 1; + /** + * Initialize with the GraphqlAPI that the policies will be scoped to + */ + constructor(private readonly graphqlApi: IGraphqlApi) { + this.stack = Stack.of(graphqlApi); + } + /** + * Generates a policy that grants GraphQL data-plane access to the provided actions + * + * The naming is a bit wonky here because the IAM action is always "appsync:GraphQL". + * The input "action" maps to the "type" in the resource name part of the ARN which is "Query", "Mutation" or "Subscription" + */ + generateGraphqlAccessPolicy(actions: AppSyncApiAction[]) { + const resources = actions + // convert from actions to GraphQL Type + .map((action) => actionToTypeMap[action]) + // convert Type to resourceName + .map((type) => [this.graphqlApi.arn, 'types', type, '*'].join('/')); + return new Policy(this.stack, `${this.policyPrefix}${this.policyCount++}`, { + statements: [ + new PolicyStatement({ + actions: ['appsync:GraphQL'], + resources, + }), + ], + }); + } +} + +const actionToTypeMap: Record = { + query: 'Query', + mutate: 'Mutation', + listen: 'Subscription', +}; diff --git a/packages/backend-data/src/assets/js_resolver_handler.ts b/packages/backend-data/src/assets/js_resolver_handler.ts new file mode 100644 index 0000000000..282dfb725d --- /dev/null +++ b/packages/backend-data/src/assets/js_resolver_handler.ts @@ -0,0 +1,12 @@ +/** + * Pipeline resolver request handler + */ +export const request = () => { + return {}; +}; +/** + * Pipeline resolver response handler + */ +export const response = (ctx: Record>) => { + return ctx.prev.result; +}; diff --git a/packages/backend-data/src/assets/tsconfig.json b/packages/backend-data/src/assets/tsconfig.json new file mode 100644 index 0000000000..9ff585e7a7 --- /dev/null +++ b/packages/backend-data/src/assets/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./", + "outDir": "../../lib/assets", + "inlineSources": false, + "sourceMap": false, + "inlineSourceMap": false + } +} diff --git a/packages/backend-data/src/convert_authorization_modes.test.ts b/packages/backend-data/src/convert_authorization_modes.test.ts index a50668c9b7..84fd09e87f 100644 --- a/packages/backend-data/src/convert_authorization_modes.test.ts +++ b/packages/backend-data/src/convert_authorization_modes.test.ts @@ -11,12 +11,12 @@ import { convertAuthorizationModesToCDK, isUsingDefaultApiKeyAuth, } from './convert_authorization_modes.js'; -import { Code, Function, IFunction, Runtime } from 'aws-cdk-lib/aws-lambda'; -import { FunctionInstanceProvider } from './convert_functions.js'; +import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'; import { AmplifyFunction, AuthResources, ConstructFactory, + ConstructFactoryGetInstanceProps, ResourceProvider, } from '@aws-amplify/plugin-types'; @@ -60,13 +60,10 @@ void describe('convertAuthorizationModesToCDK', () => { let authenticatedUserRole: IRole; let unauthenticatedUserRole: IRole; let providedAuthConfig: ProvidedAuthConfig; - let functionInstanceProvider: FunctionInstanceProvider; + const getInstancePropsStub: ConstructFactoryGetInstanceProps = + {} as unknown as ConstructFactoryGetInstanceProps; void beforeEach(() => { - functionInstanceProvider = { - provide: (func: ConstructFactory): IFunction => - func as unknown as IFunction, - }; stack = new Stack(); userPool = new UserPool(stack, 'TestPool'); authenticatedUserRole = Role.fromRoleName(stack, 'AuthRole', 'MyAuthRole'); @@ -92,7 +89,7 @@ void describe('convertAuthorizationModesToCDK', () => { assert.deepStrictEqual( convertAuthorizationModesToCDK( - functionInstanceProvider, + getInstancePropsStub, undefined, undefined ), @@ -114,7 +111,7 @@ void describe('convertAuthorizationModesToCDK', () => { assert.deepStrictEqual( convertAuthorizationModesToCDK( - functionInstanceProvider, + getInstancePropsStub, providedAuthConfig, undefined ), @@ -147,7 +144,7 @@ void describe('convertAuthorizationModesToCDK', () => { assert.deepStrictEqual( convertAuthorizationModesToCDK( - functionInstanceProvider, + getInstancePropsStub, undefined, authModes ), @@ -172,7 +169,7 @@ void describe('convertAuthorizationModesToCDK', () => { assert.deepStrictEqual( convertAuthorizationModesToCDK( - functionInstanceProvider, + getInstancePropsStub, undefined, authModes ), @@ -195,9 +192,6 @@ void describe('convertAuthorizationModesToCDK', () => { }, }), }; - functionInstanceProvider = { - provide: (): IFunction => authFn, - }; const authModes: AuthorizationModes = { lambdaAuthorizationMode: { @@ -215,57 +209,13 @@ void describe('convertAuthorizationModesToCDK', () => { assert.deepStrictEqual( convertAuthorizationModesToCDK( - functionInstanceProvider, + getInstancePropsStub, undefined, authModes ), expectedOutput ); }); - - void it('allows for specifying allow listed roles with an empty list', () => { - const authModes: AuthorizationModes = { - allowListedRoleNames: [], - }; - - const expectedOutput: CDKAuthorizationModes = { - userPoolConfig: { userPool }, - iamConfig: { - identityPoolId, - authenticatedUserRole, - unauthenticatedUserRole, - allowListedRoles: [], - }, - }; - - assert.deepStrictEqual( - convertAuthorizationModesToCDK( - functionInstanceProvider, - providedAuthConfig, - authModes - ), - expectedOutput - ); - }); - - void it('allows for specifying allow listed roles roles with values specified', () => { - const authModes: AuthorizationModes = { - allowListedRoleNames: ['MyAdminRole', 'MyQARole'], - }; - - const convertedOutput = convertAuthorizationModesToCDK( - functionInstanceProvider, - providedAuthConfig, - authModes - ); - - assert.equal(convertedOutput.iamConfig?.allowListedRoles?.length, 2); - assert.equal( - convertedOutput.iamConfig?.allowListedRoles?.[0], - 'MyAdminRole' - ); - assert.equal(convertedOutput.iamConfig?.allowListedRoles?.[1], 'MyQARole'); - }); }); void describe('isUsingDefaultApiKeyAuth', () => { diff --git a/packages/backend-data/src/convert_authorization_modes.ts b/packages/backend-data/src/convert_authorization_modes.ts index b0e7e64741..bbd94a88f4 100644 --- a/packages/backend-data/src/convert_authorization_modes.ts +++ b/packages/backend-data/src/convert_authorization_modes.ts @@ -16,8 +16,11 @@ import { LambdaAuthorizationModeProps, OIDCAuthorizationModeProps, } from './types.js'; -import { FunctionInstanceProvider } from './convert_functions.js'; -import { AuthResources, ResourceProvider } from '@aws-amplify/plugin-types'; +import { + AuthResources, + ConstructFactoryGetInstanceProps, + ResourceProvider, +} from '@aws-amplify/plugin-types'; const DEFAULT_API_KEY_EXPIRATION_DAYS = 7; const DEFAULT_LAMBDA_AUTH_TIME_TO_LIVE_SECONDS = 60; @@ -63,13 +66,13 @@ const convertApiKeyAuthConfigToCDK = ({ * Convert to CDK LambdaAuthorizationConfig. */ const convertLambdaAuthorizationConfigToCDK = ( - functionInstanceProvider: FunctionInstanceProvider, + getInstanceProps: ConstructFactoryGetInstanceProps, { function: authFn, timeToLiveInSeconds = DEFAULT_LAMBDA_AUTH_TIME_TO_LIVE_SECONDS, }: LambdaAuthorizationModeProps ): CDKLambdaAuthorizationConfig => ({ - function: functionInstanceProvider.provide(authFn), + function: authFn.getInstance(getInstanceProps).resources.lambda, ttl: Duration.seconds(timeToLiveInSeconds), }); @@ -121,14 +124,15 @@ const computeUserPoolAuthFromResource = ( */ const computeIAMAuthFromResource = ( providedAuthConfig: ProvidedAuthConfig | undefined, - authModes: AuthorizationModes | undefined + authModes: AuthorizationModes | undefined, + additionalRoles: IRole[] = [] ): CDKIAMAuthorizationConfig | undefined => { if (providedAuthConfig) { return { authenticatedUserRole: providedAuthConfig.authenticatedUserRole, unauthenticatedUserRole: providedAuthConfig.unauthenticatedUserRole, identityPoolId: providedAuthConfig.identityPoolId, - allowListedRoles: authModes?.allowListedRoleNames ?? [], + allowListedRoles: additionalRoles, }; } return; @@ -168,9 +172,10 @@ const convertAuthorizationModeToCDK = (mode?: DefaultAuthorizationMode) => { * Convert to CDK AuthorizationModes. */ export const convertAuthorizationModesToCDK = ( - functionInstanceProvider: FunctionInstanceProvider, + getInstanceProps: ConstructFactoryGetInstanceProps, authResources: ProvidedAuthConfig | undefined, - authModes: AuthorizationModes | undefined + authModes: AuthorizationModes | undefined, + additionalRoles: IRole[] = [] ): CDKAuthorizationModes => { const defaultAuthorizationMode = authModes?.defaultAuthorizationMode ?? @@ -182,10 +187,14 @@ export const convertAuthorizationModesToCDK = ( ? convertApiKeyAuthConfigToCDK(authModes.apiKeyAuthorizationMode) : computeApiKeyAuthFromResource(authResources, authModes); const userPoolConfig = computeUserPoolAuthFromResource(authResources); - const iamConfig = computeIAMAuthFromResource(authResources, authModes); + const iamConfig = computeIAMAuthFromResource( + authResources, + authModes, + additionalRoles + ); const lambdaConfig = authModes?.lambdaAuthorizationMode ? convertLambdaAuthorizationConfigToCDK( - functionInstanceProvider, + getInstanceProps, authModes.lambdaAuthorizationMode ) : undefined; diff --git a/packages/backend-data/src/convert_functions.test.ts b/packages/backend-data/src/convert_functions.test.ts index d432b1f3ea..1c404d3107 100644 --- a/packages/backend-data/src/convert_functions.test.ts +++ b/packages/backend-data/src/convert_functions.test.ts @@ -1,80 +1,25 @@ -import { beforeEach, describe, it, mock } from 'node:test'; +import { describe, it } from 'node:test'; import assert from 'node:assert'; import { Stack } from 'aws-cdk-lib'; -import { Code, Function, IFunction, Runtime } from 'aws-cdk-lib/aws-lambda'; +import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'; import { AmplifyFunction, - BackendOutputEntry, - BackendOutputStorageStrategy, - ConstructContainer, ConstructFactory, + ConstructFactoryGetInstanceProps, } from '@aws-amplify/plugin-types'; -import { - FunctionInstanceProvider, - buildConstructFactoryFunctionInstanceProvider, - convertFunctionNameMapToCDK, -} from './convert_functions.js'; -import { - ConstructContainerStub, - StackResolverStub, -} from '@aws-amplify/backend-platform-test-stubs'; -import { StackMetadataBackendOutputStorageStrategy } from '@aws-amplify/backend-output-storage'; - -void describe('buildConstructFactoryFunctionInstanceProvider', () => { - let stack: Stack; - let constructContainer: ConstructContainer; - let outputStorageStrategy: BackendOutputStorageStrategy; - let functionInstanceProvider: FunctionInstanceProvider; - - beforeEach(() => { - stack = new Stack(); - const stackResolverStub = new StackResolverStub(stack); - constructContainer = new ConstructContainerStub(stackResolverStub); - outputStorageStrategy = new StackMetadataBackendOutputStorageStrategy( - stack - ); - - functionInstanceProvider = buildConstructFactoryFunctionInstanceProvider({ - constructContainer, - outputStorageStrategy, - }); - }); - - void it('provides for an AmplifyFunctionFactory', async () => { - const originalFn = new Function(stack, 'MyFnLambdaFunction', { - runtime: Runtime.NODEJS_18_X, - code: Code.fromInline( - 'module.handler = async () => console.log("Hello");' - ), - handler: 'index.handler', - }); - const myFn: ConstructFactory = { - getInstance: () => ({ - resources: { - lambda: originalFn, - }, - }), - }; - - assert.deepStrictEqual(functionInstanceProvider.provide(myFn), originalFn); - }); -}); +import { convertFunctionNameMapToCDK } from './convert_functions.js'; void describe('convertFunctionNameMapToCDK', () => { - const functionInstanceProvider = { - provide: mock.fn( - (func: ConstructFactory) => func as unknown as IFunction - ), - }; + const getInstancePropsStub = + {} as unknown as ConstructFactoryGetInstanceProps; void it('can be invoked with empty input', () => { const convertedOutput = convertFunctionNameMapToCDK( - functionInstanceProvider, + getInstancePropsStub, {} ); assert.equal(Object.keys(convertedOutput).length, 0); - assert.equal(functionInstanceProvider.provide.mock.calls.length, 0); }); void it('can be invoked with input entries, and invokes factoryInstanceProvider', () => { @@ -108,23 +53,13 @@ void describe('convertFunctionNameMapToCDK', () => { }), }; - const convertedOutput = convertFunctionNameMapToCDK( - functionInstanceProvider, - { - echo, - update, - } - ); + const convertedOutput = convertFunctionNameMapToCDK(getInstancePropsStub, { + echo, + update, + }); assert.equal(Object.keys(convertedOutput).length, 2); - assert.equal(functionInstanceProvider.provide.mock.calls.length, 2); - assert.equal( - functionInstanceProvider.provide.mock.calls[0].arguments[0], - echo - ); - assert.equal( - functionInstanceProvider.provide.mock.calls[1].arguments[0], - update - ); + assert.strictEqual(convertedOutput.echo, echoFn); + assert.strictEqual(convertedOutput.update, updateFn); }); }); diff --git a/packages/backend-data/src/convert_functions.ts b/packages/backend-data/src/convert_functions.ts index 6ff98e7bbf..9f941399c1 100644 --- a/packages/backend-data/src/convert_functions.ts +++ b/packages/backend-data/src/convert_functions.ts @@ -5,33 +5,16 @@ import { ConstructFactoryGetInstanceProps, } from '@aws-amplify/plugin-types'; -/** - * Type used for function provider injection while transforming data props. - */ -export type FunctionInstanceProvider = { - provide: (func: ConstructFactory) => IFunction; -}; - -/** - * Build a function instance provider using the construct factory. - */ -export const buildConstructFactoryFunctionInstanceProvider = ( - props: ConstructFactoryGetInstanceProps -) => ({ - provide: (func: ConstructFactory): IFunction => - func.getInstance(props).resources.lambda, -}); - /** * Convert the provided function input map into a map of IFunctions. */ export const convertFunctionNameMapToCDK = ( - functionInstanceProvider: FunctionInstanceProvider, + getInstanceProps: ConstructFactoryGetInstanceProps, functions: Record> ): Record => Object.fromEntries( Object.entries(functions).map(([functionName, functionInput]) => [ functionName, - functionInstanceProvider.provide(functionInput), + functionInput.getInstance(getInstanceProps).resources.lambda, ]) ); diff --git a/packages/backend-data/src/convert_js_resolvers.test.ts b/packages/backend-data/src/convert_js_resolvers.test.ts new file mode 100644 index 0000000000..3b402407fd --- /dev/null +++ b/packages/backend-data/src/convert_js_resolvers.test.ts @@ -0,0 +1,161 @@ +import { Template } from 'aws-cdk-lib/assertions'; +import assert from 'node:assert'; +import { beforeEach, describe, it } from 'node:test'; +import { App, Duration, Stack } from 'aws-cdk-lib'; +import { + AmplifyData, + AmplifyDataDefinition, +} from '@aws-amplify/data-construct'; +import { resolve } from 'path'; +import { fileURLToPath } from 'url'; +import { convertJsResolverDefinition } from './convert_js_resolvers.js'; +import { a } from '@aws-amplify/data-schema'; + +// stub schema for the AmplifyApi construct +// not relevant to this test suite +const testSchema = /* GraphQL */ ` + type Todo @model { + id: ID! + } +`; + +const createStackAndSetContext = (): Stack => { + const app = new App(); + app.node.setContext('amplify-backend-name', 'testEnvName'); + app.node.setContext('amplify-backend-namespace', 'testBackendId'); + app.node.setContext('amplify-backend-type', 'branch'); + const stack = new Stack(app); + return stack; +}; + +void describe('convertJsResolverDefinition', () => { + let stack: Stack; + let amplifyApi: AmplifyData; + const authorizationModes = { apiKeyConfig: { expires: Duration.days(7) } }; + + void beforeEach(() => { + stack = createStackAndSetContext(); + amplifyApi = new AmplifyData(stack, 'amplifyData', { + apiName: 'amplifyData', + definition: AmplifyDataDefinition.fromString(testSchema), + authorizationModes, + }); + }); + + void it('handles empty array / undefined param', () => { + assert.doesNotThrow(() => + convertJsResolverDefinition(stack, amplifyApi, undefined) + ); + assert.doesNotThrow(() => + convertJsResolverDefinition(stack, amplifyApi, []) + ); + }); + + void it('handles jsFunction IR with a single function', () => { + const absolutePath = resolve( + fileURLToPath(import.meta.url), + '../../lib/assets', + 'js_resolver_handler.js' + ); + + const schema = a.schema({ + customQuery: a + .query() + .authorization([a.allow.public()]) + .returns(a.string()) + .handler( + a.handler.custom({ + entry: absolutePath, + }) + ), + }); + const { jsFunctions } = schema.transform(); + convertJsResolverDefinition(stack, amplifyApi, jsFunctions); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::AppSync::FunctionConfiguration', { + Runtime: { + Name: 'APPSYNC_JS', + RuntimeVersion: '1.0.0', + }, + DataSourceName: 'NONE_DS', + Name: 'Fn_Query_customQuery_1', + }); + + const expectedFnCount = 1; + template.resourceCountIs( + 'AWS::AppSync::FunctionConfiguration', + expectedFnCount + ); + + template.hasResourceProperties('AWS::AppSync::Resolver', { + Runtime: { + Name: 'APPSYNC_JS', + RuntimeVersion: '1.0.0', + }, + Kind: 'PIPELINE', + TypeName: 'Query', + FieldName: 'customQuery', + }); + + template.resourceCountIs('AWS::AppSync::Resolver', 1); + }); + + void it('handles jsFunction IR with multiple functions', () => { + const absolutePath = resolve( + fileURLToPath(import.meta.url), + '../../lib/assets', + 'js_resolver_handler.js' + ); + + const schema = a.schema({ + customQuery: a + .query() + .authorization([a.allow.public()]) + .returns(a.string()) + .handler([ + a.handler.custom({ + entry: absolutePath, + }), + a.handler.custom({ + entry: absolutePath, + }), + a.handler.custom({ + entry: absolutePath, + }), + ]), + }); + const { jsFunctions } = schema.transform(); + convertJsResolverDefinition(stack, amplifyApi, jsFunctions); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::AppSync::FunctionConfiguration', { + Runtime: { + Name: 'APPSYNC_JS', + RuntimeVersion: '1.0.0', + }, + DataSourceName: 'NONE_DS', + Name: 'Fn_Query_customQuery_1', + }); + + const expectedFnCount = 3; + template.resourceCountIs( + 'AWS::AppSync::FunctionConfiguration', + expectedFnCount + ); + + template.hasResourceProperties('AWS::AppSync::Resolver', { + Runtime: { + Name: 'APPSYNC_JS', + RuntimeVersion: '1.0.0', + }, + Kind: 'PIPELINE', + TypeName: 'Query', + FieldName: 'customQuery', + }); + + template.resourceCountIs('AWS::AppSync::Resolver', 1); + }); +}); diff --git a/packages/backend-data/src/convert_js_resolvers.ts b/packages/backend-data/src/convert_js_resolvers.ts new file mode 100644 index 0000000000..a71c3bc061 --- /dev/null +++ b/packages/backend-data/src/convert_js_resolvers.ts @@ -0,0 +1,109 @@ +import { Construct } from 'constructs'; +import { AmplifyData } from '@aws-amplify/data-construct'; +import { CfnFunctionConfiguration, CfnResolver } from 'aws-cdk-lib/aws-appsync'; +import { FilePathExtractor } from '@aws-amplify/platform-core'; +import { JsResolver, JsResolverEntry } from '@aws-amplify/data-schema-types'; +import { dirname, join, resolve } from 'path'; +import { fileURLToPath } from 'url'; +import { Asset } from 'aws-cdk-lib/aws-s3-assets'; + +const APPSYNC_PIPELINE_RESOLVER = 'PIPELINE'; +const APPSYNC_JS_RUNTIME_NAME = 'APPSYNC_JS'; +const APPSYNC_JS_RUNTIME_VERSION = '1.0.0'; +const JS_PIPELINE_RESOLVER_HANDLER = './assets/js_resolver_handler.js'; + +/** + * Resolve JS resolver function entry to absolute path + */ +const resolveEntryPath = (entry: JsResolverEntry): string => { + const unresolvedImportLocationError = new Error( + 'Could not determine import path to construct absolute code path from relative path. Consider using an absolute path instead.' + ); + + if (typeof entry === 'string') { + return entry; + } + + const filePath = new FilePathExtractor(entry.importLine).extract(); + if (filePath) { + return join(dirname(filePath), entry.relativePath); + } + + throw unresolvedImportLocationError; +}; + +/** + * + * This returns the top-level passthrough resolver request/response handler (see: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#anatomy-of-a-pipeline-resolver-js) + * It's required for defining a pipeline resolver. The only purpose it serves is returning the output of the last function in the pipeline back to the client. + * + * Customer-provided handlers are added as a Functions list in `pipelineConfig.functions` + */ +const defaultJsResolverAsset = (scope: Construct): Asset => { + const resolvedTemplatePath = resolve( + fileURLToPath(import.meta.url), + '../../lib', + JS_PIPELINE_RESOLVER_HANDLER + ); + + return new Asset(scope, 'default_js_resolver_handler_asset', { + path: resolveEntryPath(resolvedTemplatePath), + }); +}; + +/** + * Converts JS Resolver definition emitted by data-schema into AppSync pipeline + * resolvers via L1 construct + */ +export const convertJsResolverDefinition = ( + scope: Construct, + amplifyApi: AmplifyData, + jsResolvers: JsResolver[] | undefined +): void => { + if (!jsResolvers || jsResolvers.length < 1) { + return; + } + + const jsResolverTemplateAsset = defaultJsResolverAsset(scope); + + for (const resolver of jsResolvers) { + const functions: string[] = resolver.handlers.map((handler, idx) => { + const fnName = `Fn_${resolver.typeName}_${resolver.fieldName}_${idx + 1}`; + const s3AssetName = `${fnName}_asset`; + + const asset = new Asset(scope, s3AssetName, { + path: resolveEntryPath(handler.entry), + }); + + const fn = new CfnFunctionConfiguration(scope, fnName, { + apiId: amplifyApi.apiId, + dataSourceName: handler.dataSource, + name: fnName, + codeS3Location: asset.s3ObjectUrl, + runtime: { + name: APPSYNC_JS_RUNTIME_NAME, + runtimeVersion: APPSYNC_JS_RUNTIME_VERSION, + }, + }); + fn.node.addDependency(amplifyApi); + return fn.attrFunctionId; + }); + + const resolverName = `Resolver_${resolver.typeName}_${resolver.fieldName}`; + + new CfnResolver(scope, resolverName, { + apiId: amplifyApi.apiId, + fieldName: resolver.fieldName, + typeName: resolver.typeName, + kind: APPSYNC_PIPELINE_RESOLVER, + codeS3Location: jsResolverTemplateAsset.s3ObjectUrl, + runtime: { + name: APPSYNC_JS_RUNTIME_NAME, + runtimeVersion: APPSYNC_JS_RUNTIME_VERSION, + }, + pipelineConfig: { + functions, + }, + }).node.addDependency(amplifyApi); + } +}; diff --git a/packages/backend-data/src/convert_schema.ts b/packages/backend-data/src/convert_schema.ts index dbdb64fa30..25374ae83f 100644 --- a/packages/backend-data/src/convert_schema.ts +++ b/packages/backend-data/src/convert_schema.ts @@ -10,7 +10,9 @@ import { DataSchema } from './types.js'; * @param schema the schema that might be a derived model schema * @returns a boolean indicating whether the schema is a derived model schema, with type narrowing */ -const isModelSchema = (schema: DataSchema): schema is DerivedModelSchema => { +export const isModelSchema = ( + schema: DataSchema +): schema is DerivedModelSchema => { return ( schema !== null && typeof schema === 'object' && @@ -38,15 +40,18 @@ export const convertSchemaToCDK = ( * to generate that argument for us (so it's consistent with a customer using normal Graphql strings), then * apply that value back into the final IAmplifyDataDefinition output for data-schema users. */ + const { schema: transformedSchema, functionSlots } = schema.transform(); + const generatedModelDataSourceStrategies = AmplifyDataDefinition.fromString( - schema.transform().schema, + transformedSchema, { dbType, provisionStrategy, } ).dataSourceStrategies; return { - ...schema.transform(), + schema: transformedSchema, + functionSlots, dataSourceStrategies: generatedModelDataSourceStrategies, }; } diff --git a/packages/backend-data/src/factory.test.ts b/packages/backend-data/src/factory.test.ts index 606b9ce256..851e31b72f 100644 --- a/packages/backend-data/src/factory.test.ts +++ b/packages/backend-data/src/factory.test.ts @@ -11,10 +11,13 @@ import { ConstructContainer, ConstructFactory, ConstructFactoryGetInstanceProps, + FunctionResources, ImportPathVerifier, + ResourceAccessAcceptorFactory, ResourceProvider, + SsmEnvironmentEntry, } from '@aws-amplify/plugin-types'; -import { Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; +import { Policy, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; import { CfnIdentityPool, CfnIdentityPoolRoleAttachment, @@ -32,6 +35,9 @@ import { } from '@aws-amplify/backend-platform-test-stubs'; import { AmplifyDataResources } from '@aws-amplify/data-construct'; import { AmplifyUserError } from '@aws-amplify/platform-core'; +import { a } from '@aws-amplify/data-schema'; + +const CUSTOM_DDB_CFN_TYPE = 'Custom::AmplifyDynamoDBTable'; const testSchema = /* GraphQL */ ` type Todo @model { @@ -41,15 +47,81 @@ const testSchema = /* GraphQL */ ` } `; -const createStackAndSetContext = (): Stack => { +const createStackAndSetContext = (settings: { + isSandboxMode: boolean; +}): Stack => { const app = new App(); app.node.setContext('amplify-backend-name', 'testEnvName'); app.node.setContext('amplify-backend-namespace', 'testBackendId'); - app.node.setContext('amplify-backend-type', 'branch'); + app.node.setContext( + 'amplify-backend-type', + settings.isSandboxMode ? 'sandbox' : 'branch' + ); const stack = new Stack(app); return stack; }; +const createConstructContainerWithUserPoolAuthRegistered = ( + stack: Stack +): ConstructContainer => { + const constructContainer = new ConstructContainerStub( + new StackResolverStub(stack) + ); + const sampleUserPool = new UserPool(stack, 'UserPool'); + constructContainer.registerConstructFactory('AuthResources', { + provides: 'AuthResources', + getInstance: (): ResourceProvider => ({ + resources: { + userPool: sampleUserPool, + userPoolClient: new UserPoolClient(stack, 'UserPoolClient', { + userPool: sampleUserPool, + }), + unauthenticatedUserIamRole: new Role(stack, 'testUnauthRole', { + assumedBy: new ServicePrincipal('test.amazon.com'), + }), + authenticatedUserIamRole: new Role(stack, 'testAuthRole', { + assumedBy: new ServicePrincipal('test.amazon.com'), + }), + cfnResources: { + cfnUserPool: new CfnUserPool(stack, 'CfnUserPool', {}), + cfnUserPoolClient: new CfnUserPoolClient(stack, 'CfnUserPoolClient', { + userPoolId: 'userPool', + }), + cfnIdentityPool: new CfnIdentityPool(stack, 'identityPool', { + allowUnauthenticatedIdentities: true, + }), + cfnIdentityPoolRoleAttachment: new CfnIdentityPoolRoleAttachment( + stack, + 'identityPoolRoleAttachment', + { identityPoolId: 'identityPool' } + ), + }, + groups: {}, + }, + }), + }); + return constructContainer; +}; + +const createInstancePropsBySetupCDKApp = (settings: { + isSandboxMode: boolean; +}): ConstructFactoryGetInstanceProps => { + const stack: Stack = createStackAndSetContext({ + isSandboxMode: settings.isSandboxMode, + }); + const constructContainer: ConstructContainer = + createConstructContainerWithUserPoolAuthRegistered(stack); + const outputStorageStrategy: BackendOutputStorageStrategy = + new StackMetadataBackendOutputStorageStrategy(stack); + const importPathVerifier: ImportPathVerifier = new ImportPathVerifierStub(); + + return { + constructContainer, + outputStorageStrategy, + importPathVerifier, + }; +}; + void describe('DataFactory', () => { let stack: Stack; let constructContainer: ConstructContainer; @@ -61,48 +133,10 @@ void describe('DataFactory', () => { beforeEach(() => { resetFactoryCount(); dataFactory = defineData({ schema: testSchema }); - stack = createStackAndSetContext(); + stack = createStackAndSetContext({ isSandboxMode: false }); - constructContainer = new ConstructContainerStub( - new StackResolverStub(stack) - ); - const sampleUserPool = new UserPool(stack, 'UserPool'); - constructContainer.registerConstructFactory('AuthResources', { - provides: 'AuthResources', - getInstance: (): ResourceProvider => ({ - resources: { - userPool: sampleUserPool, - userPoolClient: new UserPoolClient(stack, 'UserPoolClient', { - userPool: sampleUserPool, - }), - unauthenticatedUserIamRole: new Role(stack, 'testUnauthRole', { - assumedBy: new ServicePrincipal('test.amazon.com'), - }), - authenticatedUserIamRole: new Role(stack, 'testAuthRole', { - assumedBy: new ServicePrincipal('test.amazon.com'), - }), - cfnResources: { - cfnUserPool: new CfnUserPool(stack, 'CfnUserPool', {}), - cfnUserPoolClient: new CfnUserPoolClient( - stack, - 'CfnUserPoolClient', - { - userPoolId: 'userPool', - } - ), - cfnIdentityPool: new CfnIdentityPool(stack, 'identityPool', { - allowUnauthenticatedIdentities: true, - }), - cfnIdentityPoolRoleAttachment: new CfnIdentityPoolRoleAttachment( - stack, - 'identityPoolRoleAttachment', - { identityPoolId: 'identityPool' } - ), - }, - groups: {}, - }, - }), - }); + constructContainer = + createConstructContainerWithUserPoolAuthRegistered(stack); outputStorageStrategy = new StackMetadataBackendOutputStorageStrategy( stack ); @@ -258,6 +292,355 @@ void describe('DataFactory', () => { }) ); }); + + void describe('function access', () => { + beforeEach(() => { + resetFactoryCount(); + }); + + void it('should attach expected policy to function role when schema access is defined', () => { + const lambda = new Function(stack, 'testFunc', { + code: Code.fromInline('test code'), + runtime: Runtime.NODEJS_LATEST, + handler: 'index.handler', + }); + const acceptResourceAccessMock = mock.fn< + (policy: Policy, ssmEnvironmentEntries: SsmEnvironmentEntry[]) => void + >((policy) => { + policy.attachToRole(lambda.role!); + }); + const myFunc: ConstructFactory< + ResourceProvider & ResourceAccessAcceptorFactory + > = { + getInstance: () => ({ + resources: { + lambda, + }, + getResourceAccessAcceptor: () => ({ + identifier: 'testId', + acceptResourceAccess: acceptResourceAccessMock, + }), + }), + }; + const schema = a + .schema({ + Todo: a.model({ + content: a.string(), + }), + }) + .authorization([ + a.allow.private().to(['read']), + a.allow.resource(myFunc), + ]); + + const dataFactory = defineData({ + schema, + }); + + const dataConstruct = dataFactory.getInstance(getInstanceProps); + + const template = Template.fromStack(Stack.of(dataConstruct)); + + // expect 2 policies in the template + // 1 is for a custom resource created by data and the other is the policy for the access config above + template.resourceCountIs('AWS::IAM::Policy', 2); + + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':appsync:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + // eslint-disable-next-line spellcheck/spell-checker + ':apis/', + { + 'Fn::GetAtt': [ + 'amplifyDataGraphQLAPI42A6FA33', + 'ApiId', + ], + }, + '/types/Query/*', + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':appsync:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + // eslint-disable-next-line spellcheck/spell-checker + ':apis/', + { + 'Fn::GetAtt': [ + 'amplifyDataGraphQLAPI42A6FA33', + 'ApiId', + ], + }, + '/types/Mutation/*', + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':appsync:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + // eslint-disable-next-line spellcheck/spell-checker + ':apis/', + { + 'Fn::GetAtt': [ + 'amplifyDataGraphQLAPI42A6FA33', + 'ApiId', + ], + }, + '/types/Subscription/*', + ], + ], + }, + ], + }, + ], + }, + Roles: [ + { + // eslint-disable-next-line spellcheck/spell-checker + Ref: 'referencetotestFuncServiceRole67735AD9Ref', + }, + ], + }); + }); + + void it('should attach expected policy to multiple function roles', () => { + // create lambda1 stub + const lambda1 = new Function(stack, 'testFunc1', { + code: Code.fromInline('test code'), + runtime: Runtime.NODEJS_LATEST, + handler: 'index.handler', + }); + const acceptResourceAccessMock1 = mock.fn< + (policy: Policy, ssmEnvironmentEntries: SsmEnvironmentEntry[]) => void + >((policy) => { + policy.attachToRole(lambda1.role!); + }); + const myFunc1: ConstructFactory< + ResourceProvider & ResourceAccessAcceptorFactory + > = { + getInstance: () => ({ + resources: { + lambda: lambda1, + }, + getResourceAccessAcceptor: () => ({ + identifier: 'testId1', + acceptResourceAccess: acceptResourceAccessMock1, + }), + }), + }; + + // create lambda1 stub + const lambda2 = new Function(stack, 'testFunc2', { + code: Code.fromInline('test code'), + runtime: Runtime.NODEJS_LATEST, + handler: 'index.handler', + }); + const acceptResourceAccessMock2 = mock.fn< + (policy: Policy, ssmEnvironmentEntries: SsmEnvironmentEntry[]) => void + >((policy) => { + policy.attachToRole(lambda2.role!); + }); + const myFunc2: ConstructFactory< + ResourceProvider & ResourceAccessAcceptorFactory + > = { + getInstance: () => ({ + resources: { + lambda: lambda2, + }, + getResourceAccessAcceptor: () => ({ + identifier: 'testId2', + acceptResourceAccess: acceptResourceAccessMock2, + }), + }), + }; + const schema = a + .schema({ + Todo: a.model({ + content: a.string(), + }), + }) + .authorization([ + a.allow.private().to(['read']), + a.allow.resource(myFunc1).to(['mutate']), + a.allow.resource(myFunc2).to(['query']), + ]); + + const dataFactory = defineData({ + schema, + }); + + const dataConstruct = dataFactory.getInstance(getInstanceProps); + + const template = Template.fromStack(Stack.of(dataConstruct)); + + // expect 3 policies in the template + // 1 is for a custom resource created by data and the other two are for the two function access definition above + template.resourceCountIs('AWS::IAM::Policy', 3); + + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':appsync:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + // eslint-disable-next-line spellcheck/spell-checker + ':apis/', + { + 'Fn::GetAtt': ['amplifyDataGraphQLAPI42A6FA33', 'ApiId'], + }, + '/types/Mutation/*', + ], + ], + }, + }, + ], + }, + Roles: [ + { + // eslint-disable-next-line spellcheck/spell-checker + Ref: 'referencetotestFunc1ServiceRoleBD09EB83Ref', + }, + ], + }); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'appsync:GraphQL', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':appsync:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + // eslint-disable-next-line spellcheck/spell-checker + ':apis/', + { + 'Fn::GetAtt': ['amplifyDataGraphQLAPI42A6FA33', 'ApiId'], + }, + '/types/Query/*', + ], + ], + }, + }, + ], + }, + Roles: [ + { + // eslint-disable-next-line spellcheck/spell-checker + Ref: 'referencetotestFunc2ServiceRole9C59B5B3Ref', + }, + ], + }); + }); + }); +}); + +void describe('Destructive Schema Updates & Replace tables upon GSI updates', () => { + let dataFactory: ConstructFactory>; + let getInstanceProps: ConstructFactoryGetInstanceProps; + + beforeEach(() => { + resetFactoryCount(); + dataFactory = defineData({ schema: testSchema }); + }); + + void it('should allow destructive updates and disable GSI update replacing tables in non-sandbox mode', () => { + getInstanceProps = createInstancePropsBySetupCDKApp({ + isSandboxMode: false, + }); + const dataConstruct = dataFactory.getInstance(getInstanceProps); + const amplifyTableStackTemplate = Template.fromStack( + Stack.of(dataConstruct.resources.nestedStacks['Todo']) + ); + amplifyTableStackTemplate.hasResourceProperties(CUSTOM_DDB_CFN_TYPE, { + allowDestructiveGraphqlSchemaUpdates: true, + replaceTableUponGsiUpdate: false, + }); + }); + void it('should allow destructive updates and enable GSI update replacing tables in sandbox mode', () => { + getInstanceProps = createInstancePropsBySetupCDKApp({ + isSandboxMode: true, + }); + const dataConstruct = dataFactory.getInstance(getInstanceProps); + const amplifyTableStackTemplate = Template.fromStack( + Stack.of(dataConstruct.resources.nestedStacks['Todo']) + ); + amplifyTableStackTemplate.hasResourceProperties(CUSTOM_DDB_CFN_TYPE, { + allowDestructiveGraphqlSchemaUpdates: true, + replaceTableUponGsiUpdate: true, + }); + }); }); const resetFactoryCount = () => { diff --git a/packages/backend-data/src/factory.ts b/packages/backend-data/src/factory.ts index 1573d5651d..9948415d67 100644 --- a/packages/backend-data/src/factory.ts +++ b/packages/backend-data/src/factory.ts @@ -1,4 +1,6 @@ +import { IConstruct } from 'constructs'; import { + AmplifyFunction, AuthResources, BackendOutputStorageStrategy, ConstructContainerEntryGenerator, @@ -7,16 +9,16 @@ import { GenerateContainerEntryProps, ResourceProvider, } from '@aws-amplify/plugin-types'; -import { AmplifyData } from '@aws-amplify/data-construct'; +import { + AmplifyData, + AmplifyDynamoDbTableWrapper, + TranslationBehavior, +} from '@aws-amplify/data-construct'; import { GraphqlOutput } from '@aws-amplify/backend-output-schemas'; import * as path from 'path'; import { AmplifyDataError, DataProps } from './types.js'; -import { convertSchemaToCDK } from './convert_schema.js'; -import { - FunctionInstanceProvider, - buildConstructFactoryFunctionInstanceProvider, - convertFunctionNameMapToCDK, -} from './convert_functions.js'; +import { convertSchemaToCDK, isModelSchema } from './convert_schema.js'; +import { convertFunctionNameMapToCDK } from './convert_functions.js'; import { ProvidedAuthConfig, buildConstructFactoryProvidedAuthConfig, @@ -24,7 +26,14 @@ import { isUsingDefaultApiKeyAuth, } from './convert_authorization_modes.js'; import { validateAuthorizationModes } from './validate_authorization_modes.js'; -import { AmplifyUserError } from '@aws-amplify/platform-core'; +import { AmplifyUserError, CDKContextKey } from '@aws-amplify/platform-core'; +import { Aspects, IAspect } from 'aws-cdk-lib'; +import { convertJsResolverDefinition } from './convert_js_resolvers.js'; +import { AppSyncPolicyGenerator } from './app_sync_policy_generator.js'; +import { + FunctionSchemaAccess, + JsResolver, +} from '@aws-amplify/data-schema-types'; /** * Singleton factory for AmplifyGraphqlApi constructs that can be used in Amplify project files. @@ -75,7 +84,7 @@ export class DataFactory implements ConstructFactory { ) ?.getInstance(props) ), - buildConstructFactoryFunctionInstanceProvider(props), + props, outputStorageStrategy ); } @@ -90,18 +99,56 @@ class DataGenerator implements ConstructContainerEntryGenerator { constructor( private readonly props: DataProps, private readonly providedAuthConfig: ProvidedAuthConfig | undefined, - private readonly functionInstanceProvider: FunctionInstanceProvider, + private readonly getInstanceProps: ConstructFactoryGetInstanceProps, private readonly outputStorageStrategy: BackendOutputStorageStrategy ) {} - generateContainerEntry = ({ scope }: GenerateContainerEntryProps) => { + generateContainerEntry = ({ + scope, + ssmEnvironmentEntriesGenerator, + }: GenerateContainerEntryProps) => { + let amplifyGraphqlDefinition; + let jsFunctions: JsResolver[] = []; + let functionSchemaAccess: FunctionSchemaAccess[] = []; + let lambdaFunctions: Record> = {}; + try { + if (isModelSchema(this.props.schema)) { + ({ jsFunctions, functionSchemaAccess, lambdaFunctions } = + this.props.schema.transform()); + } + amplifyGraphqlDefinition = convertSchemaToCDK(this.props.schema); + } catch (error) { + throw new AmplifyUserError( + 'InvalidSchemaError', + { + message: + error instanceof Error + ? error.message + : 'Failed to parse schema definition.', + resolution: + 'Check your data schema definition for syntax and type errors.', + }, + error instanceof Error ? error : undefined + ); + } + let authorizationModes; + /** + * TODO - remove this after the data construct does work to remove the need for allow-listed IAM roles + */ + const functionSchemaAccessRoles = functionSchemaAccess.map( + (accessEntry) => + accessEntry.resourceProvider.getInstance(this.getInstanceProps) + .resources.lambda.role! + ); + try { authorizationModes = convertAuthorizationModesToCDK( - this.functionInstanceProvider, + this.getInstanceProps, this.providedAuthConfig, - this.props.authorizationModes + this.props.authorizationModes, + functionSchemaAccessRoles ); } catch (error) { throw new AmplifyUserError( @@ -110,7 +157,8 @@ class DataGenerator implements ConstructContainerEntryGenerator { message: error instanceof Error ? error.message - : 'Cannot covert authorization modes', + : 'Failed to parse authorization modes.', + resolution: 'Ensure the auth rules on your schema are valid.', }, error instanceof Error ? error : undefined ); @@ -129,6 +177,7 @@ class DataGenerator implements ConstructContainerEntryGenerator { error instanceof Error ? error.message : 'Failed to validate authorization modes', + resolution: 'Ensure the auth rules on your schema are valid.', }, error instanceof Error ? error : undefined ); @@ -139,38 +188,84 @@ class DataGenerator implements ConstructContainerEntryGenerator { this.props.authorizationModes ); - const functionNameMap = convertFunctionNameMapToCDK( - this.functionInstanceProvider, - this.props.functions ?? {} - ); + const propsFunctions = this.props.functions ?? {}; - let amplifyGraphqlDefinition; - try { - amplifyGraphqlDefinition = convertSchemaToCDK(this.props.schema); - } catch (error) { - throw new AmplifyUserError( - 'InvalidSchemaError', - { - message: - error instanceof Error - ? error.message - : 'Cannot covert user schema', - }, - error instanceof Error ? error : undefined - ); - } - - return new AmplifyData(scope, this.defaultName, { + const functionNameMap = convertFunctionNameMapToCDK(this.getInstanceProps, { + ...propsFunctions, + ...lambdaFunctions, + }); + const amplifyApi = new AmplifyData(scope, this.defaultName, { apiName: this.props.name, definition: amplifyGraphqlDefinition, authorizationModes, outputStorageStrategy: this.outputStorageStrategy, functionNameMap, - translationBehavior: { sandboxModeEnabled }, + translationBehavior: { + sandboxModeEnabled, + /** + * The destructive updates should be always allowed in backend definition and not to be controlled on the IaC + * The CI/CD check should take the responsibility to validate if any tables are being replaced and determine whether to execute the changeset + */ + allowDestructiveGraphqlSchemaUpdates: true, + }, + }); + + /** + * Enable the table replacement upon GSI update + * This is allowed in sandbox mode ONLY + */ + const isSandboxDeployment = + scope.node.tryGetContext(CDKContextKey.DEPLOYMENT_TYPE) === 'sandbox'; + if (isSandboxDeployment) { + Aspects.of(amplifyApi).add(new ReplaceTableUponGsiUpdateOverrideAspect()); + } + + convertJsResolverDefinition(scope, amplifyApi, jsFunctions); + + const ssmEnvironmentEntries = + ssmEnvironmentEntriesGenerator.generateSsmEnvironmentEntries({ + [`${this.props.name}_GRAPHQL_ENDPOINT`]: + amplifyApi.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl, + }); + + const policyGenerator = new AppSyncPolicyGenerator( + amplifyApi.resources.graphqlApi + ); + + functionSchemaAccess.forEach((accessDefinition) => { + const policy = policyGenerator.generateGraphqlAccessPolicy( + accessDefinition.actions + ); + accessDefinition.resourceProvider + .getInstance(this.getInstanceProps) + .getResourceAccessAcceptor() + .acceptResourceAccess(policy, ssmEnvironmentEntries); }); + + return amplifyApi; }; } +const REPLACE_TABLE_UPON_GSI_UPDATE_ATTRIBUTE_NAME: keyof TranslationBehavior = + 'replaceTableUponGsiUpdate'; + +/** + * Aspect class to modify the amplify managed DynamoDB table + * to allow table replacement upon GSI update + */ +class ReplaceTableUponGsiUpdateOverrideAspect implements IAspect { + public visit(scope: IConstruct): void { + if (AmplifyDynamoDbTableWrapper.isAmplifyDynamoDbTableResource(scope)) { + // These value setters are not exposed in the wrapper + // Need to use the property override to escape the hatch + scope.addPropertyOverride( + REPLACE_TABLE_UPON_GSI_UPDATE_ATTRIBUTE_NAME, + true + ); + } + } +} + /** * Creates a factory that implements ConstructFactory */ diff --git a/packages/backend-data/src/types.ts b/packages/backend-data/src/types.ts index 0e109529c4..4b581e9ade 100644 --- a/packages/backend-data/src/types.ts +++ b/packages/backend-data/src/types.ts @@ -97,11 +97,6 @@ export type AuthorizationModes = { * OIDC authorization config if oidc provider is specified in the api definition. */ oidcAuthorizationMode?: OIDCAuthorizationModeProps; - - /** - * IAM Role names which are provided full r/w access to the API for models with IAM authorization. - */ - allowListedRoleNames?: string[]; }; /** diff --git a/packages/backend-data/src/validate_authorization_modes.test.ts b/packages/backend-data/src/validate_authorization_modes.test.ts index 6cf6be9f34..8c8261c660 100644 --- a/packages/backend-data/src/validate_authorization_modes.test.ts +++ b/packages/backend-data/src/validate_authorization_modes.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert'; import { beforeEach, describe, it } from 'node:test'; -import { Duration, Stack } from 'aws-cdk-lib'; +import { Stack } from 'aws-cdk-lib'; import { Role } from 'aws-cdk-lib/aws-iam'; import { validateAuthorizationModes } from './validate_authorization_modes.js'; @@ -13,44 +13,22 @@ void describe('validateAuthorizationModes', () => { void it('does not throw on well-formed input', () => { assert.doesNotThrow(() => - validateAuthorizationModes( - { - allowListedRoleNames: ['MyAdminRole'], + validateAuthorizationModes(undefined, { + iamConfig: { + identityPoolId: 'testIdentityPool', + authenticatedUserRole: Role.fromRoleName( + stack, + 'AuthUserRole', + 'MyAuthUserRole' + ), + unauthenticatedUserRole: Role.fromRoleName( + stack, + 'UnauthUserRole', + 'MyUnauthUserRole' + ), + allowListedRoles: ['MyAdminRole'], }, - { - iamConfig: { - identityPoolId: 'testIdentityPool', - authenticatedUserRole: Role.fromRoleName( - stack, - 'AuthUserRole', - 'MyAuthUserRole' - ), - unauthenticatedUserRole: Role.fromRoleName( - stack, - 'UnauthUserRole', - 'MyUnauthUserRole' - ), - allowListedRoles: ['MyAdminRole'], - }, - } - ) - ); - }); - - void it('throws if admin roles are specified and there is no iam auth configured', () => { - assert.throws( - () => - validateAuthorizationModes( - { - allowListedRoleNames: ['MyAdminRole'], - }, - { - apiKeyConfig: { - expires: Duration.days(7), - }, - } - ), - /Specifying allowListedRoleNames requires presence of IAM Authorization config. Auth must be added to the backend./ + }) ); }); diff --git a/packages/backend-data/src/validate_authorization_modes.ts b/packages/backend-data/src/validate_authorization_modes.ts index 1225287bc3..2db07e9859 100644 --- a/packages/backend-data/src/validate_authorization_modes.ts +++ b/packages/backend-data/src/validate_authorization_modes.ts @@ -6,25 +6,6 @@ type AuthorizationModeValidator = ( transformedAuthorizationModes: CDKAuthorizationModes ) => void; -/** - * Admin roles require iam config be specified. - */ -const validateAdminRolesHaveIAMAuthorizationConfig: AuthorizationModeValidator = - ( - inputAuthorizationModes: AuthorizationModes | undefined, - transformedAuthorizationModes: CDKAuthorizationModes - ): void => { - if ( - inputAuthorizationModes?.allowListedRoleNames && - inputAuthorizationModes?.allowListedRoleNames.length > 0 && - !transformedAuthorizationModes.iamConfig - ) { - throw new Error( - 'Specifying allowListedRoleNames requires presence of IAM Authorization config. Auth must be added to the backend.' - ); - } - }; - /** * At least one auth mode is required on the API, otherwise an exception will be thrown. */ @@ -60,9 +41,6 @@ export const validateAuthorizationModes = ( inputAuthorizationModes: AuthorizationModes | undefined, transformedAuthorizationModes: CDKAuthorizationModes ): void => - [ - validateAdminRolesHaveIAMAuthorizationConfig, - validateAtLeastOneAuthModeIsConfigured, - ].forEach((validate) => + [validateAtLeastOneAuthModeIsConfigured].forEach((validate) => validate(inputAuthorizationModes, transformedAuthorizationModes) ); diff --git a/packages/backend-data/tsconfig.json b/packages/backend-data/tsconfig.json index 91aaafc175..b86864896a 100644 --- a/packages/backend-data/tsconfig.json +++ b/packages/backend-data/tsconfig.json @@ -1,11 +1,13 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "lib" }, + "exclude": ["src/assets", "lib"], "references": [ { "path": "../backend-output-storage" }, { "path": "../backend-output-schemas" }, { "path": "../plugin-types" }, { "path": "../backend-platform-test-stubs" }, - { "path": "../platform-core" } + { "path": "../platform-core" }, + { "path": "./src/assets" } ] } diff --git a/packages/backend-deployer/CHANGELOG.md b/packages/backend-deployer/CHANGELOG.md index c6eb1127b4..2e3e1a1f2a 100644 --- a/packages/backend-deployer/CHANGELOG.md +++ b/packages/backend-deployer/CHANGELOG.md @@ -1,5 +1,13 @@ # @aws-amplify/backend-deployer +## 0.5.1-beta.2 + +### Patch Changes + +- 937086b: require "resolution" in AmplifyUserError options +- Updated dependencies [937086b] + - @aws-amplify/platform-core@0.5.0-beta.2 + ## 0.5.1-beta.1 ### Patch Changes diff --git a/packages/backend-deployer/package.json b/packages/backend-deployer/package.json index 2bf97b30e8..3522e18307 100644 --- a/packages/backend-deployer/package.json +++ b/packages/backend-deployer/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-deployer", - "version": "0.5.1-beta.1", + "version": "0.5.1-beta.2", "type": "module", "publishConfig": { "access": "public" @@ -18,7 +18,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/platform-core": "^0.5.0-beta.1", + "@aws-amplify/platform-core": "^0.5.0-beta.2", "@aws-amplify/plugin-types": "^0.9.0-beta.0", "execa": "^8.0.1", "tsx": "^4.6.1" diff --git a/packages/backend-deployer/src/cdk_deployer.ts b/packages/backend-deployer/src/cdk_deployer.ts index 72efdbde22..e22cc3db18 100644 --- a/packages/backend-deployer/src/cdk_deployer.ts +++ b/packages/backend-deployer/src/cdk_deployer.ts @@ -156,8 +156,9 @@ export class CDKDeployer implements BackendDeployer { throw new AmplifyUserError( 'SyntaxError', { - message: - 'TypeScript validation check failed, check your backend definition', + message: 'TypeScript validation check failed.', + resolution: + 'Fix the syntax and type errors in your backend definition.', }, err instanceof Error ? err : undefined ); diff --git a/packages/backend-deployer/src/cdk_error_mapper.test.ts b/packages/backend-deployer/src/cdk_error_mapper.test.ts index f1259a14a5..1e920113a3 100644 --- a/packages/backend-deployer/src/cdk_error_mapper.test.ts +++ b/packages/backend-deployer/src/cdk_error_mapper.test.ts @@ -26,21 +26,21 @@ const testErrorMappings = [ { errorMessage: 'ReferenceError: var is not defined\n', expectedTopLevelErrorMessage: - 'Unable to build Amplify backend. Check your backend definition in the `amplify` folder.', + 'Unable to build the Amplify backend definition.', errorName: 'SyntaxError', expectedDownstreamErrorMessage: 'ReferenceError: var is not defined\n', }, { errorMessage: 'Has the environment been bootstrapped', expectedTopLevelErrorMessage: - 'This AWS account and region has not been bootstrapped. Run `cdk bootstrap aws://{YOUR_ACCOUNT_ID}/{YOUR_REGION}` locally to resolve this.', + 'This AWS account and region has not been bootstrapped.', errorName: 'BootstrapNotDetectedError', expectedDownstreamErrorMessage: 'Has the environment been bootstrapped', }, { errorMessage: 'Amplify Backend not found in amplify/backend.ts', expectedTopLevelErrorMessage: - 'Backend definition could not be found in amplify directory', + 'Backend definition could not be found in amplify directory.', errorName: 'FileConventionError', expectedDownstreamErrorMessage: 'Amplify Backend not found in amplify/backend.ts', @@ -48,23 +48,21 @@ const testErrorMappings = [ { errorMessage: 'Amplify Auth must be defined in amplify/auth/resource.ts', expectedTopLevelErrorMessage: - 'File name or path for backend definition are incorrect', + 'File name or path for backend definition are incorrect.', errorName: 'FileConventionError', expectedDownstreamErrorMessage: 'Amplify Auth must be defined in amplify/auth/resource.ts', }, { errorMessage: 'amplify/backend.ts', - expectedTopLevelErrorMessage: - 'Unable to build Amplify backend. Check your backend definition in the `amplify` folder.', + expectedTopLevelErrorMessage: 'Unable to build Amplify backend.', errorName: 'BackendBuildError', expectedDownstreamErrorMessage: 'amplify/backend.ts', }, { errorMessage: 'Overall error message had other stuff before ❌ Deployment failed: something bad happened\n and after', - expectedTopLevelErrorMessage: - 'The CloudFormation deployment has failed. Find more information in the CloudFormation AWS Console for this stack.', + expectedTopLevelErrorMessage: 'The CloudFormation deployment has failed.', errorName: 'CloudFormationDeploymentError', expectedDownstreamErrorMessage: '❌ Deployment failed: something bad happened\n', diff --git a/packages/backend-deployer/src/cdk_error_mapper.ts b/packages/backend-deployer/src/cdk_error_mapper.ts index b6cabf7775..f4daa3e838 100644 --- a/packages/backend-deployer/src/cdk_error_mapper.ts +++ b/packages/backend-deployer/src/cdk_error_mapper.ts @@ -11,6 +11,7 @@ export class CdkErrorMapper { private knownErrors: Array<{ errorRegex: RegExp; humanReadableErrorMessage: string; + resolutionMessage: string; errorName: CDKDeploymentError; classification: AmplifyErrorClassification; }> = [ @@ -18,6 +19,7 @@ export class CdkErrorMapper { errorRegex: /ExpiredToken/, humanReadableErrorMessage: 'The security token included in the request is invalid.', + resolutionMessage: 'Ensure your local AWS credentials are valid.', errorName: 'ExpiredTokenError', classification: 'ERROR', }, @@ -25,42 +27,51 @@ export class CdkErrorMapper { errorRegex: /Access Denied/, humanReadableErrorMessage: 'The deployment role does not have sufficient permissions to perform this deployment.', + resolutionMessage: + 'Ensure your deployment role has the AmplifyBackendDeployFullAccess role along with any additional permissions required to deploy your backend definition.', errorName: 'AccessDeniedError', classification: 'ERROR', }, { errorRegex: /Has the environment been bootstrapped/, humanReadableErrorMessage: - 'This AWS account and region has not been bootstrapped. Run `cdk bootstrap aws://{YOUR_ACCOUNT_ID}/{YOUR_REGION}` locally to resolve this.', + 'This AWS account and region has not been bootstrapped.', + resolutionMessage: + 'Run `cdk bootstrap aws://{YOUR_ACCOUNT_ID}/{YOUR_REGION}` locally to resolve this.', errorName: 'BootstrapNotDetectedError', classification: 'ERROR', }, { errorRegex: /(SyntaxError|ReferenceError):(.*)\n/, humanReadableErrorMessage: - 'Unable to build Amplify backend. Check your backend definition in the `amplify` folder.', + 'Unable to build the Amplify backend definition.', + resolutionMessage: + 'Check your backend definition in the `amplify` folder for syntax and type errors.', errorName: 'SyntaxError', classification: 'ERROR', }, { errorRegex: /Amplify Backend not found in/, humanReadableErrorMessage: - 'Backend definition could not be found in amplify directory', + 'Backend definition could not be found in amplify directory.', + resolutionMessage: 'Ensure that the amplify/backend.(ts|js) file exists', errorName: 'FileConventionError', classification: 'ERROR', }, { errorRegex: /Amplify (.*) must be defined in (.*)/, humanReadableErrorMessage: - 'File name or path for backend definition are incorrect', + 'File name or path for backend definition are incorrect.', + resolutionMessage: 'Ensure that the amplify/backend.(ts|js) file exists', errorName: 'FileConventionError', classification: 'ERROR', }, { // the backend entry point file is referenced in the stack indicating a problem in customer code errorRegex: /amplify\/backend/, - humanReadableErrorMessage: - 'Unable to build Amplify backend. Check your backend definition in the `amplify` folder.', + humanReadableErrorMessage: 'Unable to build Amplify backend.', + resolutionMessage: + 'Check your backend definition in the `amplify` folder for syntax and type errors.', errorName: 'BackendBuildError', classification: 'ERROR', }, @@ -68,6 +79,8 @@ export class CdkErrorMapper { errorRegex: /Updates are not allowed for property/, humanReadableErrorMessage: 'The changes that you are trying to apply are not supported.', + resolutionMessage: + 'The resources referenced in the error message must be deleted and recreated to apply the changes.', errorName: 'CFNUpdateNotSupportedError', classification: 'ERROR', }, @@ -79,14 +92,17 @@ export class CdkErrorMapper { /Invalid AttributeDataType input, consider using the provided AttributeDataType enum/, humanReadableErrorMessage: 'User pool attributes cannot be changed after a user pool has been created.', + resolutionMessage: + 'To change these attributes, remove `defineAuth` from your backend, deploy, then add it back. Note that removing `defineAuth` and deploying will delete any users stored in your UserPool.', errorName: 'CFNUpdateNotSupportedError', classification: 'ERROR', }, { // Note that the order matters, this should be the last as it captures generic CFN error errorRegex: /❌ Deployment failed: (.*)\n/, - humanReadableErrorMessage: - 'The CloudFormation deployment has failed. Find more information in the CloudFormation AWS Console for this stack.', + humanReadableErrorMessage: 'The CloudFormation deployment has failed.', + resolutionMessage: + 'Find more information in the CloudFormation AWS Console for this stack.', errorName: 'CloudFormationDeploymentError', classification: 'ERROR', }, @@ -116,12 +132,18 @@ export class CdkErrorMapper { return matchingError.classification === 'ERROR' ? new AmplifyUserError( matchingError.errorName, - { message: matchingError.humanReadableErrorMessage }, + { + message: matchingError.humanReadableErrorMessage, + resolution: matchingError.resolutionMessage, + }, error ) : new AmplifyFault( matchingError.errorName, - { message: matchingError.humanReadableErrorMessage }, + { + message: matchingError.humanReadableErrorMessage, + resolution: matchingError.resolutionMessage, + }, error ); } diff --git a/packages/backend-function/CHANGELOG.md b/packages/backend-function/CHANGELOG.md index 58e13df3f1..de95cc357b 100644 --- a/packages/backend-function/CHANGELOG.md +++ b/packages/backend-function/CHANGELOG.md @@ -1,5 +1,30 @@ # @aws-amplify/backend-function +## 0.8.0-beta.4 + +### Patch Changes + +- 75f69ea: store attribution string in funciton stack + - @aws-amplify/backend-output-storage@0.4.0-beta.2 + +## 0.8.0-beta.3 + +### Patch Changes + +- bdbf6e8: Set default function memory to 512 +- 7f5edee: Ensure typed shim files contain only the function name + +## 0.8.0-beta.2 + +### Minor Changes + +- cec91d5: Add dynamic environment variables to function type definition files +- b0ba24d: Generate type definition file for static environment variables for functions + +### Patch Changes + +- 318335d: Ensure resource access env vars are added to function typed shim files + ## 0.8.0-beta.1 ### Minor Changes diff --git a/packages/backend-function/package.json b/packages/backend-function/package.json index 87e01fef46..e52021a7f2 100644 --- a/packages/backend-function/package.json +++ b/packages/backend-function/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-function", - "version": "0.8.0-beta.1", + "version": "0.8.0-beta.4", "type": "module", "publishConfig": { "access": "public" @@ -19,13 +19,13 @@ "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", - "@aws-amplify/backend-output-storage": "^0.4.0-beta.1", + "@aws-amplify/backend-output-storage": "^0.4.0-beta.2", "@aws-amplify/plugin-types": "^0.9.0-beta.0", "execa": "^8.0.1" }, "devDependencies": { "@aws-amplify/backend-platform-test-stubs": "^0.3.3-beta.0", - "@aws-amplify/platform-core": "^0.5.0-beta.1", + "@aws-amplify/platform-core": "^0.5.0-beta.2", "@aws-sdk/client-ssm": "^3.465.0", "aws-sdk": "^2.1550.0", "uuid": "^9.0.1" diff --git a/packages/backend-function/src/factory.test.ts b/packages/backend-function/src/factory.test.ts index 3b08d3f63b..b57e5f6937 100644 --- a/packages/backend-function/src/factory.test.ts +++ b/packages/backend-function/src/factory.test.ts @@ -182,6 +182,17 @@ void describe('AmplifyFunctionFactory', () => { }); }); + void it('sets default memory', () => { + const lambda = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + }).getInstance(getInstanceProps); + const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + + template.hasResourceProperties('AWS::Lambda::Function', { + MemorySize: 512, + }); + }); + void it('throws on memory below 128 MB', () => { assert.throws( () => @@ -336,4 +347,24 @@ void describe('AmplifyFunctionFactory', () => { }); }); }); + + void it('stores single attribution data value in stack with multiple functions', () => { + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'testLambdaName', + }); + const anotherFunction = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'anotherName', + }); + const functionStack = Stack.of( + functionFactory.getInstance(getInstanceProps).resources.lambda + ); + anotherFunction.getInstance(getInstanceProps); + const template = Template.fromStack(functionStack); + assert.equal( + JSON.parse(template.toJSON().Description).stackType, + 'function-Lambda' + ); + }); }); diff --git a/packages/backend-function/src/factory.ts b/packages/backend-function/src/factory.ts index ebc7ae2597..7f9e10ac96 100644 --- a/packages/backend-function/src/factory.ts +++ b/packages/backend-function/src/factory.ts @@ -15,7 +15,7 @@ import { Construct } from 'constructs'; import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs'; import * as path from 'path'; import { getCallerDirectory } from './get_caller_directory.js'; -import { Duration } from 'aws-cdk-lib'; +import { Duration, Stack } from 'aws-cdk-lib'; import { Runtime } from 'aws-cdk-lib/aws-lambda'; import { createRequire } from 'module'; import { FunctionEnvironmentTranslator } from './function_env_translator.js'; @@ -27,6 +27,10 @@ import { functionOutputKey, } from '@aws-amplify/backend-output-schemas'; import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js'; +import { AttributionMetadataStorage } from '@aws-amplify/backend-output-storage'; +import { fileURLToPath } from 'url'; + +const functionStackType = 'function-Lambda'; /** * Entry point for defining a function in the Amplify ecosystem @@ -177,7 +181,7 @@ class FunctionFactory implements ConstructFactory { private resolveMemory = () => { const memoryMin = 128; const memoryMax = 10240; - const memoryDefault = memoryMin; + const memoryDefault = 512; if (this.props.memoryMB === undefined) { return memoryDefault; } @@ -292,8 +296,9 @@ class AmplifyFunction this.functionEnvironmentTranslator = new FunctionEnvironmentTranslator( functionLambda, - props['environment'], - backendSecretResolver + props.environment, + backendSecretResolver, + new FunctionEnvironmentTypeGenerator(id) ); this.resources = { @@ -302,13 +307,11 @@ class AmplifyFunction this.storeOutput(outputStorageStrategy); - // Using CDK validation mechanism as a way to generate a type definition file at the end of synthesis - this.node.addValidation({ - validate: (): string[] => { - new FunctionEnvironmentTypeGenerator(id).generateTypeDefFile(); - return []; - }, - }); + new AttributionMetadataStorage().storeAttributionMetadata( + Stack.of(this), + functionStackType, + fileURLToPath(new URL('../package.json', import.meta.url)) + ); } getResourceAccessAcceptor = () => ({ diff --git a/packages/backend-function/src/function_env_translator.test.ts b/packages/backend-function/src/function_env_translator.test.ts index 13abf05184..7276d6ff70 100644 --- a/packages/backend-function/src/function_env_translator.test.ts +++ b/packages/backend-function/src/function_env_translator.test.ts @@ -12,6 +12,7 @@ import assert from 'node:assert'; import { ParameterPathConversions } from '@aws-amplify/platform-core'; import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'; import { Template } from 'aws-cdk-lib/assertions'; +import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js'; const testStack = {} as Construct; @@ -21,6 +22,8 @@ const testBackendIdentifier: BackendIdentifier = { type: 'branch', }; +const testLambdaName = 'testFunction'; + class TestBackendSecretResolver implements BackendSecretResolver { resolveSecret = (backendSecret: BackendSecret): SecretValue => { return backendSecret.resolve(testStack, testBackendIdentifier); @@ -62,7 +65,8 @@ void describe('FunctionEnvironmentTranslator', () => { new FunctionEnvironmentTranslator( testLambda, functionEnvProp, - backendResolver + backendResolver, + new FunctionEnvironmentTypeGenerator(testLambdaName) ); const template = Template.fromStack(Stack.of(testLambda)); @@ -88,7 +92,8 @@ void describe('FunctionEnvironmentTranslator', () => { new FunctionEnvironmentTranslator( testLambda, functionEnvProp, - backendResolver + backendResolver, + new FunctionEnvironmentTypeGenerator(testLambdaName) ); const template = Template.fromStack(Stack.of(testLambda)); @@ -121,7 +126,8 @@ void describe('FunctionEnvironmentTranslator', () => { new FunctionEnvironmentTranslator( getTestLambda(), functionEnvProp, - backendResolver + backendResolver, + new FunctionEnvironmentTypeGenerator(testLambdaName) ) ); }); @@ -136,7 +142,8 @@ void describe('FunctionEnvironmentTranslator', () => { new FunctionEnvironmentTranslator( testLambda, functionEnvProp, - backendResolver + backendResolver, + new FunctionEnvironmentTypeGenerator(testLambdaName) ); const template = Template.fromStack(Stack.of(testLambda)); @@ -154,7 +161,8 @@ void describe('FunctionEnvironmentTranslator', () => { new FunctionEnvironmentTranslator( testLambda, functionEnvProp, - backendResolver + backendResolver, + new FunctionEnvironmentTypeGenerator(testLambdaName) ); const template = Template.fromStack(Stack.of(testLambda)); @@ -216,7 +224,7 @@ void describe('FunctionEnvironmentTranslator', () => { }); const getTestLambda = () => - new Function(new Stack(new App()), 'testFunction', { + new Function(new Stack(new App()), testLambdaName, { code: Code.fromInline('test code'), runtime: Runtime.NODEJS_20_X, handler: 'handler', diff --git a/packages/backend-function/src/function_env_translator.ts b/packages/backend-function/src/function_env_translator.ts index d43b683d01..5e204b1f39 100644 --- a/packages/backend-function/src/function_env_translator.ts +++ b/packages/backend-function/src/function_env_translator.ts @@ -3,6 +3,7 @@ import { Arn, Lazy, Stack } from 'aws-cdk-lib'; import { FunctionProps } from './factory.js'; import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js'; /** * Translates function environment props into appropriate environment records and builds a policy statement @@ -16,13 +17,17 @@ export class FunctionEnvironmentTranslator { private readonly ssmValuePlaceholderText = ''; + // List of environment variable names for typed shim generation + private readonly amplifyBackendEnvVarNames: string[] = []; + /** * Initialize translated environment variable records */ constructor( private readonly lambda: NodejsFunction, // we need to use a specific type here so that we have all the method goodies private readonly functionEnvironmentProp: Required['environment'], - private readonly backendSecretResolver: BackendSecretResolver + private readonly backendSecretResolver: BackendSecretResolver, + private readonly functionEnvironmentTypeGenerator: FunctionEnvironmentTypeGenerator ) { for (const [key, value] of Object.entries(this.functionEnvironmentProp)) { if (key === this.amplifySsmEnvConfigKey) { @@ -42,6 +47,7 @@ export class FunctionEnvironmentTranslator { } else { this.lambda.addEnvironment(key, value); } + this.amplifyBackendEnvVarNames.push(key); } // add an environment variable for ssm parameter metadata that is resolved after initialization but before synth is finalized @@ -77,6 +83,16 @@ export class FunctionEnvironmentTranslator { return []; }, }); + + // Using CDK validation mechanism as a way to generate a typed process.env shim file at the end of synthesis + this.lambda.node.addValidation({ + validate: (): string[] => { + this.functionEnvironmentTypeGenerator.generateTypedProcessEnvShim( + this.amplifyBackendEnvVarNames + ); + return []; + }, + }); } /** @@ -88,6 +104,7 @@ export class FunctionEnvironmentTranslator { this.lambda.addEnvironment(name, this.ssmValuePlaceholderText); this.ssmPaths.push(ssmPath); this.ssmEnvVars[ssmPath] = { name }; + this.amplifyBackendEnvVarNames.push(name); }; } diff --git a/packages/backend-function/src/function_env_type_generator.test.ts b/packages/backend-function/src/function_env_type_generator.test.ts index b8b24d16cb..7febfd9232 100644 --- a/packages/backend-function/src/function_env_type_generator.test.ts +++ b/packages/backend-function/src/function_env_type_generator.test.ts @@ -19,13 +19,44 @@ void describe('FunctionEnvironmentTypeGenerator', () => { new FunctionEnvironmentTypeGenerator('testFunction'); const sampleStaticEnv = '_HANDLER: string;'; - functionEnvironmentTypeGenerator.generateTypeDefFile(); + functionEnvironmentTypeGenerator.generateTypedProcessEnvShim([]); // assert type definition file path assert.equal( fsWriteFileSyncMock.mock.calls[0].arguments[0], `${process.cwd()}/.amplify/function-env/testFunction.ts` ); + // assert content + assert.ok( + fsWriteFileSyncMock.mock.calls[0].arguments[1] + ?.toString() + .includes(sampleStaticEnv) + ); + + mock.restoreAll(); + }); + + void it('generates a type definition file with Amplify backend environment variables', () => { + const fdCloseMock = mock.fn(); + const fsOpenSyncMock = mock.method(fs, 'openSync'); + const fsWriteFileSyncMock = mock.method(fs, 'writeFileSync', () => null); + fsOpenSyncMock.mock.mockImplementation(() => { + return { + close: fdCloseMock, + }; + }); + const functionEnvironmentTypeGenerator = + new FunctionEnvironmentTypeGenerator('testFunction'); + const sampleStaticEnv = 'TEST_ENV: string;'; + + functionEnvironmentTypeGenerator.generateTypedProcessEnvShim(['TEST_ENV']); + + // assert type definition file path + assert.equal( + fsWriteFileSyncMock.mock.calls[0].arguments[0], + `${process.cwd()}/.amplify/function-env/testFunction.ts` + ); + // assert content assert.ok( fsWriteFileSyncMock.mock.calls[0].arguments[1] ?.toString() @@ -41,7 +72,7 @@ void describe('FunctionEnvironmentTypeGenerator', () => { new FunctionEnvironmentTypeGenerator('testFunction'); const filePath = `${process.cwd()}/.amplify/function-env/testFunction.ts`; - functionEnvironmentTypeGenerator.generateTypeDefFile(); + functionEnvironmentTypeGenerator.generateTypedProcessEnvShim(['TEST_ENV']); // import to validate syntax of type definition file await import(pathToFileURL(filePath).toString()); diff --git a/packages/backend-function/src/function_env_type_generator.ts b/packages/backend-function/src/function_env_type_generator.ts index 006b5f06bf..ba3bfb0d68 100644 --- a/packages/backend-function/src/function_env_type_generator.ts +++ b/packages/backend-function/src/function_env_type_generator.ts @@ -1,25 +1,30 @@ import fs from 'fs'; import { staticEnvironmentVariables } from './static_env_types.js'; import path from 'path'; -import os from 'os'; +import { EOL } from 'os'; /** - * Generates a type definition file for environment variables + * Generates a typed process.env shim for environment variables */ export class FunctionEnvironmentTypeGenerator { private typeDefFilePath: string; /** - * Initialize type definition file name and location + * Initialize typed process.env shim file name and location */ - constructor(functionName: string) { - this.typeDefFilePath = `${process.cwd()}/.amplify/function-env/${functionName}.ts`; + constructor(private readonly functionName: string) { + this.typeDefFilePath = `${process.cwd()}/.amplify/function-env/${ + this.functionName + }.ts`; } /** - * Generate a type definition file + * Generate a typed process.env shim */ - generateTypeDefFile() { + generateTypedProcessEnvShim(amplifyBackendEnvVars: string[]) { + const lambdaEnvVarTypeName = 'LambdaProvidedEnvVars'; + const amplifyBackendEnvVarTypeName = 'AmplifyBackendEnvVars'; + const declarations = []; const typeDefFileDirname = path.dirname(this.typeDefFilePath); @@ -27,18 +32,38 @@ export class FunctionEnvironmentTypeGenerator { fs.mkdirSync(typeDefFileDirname, { recursive: true }); } + // Add Lambda runtime environment variables to the typed shim + declarations.push( + `/** Lambda runtime environment variables, see https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime */` + ); + declarations.push(`type ${lambdaEnvVarTypeName} = {`); for (const key in staticEnvironmentVariables) { const comment = `/** ${staticEnvironmentVariables[key]} */`; const declaration = `${key}: string;`; - declarations.push(comment + os.EOL + declaration); + declarations.push(comment + EOL + declaration + EOL); } + declarations.push(`};${EOL}`); + + /** + * Add Amplify backend environment variables to the typed shim which can be either of the following: + * 1. Defined by the customer passing env vars to the environment parameter for defineFunction + * 2. Defined by resource access mechanisms + */ + declarations.push( + `/** Amplify backend environment variables available at runtime, this includes environment variables defined in \`defineFunction\` and by cross resource mechanisms */` + ); + declarations.push(`type ${amplifyBackendEnvVarTypeName} = {`); + amplifyBackendEnvVars.forEach((envName) => { + const declaration = `${envName}: string;`; + + declarations.push(declaration); + }); + declarations.push(`};${EOL}`); - const content = - `/** Lambda runtime environment variables, see https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime */${os.EOL}` + - `export const env = process.env as {${os.EOL}` + - declarations.join(os.EOL + os.EOL) + - `${os.EOL}};`; + const content = `export const env = process.env as ${lambdaEnvVarTypeName} & ${amplifyBackendEnvVarTypeName};${EOL}${EOL}${declarations.join( + EOL + )}`; fs.writeFileSync(this.typeDefFilePath, content); } diff --git a/packages/backend-output-storage/CHANGELOG.md b/packages/backend-output-storage/CHANGELOG.md index 860a3c88ad..fc2e5d8fe7 100644 --- a/packages/backend-output-storage/CHANGELOG.md +++ b/packages/backend-output-storage/CHANGELOG.md @@ -1,5 +1,12 @@ # @aws-amplify/backend-output-storage +## 0.4.0-beta.2 + +### Patch Changes + +- Updated dependencies [937086b] + - @aws-amplify/platform-core@0.5.0-beta.2 + ## 0.4.0-beta.1 ### Minor Changes diff --git a/packages/backend-output-storage/package.json b/packages/backend-output-storage/package.json index d0d09747b2..002a4bdd0f 100644 --- a/packages/backend-output-storage/package.json +++ b/packages/backend-output-storage/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-output-storage", - "version": "0.4.0-beta.1", + "version": "0.4.0-beta.2", "type": "commonjs", "publishConfig": { "access": "public" @@ -20,7 +20,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", - "@aws-amplify/platform-core": "^0.5.0-beta.1" + "@aws-amplify/platform-core": "^0.5.0-beta.2" }, "peerDependencies": { "aws-cdk-lib": "^2.127.0" diff --git a/packages/backend-secret/CHANGELOG.md b/packages/backend-secret/CHANGELOG.md index 62d1d73f27..e8cdd05540 100644 --- a/packages/backend-secret/CHANGELOG.md +++ b/packages/backend-secret/CHANGELOG.md @@ -1,5 +1,12 @@ # @aws-amplify/backend-secret +## 0.4.5-beta.2 + +### Patch Changes + +- Updated dependencies [937086b] + - @aws-amplify/platform-core@0.5.0-beta.2 + ## 0.4.5-beta.1 ### Patch Changes diff --git a/packages/backend-secret/package.json b/packages/backend-secret/package.json index 0a486c40d3..abea00eb45 100644 --- a/packages/backend-secret/package.json +++ b/packages/backend-secret/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-secret", - "version": "0.4.5-beta.1", + "version": "0.4.5-beta.2", "type": "module", "publishConfig": { "access": "public" @@ -19,7 +19,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-amplify/plugin-types": "^0.9.0-beta.0", - "@aws-amplify/platform-core": "^0.5.0-beta.1", + "@aws-amplify/platform-core": "^0.5.0-beta.2", "@aws-sdk/client-ssm": "^3.465.0" }, "devDependencies": { diff --git a/packages/backend-storage/API.md b/packages/backend-storage/API.md index dd358abc4f..6f0a314139 100644 --- a/packages/backend-storage/API.md +++ b/packages/backend-storage/API.md @@ -14,12 +14,9 @@ import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types'; import { ResourceProvider } from '@aws-amplify/plugin-types'; import { StorageOutput } from '@aws-amplify/backend-output-schemas'; -// @public (undocumented) -export type AccessGenerator = (allow: RoleAccessBuilder) => StorageAccessRecord; - // @public (undocumented) export type AmplifyStorageFactoryProps = Omit & { - access?: AccessGenerator; + access?: StorageAccessGenerator; }; // @public (undocumented) @@ -37,30 +34,37 @@ export type AmplifyStorageTriggerEvent = 'onDelete' | 'onUpload'; export const defineStorage: (props: AmplifyStorageFactoryProps) => ConstructFactory>; // @public -export type RoleAccessBuilder = { - authenticated: StorageAccessBuilder; - guest: StorageAccessBuilder; - owner: StorageAccessBuilder; - resource: (other: ConstructFactory) => StorageAccessBuilder; -}; +export type EntityId = 'identity'; -// @public (undocumented) +// @public export type StorageAccessBuilder = { - to: (actions: StorageAction[]) => StorageAccessDefinition; + authenticated: StorageActionBuilder; + guest: StorageActionBuilder; + group: (groupName: string) => StorageActionBuilder; + entity: (entityId: EntityId) => StorageActionBuilder; + resource: (other: ConstructFactory) => StorageActionBuilder; }; // @public (undocumented) export type StorageAccessDefinition = { getResourceAccessAcceptor: (getInstanceProps: ConstructFactoryGetInstanceProps) => ResourceAccessAcceptor; actions: StorageAction[]; - ownerPlaceholderSubstitution: string; + idSubstitution: string; }; +// @public (undocumented) +export type StorageAccessGenerator = (allow: StorageAccessBuilder) => StorageAccessRecord; + // @public (undocumented) export type StorageAccessRecord = Record; +// @public +export type StorageAction = 'read' | 'get' | 'list' | 'write' | 'delete'; + // @public (undocumented) -export type StorageAction = 'read' | 'write' | 'delete'; +export type StorageActionBuilder = { + to: (actions: StorageAction[]) => StorageAccessDefinition; +}; // @public export type StoragePath = `/${string}/*`; diff --git a/packages/backend-storage/CHANGELOG.md b/packages/backend-storage/CHANGELOG.md index e28ea4eae3..ca711b542c 100644 --- a/packages/backend-storage/CHANGELOG.md +++ b/packages/backend-storage/CHANGELOG.md @@ -1,5 +1,32 @@ # @aws-amplify/backend-storage +## 0.6.0-beta.4 + +### Patch Changes + +- @aws-amplify/backend-output-storage@0.4.0-beta.2 + +## 0.6.0-beta.3 + +### Minor Changes + +- f999897: Enable auth group access to storage and change syntax for specifying owner-based access + +## 0.6.0-beta.2 + +### Minor Changes + +- 5969a32: Implement deny-by-default behavior on access rules +- 215d65d: Group storage access policies by action rather than prefix +- 82006e5: Add "list" to available storage resource actions + +### Patch Changes + +- 64e425c: fix cogntio identity placeholder value in IAM policy +- c760df4: Use array input instead of var args for defining resource access actions +- 916d3f0: clean up s3 buckets when `defineStorage` is removed from the backend definition +- 3adf7df: Add validation for allowed path patterns in storage access definition + ## 0.6.0-beta.1 ### Minor Changes diff --git a/packages/backend-storage/package.json b/packages/backend-storage/package.json index 68830831c7..f9747f8d8d 100644 --- a/packages/backend-storage/package.json +++ b/packages/backend-storage/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-storage", - "version": "0.6.0-beta.1", + "version": "0.6.0-beta.4", "type": "module", "publishConfig": { "access": "public" @@ -19,12 +19,12 @@ "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", - "@aws-amplify/backend-output-storage": "^0.4.0-beta.1", + "@aws-amplify/backend-output-storage": "^0.4.0-beta.2", "@aws-amplify/plugin-types": "^0.9.0-beta.0" }, "devDependencies": { "@aws-amplify/backend-platform-test-stubs": "^0.3.3-beta.0", - "@aws-amplify/platform-core": "^0.5.0-beta.1" + "@aws-amplify/platform-core": "^0.5.0-beta.2" }, "peerDependencies": { "aws-cdk-lib": "^2.127.0", diff --git a/packages/backend-storage/src/access_builder.test.ts b/packages/backend-storage/src/access_builder.test.ts index eafb365e28..e8b190c2be 100644 --- a/packages/backend-storage/src/access_builder.test.ts +++ b/packages/backend-storage/src/access_builder.test.ts @@ -10,11 +10,15 @@ import { void describe('storageAccessBuilder', () => { const resourceAccessAcceptorMock = mock.fn(); + const groupAccessAcceptorMock = mock.fn(); const getResourceAccessAcceptorMock = mock.fn( // allows us to get proper typing on the mock args // eslint-disable-next-line @typescript-eslint/no-unused-vars - (_: string) => resourceAccessAcceptorMock + (roleName: string) => + roleName === 'testGroupName' + ? groupAccessAcceptorMock + : resourceAccessAcceptorMock ); const getConstructFactoryMock = mock.fn( @@ -50,7 +54,7 @@ void describe('storageAccessBuilder', () => { 'write', 'delete', ]); - assert.equal(accessDefinition.ownerPlaceholderSubstitution, '*'); + assert.equal(accessDefinition.idSubstitution, '*'); assert.equal( accessDefinition.getResourceAccessAcceptor(stubGetInstanceProps), resourceAccessAcceptorMock @@ -75,7 +79,7 @@ void describe('storageAccessBuilder', () => { 'write', 'delete', ]); - assert.equal(accessDefinition.ownerPlaceholderSubstitution, '*'); + assert.equal(accessDefinition.idSubstitution, '*'); assert.equal( accessDefinition.getResourceAccessAcceptor(stubGetInstanceProps), resourceAccessAcceptorMock @@ -89,20 +93,18 @@ void describe('storageAccessBuilder', () => { 'unauthenticatedUserIamRole' ); }); - void it('builds storage access definition for owner', () => { - const accessDefinition = roleAccessBuilder.owner.to([ - 'read', - 'write', - 'delete', - ]); + void it('builds storage access definition for IdP identity', () => { + const accessDefinition = roleAccessBuilder + .entity('identity') + .to(['read', 'write', 'delete']); assert.deepStrictEqual(accessDefinition.actions, [ 'read', 'write', 'delete', ]); assert.equal( - accessDefinition.ownerPlaceholderSubstitution, - '${cognito-identity.amazon.com:sub}' + accessDefinition.idSubstitution, + '${cognito-identity.amazonaws.com:sub}' ); assert.equal( accessDefinition.getResourceAccessAcceptor(stubGetInstanceProps), @@ -133,10 +135,23 @@ void describe('storageAccessBuilder', () => { 'write', 'delete', ]); - assert.equal(accessDefinition.ownerPlaceholderSubstitution, '*'); + assert.equal(accessDefinition.idSubstitution, '*'); assert.equal( accessDefinition.getResourceAccessAcceptor(stubGetInstanceProps), resourceAccessAcceptorMock ); }); + + void it('builds storage access definition for user pool groups', () => { + const accessDefinition = roleAccessBuilder + .group('testGroupName') + .to(['read', 'write']); + + assert.deepStrictEqual(accessDefinition.actions, ['read', 'write']); + assert.equal(accessDefinition.idSubstitution, '*'); + assert.equal( + accessDefinition.getResourceAccessAcceptor(stubGetInstanceProps), + groupAccessAcceptorMock + ); + }); }); diff --git a/packages/backend-storage/src/access_builder.ts b/packages/backend-storage/src/access_builder.ts index 0be1cb70ae..ebadabef05 100644 --- a/packages/backend-storage/src/access_builder.ts +++ b/packages/backend-storage/src/access_builder.ts @@ -4,36 +4,44 @@ import { ResourceAccessAcceptorFactory, ResourceProvider, } from '@aws-amplify/plugin-types'; -import { RoleAccessBuilder } from './types.js'; +import { StorageAccessBuilder } from './types.js'; -export const roleAccessBuilder: RoleAccessBuilder = { +export const roleAccessBuilder: StorageAccessBuilder = { authenticated: { to: (actions) => ({ getResourceAccessAcceptor: getAuthRoleResourceAccessAcceptor, actions, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }), }, guest: { to: (actions) => ({ getResourceAccessAcceptor: getUnauthRoleResourceAccessAcceptor, actions, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }), }, - owner: { + group: (groupName) => ({ + to: (actions) => ({ + getResourceAccessAcceptor: (getInstanceProps) => + getUserRoleResourceAccessAcceptor(getInstanceProps, groupName), + actions, + idSubstitution: '*', + }), + }), + entity: () => ({ to: (actions) => ({ getResourceAccessAcceptor: getAuthRoleResourceAccessAcceptor, actions, - ownerPlaceholderSubstitution: '${cognito-identity.amazon.com:sub}', + idSubstitution: '${cognito-identity.amazonaws.com:sub}', }), - }, + }), resource: (other) => ({ to: (actions) => ({ getResourceAccessAcceptor: (getInstanceProps) => other.getInstance(getInstanceProps).getResourceAccessAcceptor(), actions, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }), }), }; @@ -56,19 +64,19 @@ const getUnauthRoleResourceAccessAcceptor = ( const getUserRoleResourceAccessAcceptor = ( getInstanceProps: ConstructFactoryGetInstanceProps, - roleName: AuthRoleName + roleName: AuthRoleName | string ) => { const resourceAccessAcceptor = getInstanceProps.constructContainer .getConstructFactory< - ResourceProvider & ResourceAccessAcceptorFactory + ResourceProvider & ResourceAccessAcceptorFactory >('AuthResources') ?.getInstance(getInstanceProps) .getResourceAccessAcceptor(roleName); if (!resourceAccessAcceptor) { throw new Error( - `Cannot specify ${ + `Cannot specify auth access for ${ roleName as string - } user policies without defining auth. See for more information.` + } users without defining auth. See https://docs.amplify.aws/gen2/build-a-backend/auth/set-up-auth/ for more information.` ); } return resourceAccessAcceptor; diff --git a/packages/backend-storage/src/action_to_resources_map.ts b/packages/backend-storage/src/action_to_resources_map.ts deleted file mode 100644 index 209cc09590..0000000000 --- a/packages/backend-storage/src/action_to_resources_map.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { ResourceAccessAcceptor } from '@aws-amplify/plugin-types'; -import { StorageAction, StoragePath } from './types.js'; - -/** - * Internal collaborating class for maintaining the relationship between an acceptor token and the access map - */ -export class AcceptorTokenAccessMap { - /** - * Maintains a mapping of actions for each token - */ - private acceptorTokenAccessMap = new Map(); - - set = ( - resourceAccessAcceptor: ResourceAccessAcceptor, - actions: StorageAction[], - s3Prefix: S3Prefix - ) => { - const acceptorToken = resourceAccessAcceptor.identifier; - - if (!this.acceptorTokenAccessMap.has(acceptorToken)) { - this.acceptorTokenAccessMap.set(acceptorToken, { - actionMap: new S3PrefixActionMap(), - acceptor: resourceAccessAcceptor, - }); - } - const actionMap = this.acceptorTokenAccessMap.get(acceptorToken)!.actionMap; - actions.forEach((action) => { - actionMap.set(action, s3Prefix); - }); - }; - - getAccessList = () => { - const result: AccessEntry[] = []; - this.acceptorTokenAccessMap.forEach((value) => { - result.push(value); - }); - return result as Readonly[]>; - }; -} - -/** - * Internal collaborating class for maintaining the relationship between actions and resources - */ -class S3PrefixActionMap { - private actionToResourcesMap = new Map>(); - - /** - * Set an entry in the actionToResources Map that associates the resource with the action - */ - set = (action: StorageAction, s3Prefix: S3Prefix) => { - if (!this.actionToResourcesMap.has(action)) { - this.actionToResourcesMap.set(action, new Set()); - } - this.actionToResourcesMap.get(action)?.add(s3Prefix); - }; - - getActionToResourcesMap = () => { - return this.actionToResourcesMap as Readonly< - Map>> - >; - }; -} - -// some types internal to this file to improve readability - -type AcceptorToken = string; -type S3Prefix = string; - -type AccessEntry = { - actionMap: S3PrefixActionMap; - acceptor: ResourceAccessAcceptor; -}; diff --git a/packages/backend-storage/src/constants.ts b/packages/backend-storage/src/constants.ts index c32e06ce07..8ee0e17bd5 100644 --- a/packages/backend-storage/src/constants.ts +++ b/packages/backend-storage/src/constants.ts @@ -1 +1 @@ -export const ownerPathPartToken = '{owner}'; +export const entityIdPathToken = '{entity_id}'; diff --git a/packages/backend-storage/src/private_types.ts b/packages/backend-storage/src/private_types.ts index e3340158c7..0e10dbb5fb 100644 --- a/packages/backend-storage/src/private_types.ts +++ b/packages/backend-storage/src/private_types.ts @@ -2,7 +2,14 @@ * Types that should remain internal to the package */ +import { StorageAction } from './types.js'; + /** * Storage user error types */ export type StorageError = 'InvalidStorageAccessPathError'; + +/** + * StorageAction type intended to be used after mapping "read" to "get" and "list" + */ +export type InternalStorageAction = Exclude; diff --git a/packages/backend-storage/src/storage_access_orchestrator.test.ts b/packages/backend-storage/src/storage_access_orchestrator.test.ts new file mode 100644 index 0000000000..7e4456e65f --- /dev/null +++ b/packages/backend-storage/src/storage_access_orchestrator.test.ts @@ -0,0 +1,721 @@ +import { beforeEach, describe, it, mock } from 'node:test'; +import { StorageAccessOrchestrator } from './storage_access_orchestrator.js'; +import { ConstructFactoryGetInstanceProps } from '@aws-amplify/plugin-types'; +import { App, Stack } from 'aws-cdk-lib'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; +import assert from 'node:assert'; +import { entityIdPathToken } from './constants.js'; +import { StorageAccessPolicyFactory } from './storage_access_policy_factory.js'; + +void describe('StorageAccessOrchestrator', () => { + void describe('orchestrateStorageAccess', () => { + let stack: Stack; + let bucket: Bucket; + let storageAccessPolicyFactory: StorageAccessPolicyFactory; + + const ssmEnvironmentEntriesStub = [ + { name: 'TEST_BUCKET_NAME', path: 'test/ssm/path/to/bucket/name' }, + ]; + + beforeEach(() => { + stack = createStackAndSetContext(); + + bucket = new Bucket(stack, 'testBucket'); + + storageAccessPolicyFactory = new StorageAccessPolicyFactory(bucket); + }); + void it('throws if access prefixes are invalid', () => { + const acceptResourceAccessMock = mock.fn(); + const storageAccessOrchestrator = new StorageAccessOrchestrator( + () => ({ + '/test/prefix/*': [ + { + actions: ['get', 'write'], + getResourceAccessAcceptor: () => ({ + identifier: 'testResourceAccessAcceptor', + acceptResourceAccess: acceptResourceAccessMock, + }), + idSubstitution: '*', + }, + ], + }), + {} as unknown as ConstructFactoryGetInstanceProps, + ssmEnvironmentEntriesStub, + storageAccessPolicyFactory, + () => { + throw new Error('test validation error'); + } + ); + + assert.throws( + () => storageAccessOrchestrator.orchestrateStorageAccess(), + { message: 'test validation error' } + ); + }); + + void it('passes expected policy and ssm context to resource access acceptor', () => { + const acceptResourceAccessMock = mock.fn(); + const storageAccessOrchestrator = new StorageAccessOrchestrator( + () => ({ + '/test/prefix/*': [ + { + actions: ['get', 'write'], + getResourceAccessAcceptor: () => ({ + identifier: 'testResourceAccessAcceptor', + acceptResourceAccess: acceptResourceAccessMock, + }), + idSubstitution: '*', + }, + ], + }), + {} as unknown as ConstructFactoryGetInstanceProps, + ssmEnvironmentEntriesStub, + storageAccessPolicyFactory + ); + + storageAccessOrchestrator.orchestrateStorageAccess(); + assert.equal(acceptResourceAccessMock.mock.callCount(), 1); + assert.deepStrictEqual( + acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: 's3:GetObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/test/prefix/*`, + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/test/prefix/*`, + }, + ], + Version: '2012-10-17', + } + ); + assert.deepStrictEqual( + acceptResourceAccessMock.mock.calls[0].arguments[1], + ssmEnvironmentEntriesStub + ); + }); + + void it('handles multiple permissions for the same resource access acceptor', () => { + const acceptResourceAccessMock = mock.fn(); + const getResourceAccessAcceptorStub = () => ({ + identifier: 'testResourceAccessAcceptor', + acceptResourceAccess: acceptResourceAccessMock, + }); + const storageAccessOrchestrator = new StorageAccessOrchestrator( + () => ({ + '/test/prefix/*': [ + { + actions: ['get', 'write', 'delete'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub, + idSubstitution: '*', + }, + ], + '/another/prefix/*': [ + { + actions: ['get'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub, + idSubstitution: '*', + }, + ], + }), + {} as unknown as ConstructFactoryGetInstanceProps, + ssmEnvironmentEntriesStub, + storageAccessPolicyFactory + ); + + storageAccessOrchestrator.orchestrateStorageAccess(); + assert.equal(acceptResourceAccessMock.mock.callCount(), 1); + assert.deepStrictEqual( + acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: 's3:GetObject', + Effect: 'Allow', + Resource: [ + `${bucket.bucketArn}/test/prefix/*`, + `${bucket.bucketArn}/another/prefix/*`, + ], + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/test/prefix/*`, + }, + { + Action: 's3:DeleteObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/test/prefix/*`, + }, + ], + Version: '2012-10-17', + } + ); + assert.deepStrictEqual( + acceptResourceAccessMock.mock.calls[0].arguments[1], + ssmEnvironmentEntriesStub + ); + }); + + void it('handles multiple resource access acceptors', () => { + const acceptResourceAccessMock1 = mock.fn(); + const getResourceAccessAcceptorStub1 = () => ({ + identifier: 'testResourceAccessAcceptor1', + acceptResourceAccess: acceptResourceAccessMock1, + }); + const acceptResourceAccessMock2 = mock.fn(); + const getResourceAccessAcceptorStub2 = () => ({ + identifier: 'testResourceAccessAcceptor2', + acceptResourceAccess: acceptResourceAccessMock2, + }); + const storageAccessOrchestrator = new StorageAccessOrchestrator( + () => ({ + '/test/prefix/*': [ + { + actions: ['get', 'write', 'delete'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub1, + idSubstitution: '*', + }, + { + actions: ['get'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub2, + idSubstitution: '*', + }, + ], + '/another/prefix/*': [ + { + actions: ['get', 'delete'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub2, + idSubstitution: '*', + }, + ], + }), + {} as unknown as ConstructFactoryGetInstanceProps, + ssmEnvironmentEntriesStub, + storageAccessPolicyFactory + ); + + storageAccessOrchestrator.orchestrateStorageAccess(); + assert.equal(acceptResourceAccessMock1.mock.callCount(), 1); + assert.deepStrictEqual( + acceptResourceAccessMock1.mock.calls[0].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: 's3:GetObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/test/prefix/*`, + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/test/prefix/*`, + }, + { + Action: 's3:DeleteObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/test/prefix/*`, + }, + ], + Version: '2012-10-17', + } + ); + assert.equal(acceptResourceAccessMock2.mock.callCount(), 1); + assert.deepStrictEqual( + acceptResourceAccessMock2.mock.calls[0].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: 's3:GetObject', + Effect: 'Allow', + Resource: [ + `${bucket.bucketArn}/test/prefix/*`, + `${bucket.bucketArn}/another/prefix/*`, + ], + }, + { + Action: 's3:DeleteObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/another/prefix/*`, + }, + ], + Version: '2012-10-17', + } + ); + assert.deepStrictEqual( + acceptResourceAccessMock1.mock.calls[0].arguments[1], + ssmEnvironmentEntriesStub + ); + assert.deepStrictEqual( + acceptResourceAccessMock2.mock.calls[0].arguments[1], + ssmEnvironmentEntriesStub + ); + }); + + void it('replaces owner placeholder in s3 prefix', () => { + const acceptResourceAccessMock = mock.fn(); + const storageAccessOrchestrator = new StorageAccessOrchestrator( + () => ({ + [`/test/${entityIdPathToken}/*`]: [ + { + actions: ['get', 'write'], + getResourceAccessAcceptor: () => ({ + identifier: 'testResourceAccessAcceptor', + acceptResourceAccess: acceptResourceAccessMock, + }), + idSubstitution: '{testOwnerSub}', + }, + ], + }), + {} as unknown as ConstructFactoryGetInstanceProps, + ssmEnvironmentEntriesStub, + storageAccessPolicyFactory + ); + + storageAccessOrchestrator.orchestrateStorageAccess(); + assert.equal(acceptResourceAccessMock.mock.callCount(), 1); + assert.deepStrictEqual( + acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: 's3:GetObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/test/{testOwnerSub}/*`, + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/test/{testOwnerSub}/*`, + }, + ], + Version: '2012-10-17', + } + ); + assert.deepStrictEqual( + acceptResourceAccessMock.mock.calls[0].arguments[1], + ssmEnvironmentEntriesStub + ); + }); + + void it('denies parent actions on a subpath by default', () => { + const acceptResourceAccessMock1 = mock.fn(); + const acceptResourceAccessMock2 = mock.fn(); + const storageAccessOrchestrator = new StorageAccessOrchestrator( + () => ({ + '/foo/*': [ + { + actions: ['get', 'write'], + getResourceAccessAcceptor: () => ({ + identifier: 'resourceAccessAcceptor1', + acceptResourceAccess: acceptResourceAccessMock1, + }), + idSubstitution: '*', + }, + ], + '/foo/bar/*': [ + { + actions: ['get'], + getResourceAccessAcceptor: () => ({ + identifier: 'resourceAccessAcceptor2', + acceptResourceAccess: acceptResourceAccessMock2, + }), + idSubstitution: '*', + }, + ], + }), + {} as unknown as ConstructFactoryGetInstanceProps, + ssmEnvironmentEntriesStub, + storageAccessPolicyFactory + ); + + storageAccessOrchestrator.orchestrateStorageAccess(); + assert.equal(acceptResourceAccessMock1.mock.callCount(), 1); + assert.deepStrictEqual( + acceptResourceAccessMock1.mock.calls[0].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: 's3:GetObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/foo/*`, + }, + { + Action: 's3:GetObject', + Effect: 'Deny', + Resource: `${bucket.bucketArn}/foo/bar/*`, + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/foo/*`, + }, + { + Action: 's3:PutObject', + Effect: 'Deny', + Resource: `${bucket.bucketArn}/foo/bar/*`, + }, + ], + Version: '2012-10-17', + } + ); + assert.deepStrictEqual( + acceptResourceAccessMock1.mock.calls[0].arguments[1], + ssmEnvironmentEntriesStub + ); + + assert.equal(acceptResourceAccessMock2.mock.callCount(), 1); + assert.deepStrictEqual( + acceptResourceAccessMock2.mock.calls[0].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: 's3:GetObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/foo/bar/*`, + }, + ], + Version: '2012-10-17', + } + ); + }); + + void it('combines owner rules for same resource access acceptor', () => { + const acceptResourceAccessMock = mock.fn(); + const authenticatedResourceAccessAcceptor = () => ({ + identifier: 'authenticatedResourceAccessAcceptor', + acceptResourceAccess: acceptResourceAccessMock, + }); + + const storageAccessOrchestrator = new StorageAccessOrchestrator( + () => ({ + '/foo/{entity_id}/*': [ + { + actions: ['write', 'delete'], + getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, + idSubstitution: '{idSub}', + }, + { + actions: ['get'], + getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, + idSubstitution: '*', + }, + ], + }), + {} as unknown as ConstructFactoryGetInstanceProps, + ssmEnvironmentEntriesStub, + storageAccessPolicyFactory + ); + + storageAccessOrchestrator.orchestrateStorageAccess(); + assert.equal(acceptResourceAccessMock.mock.callCount(), 1); + assert.deepStrictEqual( + acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: 's3:PutObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/foo/{idSub}/*`, + }, + { + Action: 's3:DeleteObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/foo/{idSub}/*`, + }, + { + Action: 's3:GetObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/foo/*/*`, + }, + ], + Version: '2012-10-17', + } + ); + assert.deepStrictEqual( + acceptResourceAccessMock.mock.calls[0].arguments[1], + ssmEnvironmentEntriesStub + ); + }); + + void it('handles multiple resource access acceptors on multiple prefixes', () => { + const acceptResourceAccessMock1 = mock.fn(); + const acceptResourceAccessMock2 = mock.fn(); + const getResourceAccessAcceptorStub1 = () => ({ + identifier: 'resourceAccessAcceptor1', + acceptResourceAccess: acceptResourceAccessMock1, + }); + const getResourceAccessAcceptorStub2 = () => ({ + identifier: 'resourceAccessAcceptor2', + acceptResourceAccess: acceptResourceAccessMock2, + }); + + const storageAccessOrchestrator = new StorageAccessOrchestrator( + () => ({ + // acceptor1 should have read write on this path + // acceptor2 should not have any rules for this path + '/foo/*': [ + { + actions: ['get', 'write'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub1, + idSubstitution: '*', + }, + ], + // acceptor1 should be denied read and write on this path + // acceptor2 should have only read on this path + '/foo/bar/*': [ + { + actions: ['get'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub2, + idSubstitution: '{idSub}', + }, + ], + // acceptor1 should be denied write on this path (read from parent path covers read on this path) + // acceptor2 should not have any rules for this path + '/foo/baz/*': [ + { + actions: ['get'], + idSubstitution: '*', + getResourceAccessAcceptor: getResourceAccessAcceptorStub1, + }, + ], + // acceptor 1 is denied write on this path (read still allowed) + // acceptor 2 has read/write/delete on path with ownerSub + '/other/{entity_id}/*': [ + { + actions: ['get', 'write', 'delete'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub2, + idSubstitution: '{idSub}', + }, + { + actions: ['get'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub1, + idSubstitution: '*', + }, + ], + }), + {} as unknown as ConstructFactoryGetInstanceProps, + ssmEnvironmentEntriesStub, + storageAccessPolicyFactory + ); + + storageAccessOrchestrator.orchestrateStorageAccess(); + assert.equal(acceptResourceAccessMock1.mock.callCount(), 1); + assert.deepStrictEqual( + acceptResourceAccessMock1.mock.calls[0].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: 's3:GetObject', + Effect: 'Allow', + Resource: [ + `${bucket.bucketArn}/foo/*`, + `${bucket.bucketArn}/other/*/*`, + ], + }, + { + Action: 's3:GetObject', + Effect: 'Deny', + Resource: `${bucket.bucketArn}/foo/bar/*`, + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/foo/*`, + }, + { + Action: 's3:PutObject', + Effect: 'Deny', + Resource: [ + `${bucket.bucketArn}/foo/bar/*`, + `${bucket.bucketArn}/foo/baz/*`, + ], + }, + ], + Version: '2012-10-17', + } + ); + assert.deepStrictEqual( + acceptResourceAccessMock1.mock.calls[0].arguments[1], + ssmEnvironmentEntriesStub + ); + + assert.equal(acceptResourceAccessMock2.mock.callCount(), 1); + assert.deepStrictEqual( + acceptResourceAccessMock2.mock.calls[0].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: 's3:GetObject', + Effect: 'Allow', + Resource: [ + `${bucket.bucketArn}/foo/bar/*`, + `${bucket.bucketArn}/other/{idSub}/*`, + ], + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/other/{idSub}/*`, + }, + { + Action: 's3:DeleteObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/other/{idSub}/*`, + }, + ], + Version: '2012-10-17', + } + ); + }); + + void it('combines actions from multiple rules on the same resource access acceptor', () => { + const acceptResourceAccessMock = mock.fn(); + const authenticatedResourceAccessAcceptor = () => ({ + identifier: 'authenticatedResourceAccessAcceptor', + acceptResourceAccess: acceptResourceAccessMock, + }); + + const storageAccessOrchestrator = new StorageAccessOrchestrator( + () => ({ + '/foo/*': [ + { + actions: ['get'], + getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, + idSubstitution: '*', + }, + { + actions: ['write'], + getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, + idSubstitution: '{idSub}', + }, + { + actions: ['delete'], + getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, + idSubstitution: '*', + }, + ], + }), + {} as unknown as ConstructFactoryGetInstanceProps, + ssmEnvironmentEntriesStub, + storageAccessPolicyFactory + ); + + storageAccessOrchestrator.orchestrateStorageAccess(); + assert.equal(acceptResourceAccessMock.mock.callCount(), 1); + assert.deepStrictEqual( + acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: 's3:GetObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/foo/*`, + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/foo/*`, + }, + { + Action: 's3:DeleteObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/foo/*`, + }, + ], + Version: '2012-10-17', + } + ); + assert.deepStrictEqual( + acceptResourceAccessMock.mock.calls[0].arguments[1], + ssmEnvironmentEntriesStub + ); + }); + + void it('replaces "read" access with "get" and "list" and merges duplicate actions', () => { + const acceptResourceAccessMock = mock.fn(); + const authenticatedResourceAccessAcceptor = () => ({ + identifier: 'authenticatedResourceAccessAcceptor', + acceptResourceAccess: acceptResourceAccessMock, + }); + + const storageAccessOrchestrator = new StorageAccessOrchestrator( + () => ({ + '/foo/bar/*': [ + { + actions: ['read', 'get', 'list'], + getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, + idSubstitution: '*', + }, + { + actions: ['list'], + getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, + idSubstitution: '*', + }, + ], + '/other/baz/*': [ + { + actions: ['read'], + getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, + idSubstitution: '*', + }, + ], + }), + {} as unknown as ConstructFactoryGetInstanceProps, + ssmEnvironmentEntriesStub, + storageAccessPolicyFactory + ); + + storageAccessOrchestrator.orchestrateStorageAccess(); + assert.equal(acceptResourceAccessMock.mock.callCount(), 1); + assert.deepStrictEqual( + acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: 's3:GetObject', + Effect: 'Allow', + Resource: [ + `${bucket.bucketArn}/foo/bar/*`, + `${bucket.bucketArn}/other/baz/*`, + ], + }, + { + Action: 's3:ListBucket', + Effect: 'Allow', + Resource: bucket.bucketArn, + Condition: { + StringLike: { + 's3:prefix': [ + 'foo/bar/*', + 'foo/bar/', + 'other/baz/*', + 'other/baz/', + ], + }, + }, + }, + ], + Version: '2012-10-17', + } + ); + assert.deepStrictEqual( + acceptResourceAccessMock.mock.calls[0].arguments[1], + ssmEnvironmentEntriesStub + ); + }); + }); +}); + +const createStackAndSetContext = (): Stack => { + const app = new App(); + app.node.setContext('amplify-backend-name', 'testEnvName'); + app.node.setContext('amplify-backend-namespace', 'testBackendId'); + app.node.setContext('amplify-backend-type', 'branch'); + const stack = new Stack(app); + return stack; +}; diff --git a/packages/backend-storage/src/storage_access_orchestrator.ts b/packages/backend-storage/src/storage_access_orchestrator.ts new file mode 100644 index 0000000000..c1dccaccb0 --- /dev/null +++ b/packages/backend-storage/src/storage_access_orchestrator.ts @@ -0,0 +1,263 @@ +import { + ConstructFactoryGetInstanceProps, + ResourceAccessAcceptor, + SsmEnvironmentEntry, +} from '@aws-amplify/plugin-types'; +import { + StorageAccessBuilder, + StorageAccessGenerator, + StoragePath, +} from './types.js'; +import { entityIdPathToken } from './constants.js'; +import { StorageAccessPolicyFactory } from './storage_access_policy_factory.js'; +import { validateStorageAccessPaths as _validateStorageAccessPaths } from './validate_storage_access_paths.js'; +import { roleAccessBuilder as _roleAccessBuilder } from './access_builder.js'; +import { InternalStorageAction } from './private_types.js'; + +/* some types internal to this file to improve readability */ + +// Alias type for a string that is a ResourceAccessAcceptor token +type AcceptorToken = string; + +// Callback function that places storagePath in the deny list for an action if it is not explicitly allowed by another rule +type SetDenyByDefault = (storagePath: StoragePath) => void; + +/** + * Orchestrates the process of converting customer-defined storage access rules into corresponding IAM policies + * and attaching those policies to the corresponding IAM roles + */ +export class StorageAccessOrchestrator { + /** + * Maintains a mapping from a resource access acceptor to all of the access grants it has been configured with + * Each entry of this map is fed into the policy generator to create a single policy for each acceptor + */ + private acceptorAccessMap = new Map< + AcceptorToken, + { + acceptor: ResourceAccessAcceptor; + accessMap: Map< + InternalStorageAction, + { allow: Set; deny: Set } + >; + } + >(); + + /** + * Maintains pointers to the "deny" StoragePath Set for each access entry in the map above + * This map is used during a final pass over all the StoragePaths to deny access on any paths where explicit allow rules were not specified + */ + private prefixDenyMap = new Map(); + + /** + * Instantiate with the access generator and other dependencies necessary for evaluating and constructing access policies + * @param storageAccessGenerator The access callback defined by the customer + * @param getInstanceProps props for fetching construct instances from the construct container + * @param ssmEnvironmentEntries SSM context that should be passed to the ResourceAccessAcceptors when configuring access + * @param policyFactory factory that generates IAM policies for various access control definitions + * @param validateStorageAccessPaths validator function for checking access definition paths + * @param roleAccessBuilder builder instance that is injected into the storageAccessGenerator to evaluate the rules + */ + constructor( + private readonly storageAccessGenerator: StorageAccessGenerator, + private readonly getInstanceProps: ConstructFactoryGetInstanceProps, + private readonly ssmEnvironmentEntries: SsmEnvironmentEntry[], + private readonly policyFactory: StorageAccessPolicyFactory, + private readonly validateStorageAccessPaths = _validateStorageAccessPaths, + private readonly roleAccessBuilder: StorageAccessBuilder = _roleAccessBuilder + ) {} + + /** + * Orchestrates the process of translating the customer-provided storage access rules into IAM policies and attaching those policies to the appropriate roles. + * + * The high level steps are: + * 1. Invokes the storageAccessGenerator to produce a storageAccessDefinition + * 2. Validates the paths in the storageAccessDefinition + * 3. Organizes the storageAccessDefinition into internally managed maps to facilitate translation into allow / deny rules on IAM policies + * 4. Invokes the policy generator to produce a policy with appropriate allow / deny rules + * 5. Invokes the resourceAccessAcceptors for each entry in the storageAccessDefinition to accept the corresponding IAM policy + */ + orchestrateStorageAccess = () => { + // storageAccessGenerator is the access callback defined by the customer + // here we inject the roleAccessBuilder into the callback and run it + // this produces the access definition that will be used to create the storage policies + const storageAccessDefinition = this.storageAccessGenerator( + this.roleAccessBuilder + ); + + // verify that the paths in the access definition are valid + this.validateStorageAccessPaths(Object.keys(storageAccessDefinition)); + + // iterate over the access definition and group permissions by ResourceAccessAcceptor + Object.entries(storageAccessDefinition).forEach( + // in the access definition, permissions are grouped by storage prefix + ([s3Prefix, accessPermissions]) => { + // iterate over all of the access definitions for a given prefix + accessPermissions.forEach((permission) => { + // get the ResourceAccessAcceptor for the permission and add it to the map if not already present + const resourceAccessAcceptor = permission.getResourceAccessAcceptor( + this.getInstanceProps + ); + + // make the owner placeholder substitution in the s3 prefix + const prefix = s3Prefix.replaceAll( + entityIdPathToken, + permission.idSubstitution + ) as StoragePath; + + // replace "read" with "get" and "list" in actions + const replaceReadWithGetAndList = permission.actions.flatMap( + (action) => (action === 'read' ? ['get', 'list'] : [action]) + ) as InternalStorageAction[]; + + // ensure the actions list has no duplicates + const noDuplicateActions = Array.from( + new Set(replaceReadWithGetAndList) + ); + + // set an entry that maps this permission to the resource acceptor + this.addAccessDefinition( + resourceAccessAcceptor, + noDuplicateActions, + prefix + ); + }); + } + ); + + // iterate over the access map entries and invoke each ResourceAccessAcceptor to accept the permissions + this.attachPolicies(this.ssmEnvironmentEntries); + }; + + /** + * Add an entry to the internal acceptorAccessMap and prefixDenyMap. + * This entry defines a set of actions on a single s3 prefix that should be attached to a given ResourceAccessAcceptor + */ + private addAccessDefinition = ( + resourceAccessAcceptor: ResourceAccessAcceptor, + actions: InternalStorageAction[], + s3Prefix: StoragePath + ) => { + const acceptorToken = resourceAccessAcceptor.identifier; + + // if we haven't seen this token before, add it to the map + if (!this.acceptorAccessMap.has(acceptorToken)) { + this.acceptorAccessMap.set(acceptorToken, { + accessMap: new Map(), + acceptor: resourceAccessAcceptor, + }); + } + const accessMap = this.acceptorAccessMap.get(acceptorToken)!.accessMap; + // add each action to the accessMap for this acceptorToken + actions.forEach((action) => { + if (!accessMap.has(action)) { + // if we haven't seen this action for this acceptorToken before, add it to the map + const allowSet = new Set([s3Prefix]); + const denySet = new Set(); + accessMap.set(action, { allow: allowSet, deny: denySet }); + + // this is where we create the reverse mapping that allows us to add entries to the denySet later by looking up the prefix + this.setPrefixDenyMapEntry(s3Prefix, allowSet, denySet); + } else { + // otherwise add the prefix to the existing allow set + const { allow: allowSet, deny: denySet } = accessMap.get(action)!; + allowSet.add(s3Prefix); + + // add an entry in the prefixDenyMap for the existing allow and deny set + this.setPrefixDenyMapEntry(s3Prefix, allowSet, denySet); + } + }); + }; + + /** + * Iterates over all of the access definitions that have been added to the orchestrator, + * generates a policy for each accessMap, + * and attaches the policy to the corresponding ResourceAccessAcceptor + * + * After this method is called, the existing access definition state is cleared. + * This prevents multiple calls to this method from producing duplicate policies. + * The class can continue to be used to build up state for a new set of policies if desired. + * @param ssmEnvironmentEntries Additional SSM context that is passed to each ResourceAccessAcceptor + */ + private attachPolicies = (ssmEnvironmentEntries: SsmEnvironmentEntry[]) => { + const allPaths = Array.from(this.prefixDenyMap.keys()); + allPaths.forEach((storagePath) => { + const parent = findParent(storagePath, allPaths); + if (!parent) { + return; + } + // if a parent path is defined, invoke the denyByDefault callback on this subpath for all policies that exist on the parent path + this.prefixDenyMap + .get(parent) + ?.forEach((denyByDefaultCallback) => + denyByDefaultCallback(storagePath) + ); + }); + + this.acceptorAccessMap.forEach(({ acceptor, accessMap }) => { + // removing subpaths from the allow set prevents unnecessary paths from being added to the policy + // for example, if there are allow read rules for /foo/* and /foo/bar/* we only need to add /foo/* to the policy because that includes /foo/bar/* + accessMap.forEach(({ allow }) => { + removeSubPathsFromSet(allow); + }); + acceptor.acceptResourceAccess( + this.policyFactory.createPolicy(accessMap), + ssmEnvironmentEntries + ); + }); + this.acceptorAccessMap.clear(); + this.prefixDenyMap.clear(); + }; + + private setPrefixDenyMapEntry = ( + storagePath: StoragePath, + allowPathSet: Set, + denyPathSet: Set + ) => { + // function that will add the denyPath to the denyPathSet unless the allowPathSet explicitly allows the path + const setDenyByDefault = (denyPath: StoragePath) => { + if (!allowPathSet.has(denyPath)) { + denyPathSet.add(denyPath); + } + }; + if (!this.prefixDenyMap.has(storagePath)) { + this.prefixDenyMap.set(storagePath, [setDenyByDefault]); + } else { + this.prefixDenyMap.get(storagePath)?.push(setDenyByDefault); + } + }; +} + +/** + * This factory is really only necessary for allowing us to mock the StorageAccessOrchestrator in tests + */ +export class StorageAccessOrchestratorFactory { + getInstance = ( + storageAccessGenerator: StorageAccessGenerator, + getInstanceProps: ConstructFactoryGetInstanceProps, + ssmEnvironmentEntries: SsmEnvironmentEntry[], + policyFactory: StorageAccessPolicyFactory + ) => + new StorageAccessOrchestrator( + storageAccessGenerator, + getInstanceProps, + ssmEnvironmentEntries, + policyFactory + ); +} + +/** + * Returns the element in paths that is a prefix of path, if any + * Note that there can only be one at this point because of upstream validation + */ +const findParent = (path: string, paths: string[]) => + paths.find((p) => path !== p && path.startsWith(p.replaceAll('*', ''))) as + | StoragePath + | undefined; + +const removeSubPathsFromSet = (paths: Set) => { + paths.forEach((path) => { + if (findParent(path, Array.from(paths))) { + paths.delete(path); + } + }); +}; diff --git a/packages/backend-storage/src/storage_access_policy_arbiter.test.ts b/packages/backend-storage/src/storage_access_policy_arbiter.test.ts deleted file mode 100644 index 3be7292879..0000000000 --- a/packages/backend-storage/src/storage_access_policy_arbiter.test.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { beforeEach, describe, it, mock } from 'node:test'; -import { StorageAccessPolicyArbiter } from './storage_access_policy_arbiter.js'; -import { StackMetadataBackendOutputStorageStrategy } from '@aws-amplify/backend-output-storage'; -import { - ConstructContainerStub, - ImportPathVerifierStub, - StackResolverStub, -} from '@aws-amplify/backend-platform-test-stubs'; -import { - BackendOutputEntry, - BackendOutputStorageStrategy, - ConstructContainer, - ConstructFactoryGetInstanceProps, - ImportPathVerifier, - SsmEnvironmentEntriesGenerator, -} from '@aws-amplify/plugin-types'; -import { App, Stack } from 'aws-cdk-lib'; -import { Bucket } from 'aws-cdk-lib/aws-s3'; -import assert from 'node:assert'; -import { ownerPathPartToken } from './constants.js'; - -void describe('StorageAccessPolicyArbiter', () => { - void describe('arbitratePolicies', () => { - let stack: Stack; - let constructContainer: ConstructContainer; - let outputStorageStrategy: BackendOutputStorageStrategy; - let importPathVerifier: ImportPathVerifier; - let getInstanceProps: ConstructFactoryGetInstanceProps; - - const ssmEnvironmentEntriesGeneratorStub: SsmEnvironmentEntriesGenerator = { - generateSsmEnvironmentEntries: mock.fn(() => [ - { name: 'TEST_BUCKET_NAME', path: 'test/ssm/path/to/bucket/name' }, - ]), - }; - - beforeEach(() => { - stack = createStackAndSetContext(); - - constructContainer = new ConstructContainerStub( - new StackResolverStub(stack) - ); - - outputStorageStrategy = new StackMetadataBackendOutputStorageStrategy( - stack - ); - - importPathVerifier = new ImportPathVerifierStub(); - - getInstanceProps = { - constructContainer, - outputStorageStrategy, - importPathVerifier, - }; - }); - void it('passes expected policy and ssm context to resource access acceptor', () => { - const bucket = new Bucket(stack, 'testBucket'); - const acceptResourceAccessMock = mock.fn(); - const storageAccessPolicyArbiter = new StorageAccessPolicyArbiter( - 'testName', - { - '/test/prefix/*': [ - { - actions: ['read', 'write'], - getResourceAccessAcceptor: () => ({ - identifier: 'testResourceAccessAcceptor', - acceptResourceAccess: acceptResourceAccessMock, - }), - ownerPlaceholderSubstitution: '*', - }, - ], - }, - ssmEnvironmentEntriesGeneratorStub, - getInstanceProps, - bucket - ); - - storageAccessPolicyArbiter.arbitratePolicies(); - assert.equal(acceptResourceAccessMock.mock.callCount(), 1); - assert.deepStrictEqual( - acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), - { - Statement: [ - { - Action: 's3:GetObject', - Effect: 'Allow', - Resource: `${bucket.bucketArn}/test/prefix/*`, - }, - { - Action: 's3:PutObject', - Effect: 'Allow', - Resource: `${bucket.bucketArn}/test/prefix/*`, - }, - ], - Version: '2012-10-17', - } - ); - assert.deepStrictEqual( - acceptResourceAccessMock.mock.calls[0].arguments[1], - [{ name: 'TEST_BUCKET_NAME', path: 'test/ssm/path/to/bucket/name' }] - ); - }); - - void it('handles multiple permissions for the same resource access acceptor', () => { - const bucket = new Bucket(stack, 'testBucket'); - const acceptResourceAccessMock = mock.fn(); - const getResourceAccessAcceptorStub = () => ({ - identifier: 'testResourceAccessAcceptor', - acceptResourceAccess: acceptResourceAccessMock, - }); - const storageAccessPolicyArbiter = new StorageAccessPolicyArbiter( - 'testName', - { - '/test/prefix/*': [ - { - actions: ['read', 'write', 'delete'], - getResourceAccessAcceptor: getResourceAccessAcceptorStub, - ownerPlaceholderSubstitution: '*', - }, - ], - '/another/prefix/*': [ - { - actions: ['read'], - getResourceAccessAcceptor: getResourceAccessAcceptorStub, - ownerPlaceholderSubstitution: '*', - }, - ], - }, - ssmEnvironmentEntriesGeneratorStub, - getInstanceProps, - bucket - ); - - storageAccessPolicyArbiter.arbitratePolicies(); - assert.equal(acceptResourceAccessMock.mock.callCount(), 1); - assert.deepStrictEqual( - acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), - { - Statement: [ - { - Action: 's3:GetObject', - Effect: 'Allow', - Resource: [ - `${bucket.bucketArn}/test/prefix/*`, - `${bucket.bucketArn}/another/prefix/*`, - ], - }, - { - Action: 's3:PutObject', - Effect: 'Allow', - Resource: `${bucket.bucketArn}/test/prefix/*`, - }, - { - Action: 's3:DeleteObject', - Effect: 'Allow', - Resource: `${bucket.bucketArn}/test/prefix/*`, - }, - ], - Version: '2012-10-17', - } - ); - assert.deepStrictEqual( - acceptResourceAccessMock.mock.calls[0].arguments[1], - [{ name: 'TEST_BUCKET_NAME', path: 'test/ssm/path/to/bucket/name' }] - ); - }); - - void it('handles multiple resource access acceptors', () => { - const bucket = new Bucket(stack, 'testBucket'); - const acceptResourceAccessMock1 = mock.fn(); - const getResourceAccessAcceptorStub1 = () => ({ - identifier: 'testResourceAccessAcceptor1', - acceptResourceAccess: acceptResourceAccessMock1, - }); - const acceptResourceAccessMock2 = mock.fn(); - const getResourceAccessAcceptorStub2 = () => ({ - identifier: 'testResourceAccessAcceptor2', - acceptResourceAccess: acceptResourceAccessMock2, - }); - const storageAccessPolicyArbiter = new StorageAccessPolicyArbiter( - 'testName', - { - '/test/prefix/*': [ - { - actions: ['read', 'write', 'delete'], - getResourceAccessAcceptor: getResourceAccessAcceptorStub1, - ownerPlaceholderSubstitution: '*', - }, - { - actions: ['read'], - getResourceAccessAcceptor: getResourceAccessAcceptorStub2, - ownerPlaceholderSubstitution: '*', - }, - ], - '/another/prefix/*': [ - { - actions: ['read', 'delete'], - getResourceAccessAcceptor: getResourceAccessAcceptorStub2, - ownerPlaceholderSubstitution: '*', - }, - ], - }, - ssmEnvironmentEntriesGeneratorStub, - getInstanceProps, - bucket - ); - - storageAccessPolicyArbiter.arbitratePolicies(); - assert.equal(acceptResourceAccessMock1.mock.callCount(), 1); - assert.deepStrictEqual( - acceptResourceAccessMock1.mock.calls[0].arguments[0].document.toJSON(), - { - Statement: [ - { - Action: 's3:GetObject', - Effect: 'Allow', - Resource: `${bucket.bucketArn}/test/prefix/*`, - }, - { - Action: 's3:PutObject', - Effect: 'Allow', - Resource: `${bucket.bucketArn}/test/prefix/*`, - }, - { - Action: 's3:DeleteObject', - Effect: 'Allow', - Resource: `${bucket.bucketArn}/test/prefix/*`, - }, - ], - Version: '2012-10-17', - } - ); - assert.equal(acceptResourceAccessMock2.mock.callCount(), 1); - assert.deepStrictEqual( - acceptResourceAccessMock2.mock.calls[0].arguments[0].document.toJSON(), - { - Statement: [ - { - Action: 's3:GetObject', - Effect: 'Allow', - Resource: [ - `${bucket.bucketArn}/test/prefix/*`, - `${bucket.bucketArn}/another/prefix/*`, - ], - }, - { - Action: 's3:DeleteObject', - Effect: 'Allow', - Resource: `${bucket.bucketArn}/another/prefix/*`, - }, - ], - Version: '2012-10-17', - } - ); - assert.deepStrictEqual( - acceptResourceAccessMock1.mock.calls[0].arguments[1], - [{ name: 'TEST_BUCKET_NAME', path: 'test/ssm/path/to/bucket/name' }] - ); - assert.deepStrictEqual( - acceptResourceAccessMock2.mock.calls[0].arguments[1], - [{ name: 'TEST_BUCKET_NAME', path: 'test/ssm/path/to/bucket/name' }] - ); - }); - - void it('replaces owner placeholder in s3 prefix', () => { - const bucket = new Bucket(stack, 'testBucket'); - const acceptResourceAccessMock = mock.fn(); - const storageAccessPolicyArbiter = new StorageAccessPolicyArbiter( - 'testName', - { - [`/test/${ownerPathPartToken}/*`]: [ - { - actions: ['read', 'write'], - getResourceAccessAcceptor: () => ({ - identifier: 'testResourceAccessAcceptor', - acceptResourceAccess: acceptResourceAccessMock, - }), - ownerPlaceholderSubstitution: '{testOwnerSub}', - }, - ], - }, - ssmEnvironmentEntriesGeneratorStub, - getInstanceProps, - bucket - ); - - storageAccessPolicyArbiter.arbitratePolicies(); - assert.equal(acceptResourceAccessMock.mock.callCount(), 1); - assert.deepStrictEqual( - acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), - { - Statement: [ - { - Action: 's3:GetObject', - Effect: 'Allow', - Resource: `${bucket.bucketArn}/test/{testOwnerSub}/*`, - }, - { - Action: 's3:PutObject', - Effect: 'Allow', - Resource: `${bucket.bucketArn}/test/{testOwnerSub}/*`, - }, - ], - Version: '2012-10-17', - } - ); - assert.deepStrictEqual( - acceptResourceAccessMock.mock.calls[0].arguments[1], - [{ name: 'TEST_BUCKET_NAME', path: 'test/ssm/path/to/bucket/name' }] - ); - }); - }); -}); - -const createStackAndSetContext = (): Stack => { - const app = new App(); - app.node.setContext('amplify-backend-name', 'testEnvName'); - app.node.setContext('amplify-backend-namespace', 'testBackendId'); - app.node.setContext('amplify-backend-type', 'branch'); - const stack = new Stack(app); - return stack; -}; diff --git a/packages/backend-storage/src/storage_access_policy_arbiter.ts b/packages/backend-storage/src/storage_access_policy_arbiter.ts deleted file mode 100644 index 51a5d447cc..0000000000 --- a/packages/backend-storage/src/storage_access_policy_arbiter.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { - ConstructFactoryGetInstanceProps, - SsmEnvironmentEntriesGenerator, -} from '@aws-amplify/plugin-types'; -import { StorageAccessDefinition, StoragePath } from './types.js'; -import { IBucket } from 'aws-cdk-lib/aws-s3'; -import { StorageAccessPolicyFactory } from './storage_access_policy_factory.js'; -import { ownerPathPartToken } from './constants.js'; -import { AcceptorTokenAccessMap } from './action_to_resources_map.js'; - -/** - * Middleman between creating bucket policies and attaching those policies to corresponding roles - */ -export class StorageAccessPolicyArbiter { - /** - * Instantiate with context from the storage factory - */ - constructor( - private readonly name: string, - private readonly accessDefinition: Record< - StoragePath, - StorageAccessDefinition[] - >, - private readonly ssmEnvironmentEntriesGenerator: SsmEnvironmentEntriesGenerator, - private readonly getInstanceProps: ConstructFactoryGetInstanceProps, - private readonly bucket: IBucket, - private readonly storageAccessPolicyFactory = new StorageAccessPolicyFactory( - bucket - ) - ) {} - - /** - * Responsible for creating bucket policies corresponding to the definition, - * then invoking the corresponding ResourceAccessAcceptor to accept the policies - */ - arbitratePolicies = () => { - const acceptorTokenAccessMap = new AcceptorTokenAccessMap(); - - // iterate over the access definition and group permissions by ResourceAccessAcceptor - Object.entries(this.accessDefinition).forEach( - // in the access definition, permissions are grouped by storage prefix - ([s3Prefix, accessPermissions]) => { - // iterate over all of the access definitions for a given prefix - accessPermissions.forEach((permission) => { - // get the ResourceAccessAcceptor for the permission and add it to the map if not already present - const resourceAccessAcceptor = permission.getResourceAccessAcceptor( - this.getInstanceProps - ); - - // make the owner placeholder substitution in the s3 prefix - const prefix = s3Prefix.replaceAll( - ownerPathPartToken, - permission.ownerPlaceholderSubstitution - ); - - acceptorTokenAccessMap.set( - resourceAccessAcceptor, - permission.actions, - prefix - ); - }); - } - ); - - // generate the ssm environment context necessary to access the s3 bucket (in this case, just the bucket name) - const ssmEnvironmentEntries = - this.ssmEnvironmentEntriesGenerator.generateSsmEnvironmentEntries({ - [`${this.name}_BUCKET_NAME`]: this.bucket.bucketName, - }); - - // iterate over the access map entries and invoke each ResourceAccessAcceptor to accept the permissions - acceptorTokenAccessMap - .getAccessList() - .forEach(({ actionMap, acceptor }) => { - acceptor.acceptResourceAccess( - this.storageAccessPolicyFactory.createPolicy( - actionMap.getActionToResourcesMap() - ), - ssmEnvironmentEntries - ); - }); - }; -} - -/** - * This factory is really only necessary for allowing us to mock the BucketPolicyArbiter in tests - */ -export class StorageAccessPolicyArbiterFactory { - getInstance = ( - name: string, - accessDefinition: Record, - ssmEnvironmentEntriesGenerator: SsmEnvironmentEntriesGenerator, - getInstanceProps: ConstructFactoryGetInstanceProps, - bucket: IBucket, - bucketPolicyFactory = new StorageAccessPolicyFactory(bucket) - ) => - new StorageAccessPolicyArbiter( - name, - accessDefinition, - ssmEnvironmentEntriesGenerator, - getInstanceProps, - bucket, - bucketPolicyFactory - ); -} diff --git a/packages/backend-storage/src/storage_access_policy_factory.test.ts b/packages/backend-storage/src/storage_access_policy_factory.test.ts index ba9ad292c3..653f3f108b 100644 --- a/packages/backend-storage/src/storage_access_policy_factory.test.ts +++ b/packages/backend-storage/src/storage_access_policy_factory.test.ts @@ -5,7 +5,6 @@ import { StorageAccessPolicyFactory } from './storage_access_policy_factory.js'; import assert from 'node:assert'; import { Template } from 'aws-cdk-lib/assertions'; import { AccountPrincipal, Policy, Role } from 'aws-cdk-lib/aws-iam'; -import { StorageAction, StoragePath } from './types.js'; void describe('StorageAccessPolicyFactory', () => { let bucket: Bucket; @@ -21,7 +20,9 @@ void describe('StorageAccessPolicyFactory', () => { void it('returns policy with read actions', () => { const bucketPolicyFactory = new StorageAccessPolicyFactory(bucket); const policy = bucketPolicyFactory.createPolicy( - new Map([['read', new Set(['/some/prefix/*'])]]) + new Map([ + ['get', { allow: new Set(['/some/prefix/*']), deny: new Set() }], + ]) ); // we have to attach the policy to a role, otherwise CDK erases the policy from the stack @@ -56,7 +57,9 @@ void describe('StorageAccessPolicyFactory', () => { void it('returns policy with write actions', () => { const bucketPolicyFactory = new StorageAccessPolicyFactory(bucket); const policy = bucketPolicyFactory.createPolicy( - new Map([['write', new Set(['/some/prefix/*'])]]) + new Map([ + ['write', { allow: new Set(['/some/prefix/*']), deny: new Set() }], + ]) ); // we have to attach the policy to a role, otherwise CDK erases the policy from the stack @@ -92,7 +95,9 @@ void describe('StorageAccessPolicyFactory', () => { void it('returns policy with delete actions', () => { const bucketPolicyFactory = new StorageAccessPolicyFactory(bucket); const policy = bucketPolicyFactory.createPolicy( - new Map([['delete', new Set(['/some/prefix/*'])]]) + new Map([ + ['delete', { allow: new Set(['/some/prefix/*']), deny: new Set() }], + ]) ); // we have to attach the policy to a role, otherwise CDK erases the policy from the stack @@ -128,7 +133,15 @@ void describe('StorageAccessPolicyFactory', () => { void it('handles multiple prefix paths on same action', () => { const bucketPolicyFactory = new StorageAccessPolicyFactory(bucket); const policy = bucketPolicyFactory.createPolicy( - new Map([['read', new Set(['/some/prefix/*', '/another/path/*'])]]) + new Map([ + [ + 'get', + { + allow: new Set(['/some/prefix/*', '/another/path/*']), + deny: new Set(), + }, + ], + ]) ); // we have to attach the policy to a role, otherwise CDK erases the policy from the stack @@ -177,9 +190,9 @@ void describe('StorageAccessPolicyFactory', () => { void it('handles different actions on different prefixes', () => { const bucketPolicyFactory = new StorageAccessPolicyFactory(bucket); const policy = bucketPolicyFactory.createPolicy( - new Map>([ - ['read', new Set(['/some/prefix/*'])], - ['write', new Set(['/another/path/*'])], + new Map([ + ['get', { allow: new Set(['/some/prefix/*']), deny: new Set() }], + ['write', { allow: new Set(['/another/path/*']), deny: new Set() }], ]) ); @@ -231,9 +244,9 @@ void describe('StorageAccessPolicyFactory', () => { const bucketPolicyFactory = new StorageAccessPolicyFactory(bucket); const policy = bucketPolicyFactory.createPolicy( new Map([ - ['read', new Set(['/some/prefix/*'])], - ['write', new Set(['/some/prefix/*'])], - ['delete', new Set(['/some/prefix/*'])], + ['get', { allow: new Set(['/some/prefix/*']), deny: new Set() }], + ['write', { allow: new Set(['/some/prefix/*']), deny: new Set() }], + ['delete', { allow: new Set(['/some/prefix/*']), deny: new Set() }], ]) ); @@ -294,6 +307,318 @@ void describe('StorageAccessPolicyFactory', () => { }, }); }); + + void it('handles deny on single action', () => { + const bucketPolicyFactory = new StorageAccessPolicyFactory(bucket); + const policy = bucketPolicyFactory.createPolicy( + new Map([ + ['get', { allow: new Set(['/foo/*', '/foo/bar/*']), deny: new Set() }], + [ + 'write', + { allow: new Set(['/foo/*']), deny: new Set(['/foo/bar/*']) }, + ], + ]) + ); + + // we have to attach the policy to a role, otherwise CDK erases the policy from the stack + policy.attachToRole( + new Role(stack, 'testRole', { assumedBy: new AccountPrincipal('1234') }) + ); + + assert.ok(policy instanceof Policy); + + const template = Template.fromStack(Stack.of(bucket)); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 's3:GetObject', + Resource: [ + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testBucketDF4D7D1A', 'Arn'], + }, + '/foo/*', + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testBucketDF4D7D1A', 'Arn'], + }, + '/foo/bar/*', + ], + ], + }, + ], + }, + { + Action: 's3:PutObject', + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testBucketDF4D7D1A', 'Arn'], + }, + '/foo/*', + ], + ], + }, + }, + { + Effect: 'Deny', + Action: 's3:PutObject', + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testBucketDF4D7D1A', 'Arn'], + }, + '/foo/bar/*', + ], + ], + }, + }, + ], + }, + }); + }); + + void it('handles deny on multiple actions for the same path', () => { + const bucketPolicyFactory = new StorageAccessPolicyFactory(bucket); + const policy = bucketPolicyFactory.createPolicy( + new Map([ + [ + 'get', + { + allow: new Set(['/foo/*']), + deny: new Set(['/foo/bar/*']), + }, + ], + [ + 'write', + { allow: new Set(['/foo/*']), deny: new Set(['/foo/bar/*']) }, + ], + ]) + ); + + // we have to attach the policy to a role, otherwise CDK erases the policy from the stack + policy.attachToRole( + new Role(stack, 'testRole', { assumedBy: new AccountPrincipal('1234') }) + ); + + assert.ok(policy instanceof Policy); + + const template = Template.fromStack(Stack.of(bucket)); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 's3:GetObject', + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testBucketDF4D7D1A', 'Arn'], + }, + '/foo/*', + ], + ], + }, + }, + { + Effect: 'Deny', + Action: 's3:GetObject', + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testBucketDF4D7D1A', 'Arn'], + }, + '/foo/bar/*', + ], + ], + }, + }, + { + Action: 's3:PutObject', + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testBucketDF4D7D1A', 'Arn'], + }, + '/foo/*', + ], + ], + }, + }, + { + Effect: 'Deny', + Action: 's3:PutObject', + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testBucketDF4D7D1A', 'Arn'], + }, + '/foo/bar/*', + ], + ], + }, + }, + ], + }, + }); + }); + + void it('handles deny for same action on multiple paths', () => { + const bucketPolicyFactory = new StorageAccessPolicyFactory(bucket); + const policy = bucketPolicyFactory.createPolicy( + new Map([ + [ + 'get', + { + allow: new Set(['/foo/*']), + deny: new Set(['/foo/bar/*', '/other/path/*', '/something/else/*']), + }, + ], + ]) + ); + + // we have to attach the policy to a role, otherwise CDK erases the policy from the stack + policy.attachToRole( + new Role(stack, 'testRole', { assumedBy: new AccountPrincipal('1234') }) + ); + + assert.ok(policy instanceof Policy); + + const template = Template.fromStack(Stack.of(bucket)); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 's3:GetObject', + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testBucketDF4D7D1A', 'Arn'], + }, + '/foo/*', + ], + ], + }, + }, + { + Effect: 'Deny', + Action: 's3:GetObject', + Resource: [ + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testBucketDF4D7D1A', 'Arn'], + }, + '/foo/bar/*', + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testBucketDF4D7D1A', 'Arn'], + }, + '/other/path/*', + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': ['testBucketDF4D7D1A', 'Arn'], + }, + '/something/else/*', + ], + ], + }, + ], + }, + ], + }, + }); + }); + + void it('handles allow and deny on "list" action', () => { + const bucketPolicyFactory = new StorageAccessPolicyFactory(bucket); + const policy = bucketPolicyFactory.createPolicy( + new Map([ + [ + 'list', + { + allow: new Set(['/some/prefix/*']), + deny: new Set(['/some/prefix/subpath/*']), + }, + ], + ]) + ); + + // we have to attach the policy to a role, otherwise CDK erases the policy from the stack + policy.attachToRole( + new Role(stack, 'testRole', { assumedBy: new AccountPrincipal('1234') }) + ); + + assert.ok(policy instanceof Policy); + + const template = Template.fromStack(Stack.of(bucket)); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 's3:ListBucket', + Resource: { + 'Fn::GetAtt': ['testBucketDF4D7D1A', 'Arn'], + }, + Condition: { + StringLike: { + 's3:prefix': ['some/prefix/*', 'some/prefix/'], + }, + }, + }, + { + Action: 's3:ListBucket', + Effect: 'Deny', + Resource: { + 'Fn::GetAtt': ['testBucketDF4D7D1A', 'Arn'], + }, + Condition: { + StringLike: { + 's3:prefix': ['some/prefix/subpath/*', 'some/prefix/subpath/'], + }, + }, + }, + ], + }, + }); + }); }); const createStackAndBucket = (): { stack: Stack; bucket: Bucket } => { diff --git a/packages/backend-storage/src/storage_access_policy_factory.ts b/packages/backend-storage/src/storage_access_policy_factory.ts index 0ee4e799c5..1d6cb43f8e 100644 --- a/packages/backend-storage/src/storage_access_policy_factory.ts +++ b/packages/backend-storage/src/storage_access_policy_factory.ts @@ -1,8 +1,9 @@ import { IBucket } from 'aws-cdk-lib/aws-s3'; -import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { Effect, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Stack } from 'aws-cdk-lib'; import { AmplifyFault } from '@aws-amplify/platform-core'; import { StorageAction, StoragePath } from './types.js'; +import { InternalStorageAction } from './private_types.js'; export type Permission = { actions: StorageAction[]; @@ -29,7 +30,10 @@ export class StorageAccessPolicyFactory { } createPolicy = ( - permissions: Readonly>>> + permissions: Map< + InternalStorageAction, + { allow: Set; deny: Set } + > ) => { if (permissions.size === 0) { throw new AmplifyFault('EmptyPolicyFault', { @@ -39,28 +43,75 @@ export class StorageAccessPolicyFactory { const statements: PolicyStatement[] = []; - permissions.forEach((s3Prefixes, action) => { - statements.push(this.getStatement(s3Prefixes, action)); - }); + permissions.forEach( + ({ allow: allowPrefixes, deny: denyPrefixes }, action) => { + if (allowPrefixes.size > 0) { + statements.push( + this.getStatement(allowPrefixes, action, Effect.ALLOW) + ); + } + if (denyPrefixes.size > 0) { + statements.push(this.getStatement(denyPrefixes, action, Effect.DENY)); + } + } + ); + + if (statements.length === 0) { + // this could happen if the Map contained entries but all of the path sets were empty + throw new AmplifyFault('EmptyPolicyFault', { + message: 'At least one permission must be specified', + }); + } + return new Policy(this.stack, `${this.namePrefix}${this.policyCount++}`, { - statements: statements, + statements, }); }; private getStatement = ( s3Prefixes: Readonly>, - action: StorageAction - ) => - new PolicyStatement({ - actions: actionMap[action], - resources: Array.from(s3Prefixes).map( - (s3Prefix) => `${this.bucket.bucketArn}${s3Prefix}` - ), - }); + action: InternalStorageAction, + effect: Effect + ) => { + switch (action) { + case 'delete': + case 'get': + case 'write': + return new PolicyStatement({ + effect, + actions: actionMap[action], + resources: Array.from(s3Prefixes).map( + (s3Prefix) => `${this.bucket.bucketArn}${s3Prefix}` + ), + }); + case 'list': + return new PolicyStatement({ + effect, + actions: actionMap[action], + resources: [this.bucket.bucketArn], + conditions: { + StringLike: { + 's3:prefix': Array.from(s3Prefixes).flatMap(toConditionPrefix), + }, + }, + }); + } + }; } -const actionMap: Record = { - read: ['s3:GetObject'], +const actionMap: Record = { + get: ['s3:GetObject'], + list: ['s3:ListBucket'], write: ['s3:PutObject'], delete: ['s3:DeleteObject'], }; + +/** + * Converts a prefix like /foo/bar/* into [foo/bar/, foo/bar/*] + * This is necessary to grant the ability to list all objects directly in "foo/bar" and all objects under "foo/bar" + */ +const toConditionPrefix = (prefix: StoragePath) => { + const noLeadingSlash = prefix.slice(1); + const noTrailingWildcard = noLeadingSlash.slice(0, -1); + return [noLeadingSlash, noTrailingWildcard]; +}; diff --git a/packages/backend-storage/src/storage_container_entry_generator.test.ts b/packages/backend-storage/src/storage_container_entry_generator.test.ts index 425ed87136..aa46169f2e 100644 --- a/packages/backend-storage/src/storage_container_entry_generator.test.ts +++ b/packages/backend-storage/src/storage_container_entry_generator.test.ts @@ -12,17 +12,16 @@ import { ConstructFactoryGetInstanceProps, FunctionResources, GenerateContainerEntryProps, - ResourceAccessAcceptorFactory, ResourceProvider, SsmEnvironmentEntriesGenerator, } from '@aws-amplify/plugin-types'; import { App, Stack } from 'aws-cdk-lib'; -import { StorageAccessPolicyArbiterFactory } from './storage_access_policy_arbiter.js'; +import { StorageAccessOrchestratorFactory } from './storage_access_orchestrator.js'; import { AmplifyStorage } from './construct.js'; import { StackMetadataBackendOutputStorageStrategy } from '@aws-amplify/backend-output-storage'; import { Function, InlineCode, Runtime } from 'aws-cdk-lib/aws-lambda'; import { Template } from 'aws-cdk-lib/assertions'; -import { RoleAccessBuilder } from './types.js'; +import { StorageAccessGenerator } from './types.js'; void describe('StorageGenerator', () => { void describe('generateContainerEntry', () => { @@ -62,7 +61,7 @@ void describe('StorageGenerator', () => { const storageGenerator = new StorageContainerEntryGenerator( { name: 'testName' }, getInstanceProps, - new StorageAccessPolicyArbiterFactory() + new StorageAccessOrchestratorFactory() ); const storageInstance = storageGenerator.generateContainerEntry( @@ -72,134 +71,36 @@ void describe('StorageGenerator', () => { assert.ok(storageInstance instanceof AmplifyStorage); }); - void it('throws if access prefixes are invalid', () => { - const storageGenerator = new StorageContainerEntryGenerator( - { name: 'testName', access: () => ({}) }, - getInstanceProps, - new StorageAccessPolicyArbiterFactory(), - undefined, - () => { - throw new Error('test validation error'); - } - ); - - assert.throws( - () => - storageGenerator.generateContainerEntry(generateContainerEntryProps), - { message: 'test validation error' } - ); - }); - - void it('invokes the policy arbiter with correct accessDefinition if access is defined', () => { - const arbitratePoliciesMock = mock.fn(); - const bucketPolicyArbiterFactory = - new StorageAccessPolicyArbiterFactory(); + void it('invokes the policy orchestrator when access rules are defined', () => { + const orchestrateStorageAccessMock = mock.fn(); + const storageAccessOrchestratorFactoryStub = + new StorageAccessOrchestratorFactory(); const getInstanceMock = mock.method( - bucketPolicyArbiterFactory, + storageAccessOrchestratorFactoryStub, 'getInstance', () => ({ - arbitratePolicies: arbitratePoliciesMock, + orchestrateStorageAccess: orchestrateStorageAccessMock, }) ); - const authenticatedAccessAcceptorMock = mock.fn(() => ({ - identifier: 'testAuthenticatedAccessAcceptor', - acceptResourceAccess: mock.fn(), - })); - const guestAccessAcceptorMock = mock.fn(() => ({ - identifier: 'testGuestAccessAcceptor', - acceptResourceAccess: mock.fn(), - })); - const ownerAccessAcceptorMock = mock.fn(() => ({ - identifier: 'testOwnerAccessAcceptor', - acceptResourceAccess: mock.fn(), - })); - const resourceAccessAcceptorMock = mock.fn(() => ({ - identifier: 'testResourceAccessAcceptor', - acceptResourceAccess: mock.fn(), - })); - - const stubRoleAccessBuilder: RoleAccessBuilder = { - authenticated: { - to: (actions) => ({ - getResourceAccessAcceptor: authenticatedAccessAcceptorMock, - actions, - ownerPlaceholderSubstitution: '*', - }), - }, - guest: { - to: (actions) => ({ - getResourceAccessAcceptor: guestAccessAcceptorMock, - actions, - ownerPlaceholderSubstitution: '*', - }), - }, - owner: { - to: (actions) => ({ - getResourceAccessAcceptor: ownerAccessAcceptorMock, - actions, - ownerPlaceholderSubstitution: 'testOwnerSubstitution', - }), - }, - resource: () => ({ - to: (actions) => ({ - getResourceAccessAcceptor: resourceAccessAcceptorMock, - actions, - ownerPlaceholderSubstitution: '*', - }), - }), - }; + const accessRulesCallback: StorageAccessGenerator = () => ({}); const storageGenerator = new StorageContainerEntryGenerator( { name: 'testName', - access: (allow) => ({ - '/test/*': [ - allow.authenticated.to(['read', 'write']), - allow.guest.to(['read']), - allow.owner.to(['read', 'write', 'delete']), - allow - .resource( - {} as unknown as ConstructFactory< - ResourceProvider & ResourceAccessAcceptorFactory - > - ) - .to(['read']), - ], - }), + access: accessRulesCallback, }, getInstanceProps, - bucketPolicyArbiterFactory, - stubRoleAccessBuilder + storageAccessOrchestratorFactoryStub ); storageGenerator.generateContainerEntry(generateContainerEntryProps); - assert.equal(arbitratePoliciesMock.mock.callCount(), 1); - assert.deepStrictEqual(getInstanceMock.mock.calls[0].arguments[1], { - '/test/*': [ - { - getResourceAccessAcceptor: authenticatedAccessAcceptorMock, - actions: ['read', 'write'], - ownerPlaceholderSubstitution: '*', - }, - { - getResourceAccessAcceptor: guestAccessAcceptorMock, - actions: ['read'], - ownerPlaceholderSubstitution: '*', - }, - { - getResourceAccessAcceptor: ownerAccessAcceptorMock, - actions: ['read', 'write', 'delete'], - ownerPlaceholderSubstitution: 'testOwnerSubstitution', - }, - { - getResourceAccessAcceptor: resourceAccessAcceptorMock, - actions: ['read'], - ownerPlaceholderSubstitution: '*', - }, - ], - }); + assert.equal(orchestrateStorageAccessMock.mock.callCount(), 1); + assert.equal( + getInstanceMock.mock.calls[0].arguments[0], + accessRulesCallback + ); }); void it('configures S3 triggers if defined', () => { @@ -227,7 +128,7 @@ void describe('StorageGenerator', () => { }, }, getInstanceProps, - new StorageAccessPolicyArbiterFactory() + new StorageAccessOrchestratorFactory() ); storageGenerator.generateContainerEntry(generateContainerEntryProps); diff --git a/packages/backend-storage/src/storage_container_entry_generator.ts b/packages/backend-storage/src/storage_container_entry_generator.ts index aca57ee052..66e1b3520d 100644 --- a/packages/backend-storage/src/storage_container_entry_generator.ts +++ b/packages/backend-storage/src/storage_container_entry_generator.ts @@ -4,11 +4,10 @@ import { GenerateContainerEntryProps, } from '@aws-amplify/plugin-types'; import { AmplifyStorage, AmplifyStorageTriggerEvent } from './construct.js'; -import { StorageAccessPolicyArbiterFactory } from './storage_access_policy_arbiter.js'; -import { AmplifyStorageFactoryProps, RoleAccessBuilder } from './types.js'; -import { roleAccessBuilder as _roleAccessBuilder } from './access_builder.js'; +import { StorageAccessOrchestratorFactory } from './storage_access_orchestrator.js'; +import { AmplifyStorageFactoryProps } from './types.js'; import { EventType } from 'aws-cdk-lib/aws-s3'; -import { validateStorageAccessPaths as _validateStorageAccessPaths } from './validate_storage_access_paths.js'; +import { StorageAccessPolicyFactory } from './storage_access_policy_factory.js'; /** * Generates a single instance of storage resources @@ -24,9 +23,7 @@ export class StorageContainerEntryGenerator constructor( private readonly props: AmplifyStorageFactoryProps, private readonly getInstanceProps: ConstructFactoryGetInstanceProps, - private readonly bucketPolicyArbiterFactory: StorageAccessPolicyArbiterFactory = new StorageAccessPolicyArbiterFactory(), - private readonly roleAccessBuilder: RoleAccessBuilder = _roleAccessBuilder, - private readonly validateStorageAccessPaths = _validateStorageAccessPaths + private readonly storageAccessOrchestratorFactory: StorageAccessOrchestratorFactory = new StorageAccessOrchestratorFactory() ) {} generateContainerEntry = ({ @@ -60,24 +57,24 @@ export class StorageContainerEntryGenerator return amplifyStorage; } - // props.access is the access callback defined by the customer - // here we inject the roleAccessBuilder into the callback and run it - // this produces the access definition that will be used to create the storage policies - const accessDefinition = this.props.access(this.roleAccessBuilder); + // generate the ssm environment context necessary to access the s3 bucket (in this case, just the bucket name) + const ssmEnvironmentEntries = + ssmEnvironmentEntriesGenerator.generateSsmEnvironmentEntries({ + [`${this.props.name}_BUCKET_NAME`]: + amplifyStorage.resources.bucket.bucketName, + }); - this.validateStorageAccessPaths(Object.keys(accessDefinition)); + // we pass the access definition along with other dependencies to the storageAccessOrchestrator + const storageAccessOrchestrator = + this.storageAccessOrchestratorFactory.getInstance( + this.props.access, + this.getInstanceProps, + ssmEnvironmentEntries, + new StorageAccessPolicyFactory(amplifyStorage.resources.bucket) + ); - // we pass the access definition along with other dependencies to the bucketPolicyArbiter - const bucketPolicyArbiter = this.bucketPolicyArbiterFactory.getInstance( - this.props.name, - accessDefinition, - ssmEnvironmentEntriesGenerator, - this.getInstanceProps, - amplifyStorage.resources.bucket - ); - - // the arbiter generates policies according to the accessDefinition and attaches the policies to appropriate roles - bucketPolicyArbiter.arbitratePolicies(); + // the orchestrator generates policies according to the accessDefinition and attaches the policies to appropriate roles + storageAccessOrchestrator.orchestrateStorageAccess(); return amplifyStorage; }; diff --git a/packages/backend-storage/src/types.ts b/packages/backend-storage/src/types.ts index a157c0d32c..c7a5f8fc1b 100644 --- a/packages/backend-storage/src/types.ts +++ b/packages/backend-storage/src/types.ts @@ -17,29 +17,41 @@ export type AmplifyStorageFactoryProps = Omit< * Access control is under active development and is subject to change without notice. * Use at your own risk and do not use in production */ - access?: AccessGenerator; + access?: StorageAccessGenerator; }; +/** + * Types of entity IDs that can be substituted in access policies + * + * 'identity' corresponds to the Cognito Identity Pool IdentityID + * + * Currently this is the only supported entity type. + */ +export type EntityId = 'identity'; + /** * !EXPERIMENTAL! * * Resource access patterns are under active development and are subject to breaking changes. * Do not use in production. */ -export type RoleAccessBuilder = { - authenticated: StorageAccessBuilder; - guest: StorageAccessBuilder; - owner: StorageAccessBuilder; +export type StorageAccessBuilder = { + authenticated: StorageActionBuilder; + guest: StorageActionBuilder; + group: (groupName: string) => StorageActionBuilder; + entity: (entityId: EntityId) => StorageActionBuilder; resource: ( other: ConstructFactory - ) => StorageAccessBuilder; + ) => StorageActionBuilder; }; -export type StorageAccessBuilder = { +export type StorageActionBuilder = { to: (actions: StorageAction[]) => StorageAccessDefinition; }; -export type AccessGenerator = (allow: RoleAccessBuilder) => StorageAccessRecord; +export type StorageAccessGenerator = ( + allow: StorageAccessBuilder +) => StorageAccessRecord; export type StorageAccessRecord = Record< StoragePath, @@ -57,12 +69,25 @@ export type StorageAccessDefinition = { /** * The value that will be substituted into the resource string in place of the {owner} token */ - ownerPlaceholderSubstitution: string; + idSubstitution: string; }; -export type StorageAction = 'read' | 'write' | 'delete'; +/** + * Actions that can be granted to specific paths within the storage resource. + * + * 'read' grants both 'get' and 'list' actions. + * + * 'get' grants the ability to fetch objects matching the path prefix. + * + * 'list' grants the ability to list object names matching the path prefix. It does NOT grant the ability to get the content of those objects. + * + * 'write' grants the ability to upload objects with a certain prefix. Note that this allows both creating new objects and updating existing ones. + * + * 'delete' grant the ability to delete objects with a certain prefix. + */ +export type StorageAction = 'read' | 'get' | 'list' | 'write' | 'delete'; /** - * Storage access keys must start with / and end with /* + * Storage access paths must start with / and end with /* */ export type StoragePath = `/${string}/*`; diff --git a/packages/backend-storage/src/validate_storage_access_paths.test.ts b/packages/backend-storage/src/validate_storage_access_paths.test.ts index 03cd661b55..85a1773852 100644 --- a/packages/backend-storage/src/validate_storage_access_paths.test.ts +++ b/packages/backend-storage/src/validate_storage_access_paths.test.ts @@ -1,7 +1,7 @@ import { describe, it } from 'node:test'; import { validateStorageAccessPaths } from './validate_storage_access_paths.js'; import assert from 'node:assert'; -import { ownerPathPartToken } from './constants.js'; +import { entityIdPathToken } from './constants.js'; void describe('validateStorageAccessPaths', () => { void it('is a noop on valid paths', () => { @@ -10,8 +10,8 @@ void describe('validateStorageAccessPaths', () => { '/foo/bar/*', '/foo/baz/*', '/other/*', - '/something/{owner}/*', - '/another/{owner}/*', + '/something/{entity_id}/*', + '/another/{entity_id}/*', ]); // completing successfully indicates success }); @@ -30,6 +30,12 @@ void describe('validateStorageAccessPaths', () => { }); }); + void it('throws on path that has "//" in it', () => { + assert.throws(() => validateStorageAccessPaths(['/foo//bar/*']), { + message: 'Path cannot contain "//". Found [/foo//bar/*].', + }); + }); + void it('throws on path that has wildcards in the middle', () => { assert.throws(() => validateStorageAccessPaths(['/foo/*/bar/*']), { message: `Wildcards are only allowed as the final part of a path. Found [/foo/*/bar/*].`, @@ -49,36 +55,45 @@ void describe('validateStorageAccessPaths', () => { void it('throws on path that has multiple owner tokens', () => { assert.throws( - () => validateStorageAccessPaths(['/foo/{owner}/{owner}/*']), + () => validateStorageAccessPaths(['/foo/{entity_id}/{entity_id}/*']), { - message: `The ${ownerPathPartToken} token can only appear once in a path. Found [/foo/{owner}/{owner}/*]`, + message: `The ${entityIdPathToken} token can only appear once in a path. Found [/foo/{entity_id}/{entity_id}/*]`, } ); }); void it('throws on path where owner token is not at the end', () => { - assert.throws(() => validateStorageAccessPaths(['/foo/{owner}/bar/*']), { - message: `The ${ownerPathPartToken} token must be the path part right before the ending wildcard. Found [/foo/{owner}/bar/*].`, - }); + assert.throws( + () => validateStorageAccessPaths(['/foo/{entity_id}/bar/*']), + { + message: `The ${entityIdPathToken} token must be the path part right before the ending wildcard. Found [/foo/{entity_id}/bar/*].`, + } + ); }); void it('throws on path that starts with owner token', () => { - assert.throws(() => validateStorageAccessPaths(['/{owner}/*']), { - message: `The ${ownerPathPartToken} token must not be the first path part. Found [/{owner}/*].`, + assert.throws(() => validateStorageAccessPaths(['/{entity_id}/*']), { + message: `The ${entityIdPathToken} token must not be the first path part. Found [/{entity_id}/*].`, }); }); void it('throws on path that has owner token and other characters in single path part', () => { - assert.throws(() => validateStorageAccessPaths(['/abc{owner}/*']), { - message: `A path part that includes the ${ownerPathPartToken} token cannot include any other characters. Found [/abc{owner}/*].`, + assert.throws(() => validateStorageAccessPaths(['/abc{entity_id}/*']), { + message: `A path part that includes the ${entityIdPathToken} token cannot include any other characters. Found [/abc{entity_id}/*].`, }); }); - void it('throws on path where owner token conflicts with wildcard in another path', () => { + void it('throws on path that is a prefix of a path with an owner token', () => { + assert.throws( + () => validateStorageAccessPaths(['/foo/{entity_id}/*', '/foo/*']), + { + message: `A path cannot be a prefix of another path that contains the ${entityIdPathToken} token.`, + } + ); assert.throws( - () => validateStorageAccessPaths(['/foo/{owner}/*', '/foo/*']), + () => validateStorageAccessPaths(['/foo/bar/{entity_id}/*', '/foo/*']), { - message: `Wildcard conflict detected with an ${ownerPathPartToken} token.`, + message: `A path cannot be a prefix of another path that contains the ${entityIdPathToken} token.`, } ); }); diff --git a/packages/backend-storage/src/validate_storage_access_paths.ts b/packages/backend-storage/src/validate_storage_access_paths.ts index 4ee221fa35..8bb5689eba 100644 --- a/packages/backend-storage/src/validate_storage_access_paths.ts +++ b/packages/backend-storage/src/validate_storage_access_paths.ts @@ -1,5 +1,5 @@ import { AmplifyUserError } from '@aws-amplify/platform-core'; -import { ownerPathPartToken } from './constants.js'; +import { entityIdPathToken } from './constants.js'; import { StorageError } from './private_types.js'; /** @@ -23,6 +23,13 @@ const validateStoragePath = ( }); } + if (path.includes('//')) { + throw new AmplifyUserError('InvalidStorageAccessPathError', { + message: `Path cannot contain "//". Found [${path}].`, + resolution: 'Update all paths to match the format requirements.', + }); + } + if (path.indexOf('*') < path.length - 1) { throw new AmplifyUserError('InvalidStorageAccessPathError', { message: `Wildcards are only allowed as the final part of a path. Found [${path}].`, @@ -50,21 +57,34 @@ const validateStoragePath = ( }); } - if (path.includes(ownerPathPartToken)) { - validatePathWithOwnerToken(path, allPaths); - } + validateOwnerTokenRules(path, otherPrefixes); }; /** * Extra validations that are only necessary if the path includes an owner token */ -const validatePathWithOwnerToken = (path: string, allPaths: string[]) => { - const ownerSplit = path.split(ownerPathPartToken); +const validateOwnerTokenRules = (path: string, otherPrefixes: string[]) => { + // if there's no owner token in the path, this validation is a noop + if (!path.includes(entityIdPathToken)) { + return; + } + + if (otherPrefixes.length > 0) { + throw new AmplifyUserError('InvalidStorageAccessPathError', { + message: `A path cannot be a prefix of another path that contains the ${entityIdPathToken} token.`, + details: `Found [${path}] which has prefixes [${otherPrefixes.join( + ', ' + )}].`, + resolution: `Update the storage access paths such that any given path has at most one other path that is a prefix.`, + }); + } + + const ownerSplit = path.split(entityIdPathToken); if (ownerSplit.length > 2) { throw new AmplifyUserError('InvalidStorageAccessPathError', { - message: `The ${ownerPathPartToken} token can only appear once in a path. Found [${path}]`, - resolution: `Remove all but one occurrence of the ${ownerPathPartToken} token`, + message: `The ${entityIdPathToken} token can only appear once in a path. Found [${path}]`, + resolution: `Remove all but one occurrence of the ${entityIdPathToken} token`, }); } @@ -72,41 +92,22 @@ const validatePathWithOwnerToken = (path: string, allPaths: string[]) => { if (substringAfterOwnerToken !== '/*') { throw new AmplifyUserError('InvalidStorageAccessPathError', { - message: `The ${ownerPathPartToken} token must be the path part right before the ending wildcard. Found [${path}].`, - resolution: `Update the path such that the owner token is the last path part before the ending wildcard. For example: "/foo/bar/${ownerPathPartToken}/*.`, + message: `The ${entityIdPathToken} token must be the path part right before the ending wildcard. Found [${path}].`, + resolution: `Update the path such that the owner token is the last path part before the ending wildcard. For example: "/foo/bar/${entityIdPathToken}/*.`, }); } if (substringBeforeOwnerToken === '/') { throw new AmplifyUserError('InvalidStorageAccessPathError', { - message: `The ${ownerPathPartToken} token must not be the first path part. Found [${path}].`, - resolution: `Add an additional prefix to the path. For example: "/foo/${ownerPathPartToken}/*.`, + message: `The ${entityIdPathToken} token must not be the first path part. Found [${path}].`, + resolution: `Add an additional prefix to the path. For example: "/foo/${entityIdPathToken}/*.`, }); } if (!substringBeforeOwnerToken.endsWith('/')) { throw new AmplifyUserError('InvalidStorageAccessPathError', { - message: `A path part that includes the ${ownerPathPartToken} token cannot include any other characters. Found [${path}].`, - resolution: `Remove all other characters from the path part with the ${ownerPathPartToken} token. For example: "/foo/${ownerPathPartToken}/*"`, - }); - } - - /** - * If the path includes the owner token, we need to do one more pass through the prefixes where we substitute the owner toke with a * and check for prefixes again - * This is because the owner token becomes a * for all access except owner rules so we need to make sure there are no other prefix conflicts - */ - - const substitutionPrefixes = getPrefixes( - path.replace(ownerPathPartToken, '*'), - allPaths - ); - if (substitutionPrefixes.length > 0) { - throw new AmplifyUserError('InvalidStorageAccessPathError', { - message: `Wildcard conflict detected with an ${ownerPathPartToken} token.`, - details: `Paths [${substitutionPrefixes.join( - ', ' - )}] conflicts with ${ownerPathPartToken} token in path [${path}].`, - resolution: `Update the storage access paths such that no path has a wildcard that conflicts with an ${ownerPathPartToken} token.`, + message: `A path part that includes the ${entityIdPathToken} token cannot include any other characters. Found [${path}].`, + resolution: `Remove all other characters from the path part with the ${entityIdPathToken} token. For example: "/foo/${entityIdPathToken}/*"`, }); } }; @@ -115,5 +116,13 @@ const validatePathWithOwnerToken = (path: string, allPaths: string[]) => { * Returns a subset of paths where each element is a prefix of path * Equivalent paths are NOT considered prefixes of each other (mainly just for simplicity of the calling logic) */ -const getPrefixes = (path: string, paths: string[]): string[] => - paths.filter((p) => path !== p && path.startsWith(p.replaceAll('*', ''))); +const getPrefixes = ( + path: string, + paths: string[], + treatWildcardAsLiteral = false +): string[] => + paths.filter( + (p) => + path !== p && + path.startsWith(treatWildcardAsLiteral ? p : p.replaceAll('*', '')) + ); diff --git a/packages/backend/API.md b/packages/backend/API.md index b59454e627..f960ac0468 100644 --- a/packages/backend/API.md +++ b/packages/backend/API.md @@ -5,19 +5,39 @@ ```ts import { a } from '@aws-amplify/data-schema'; +import { AuthCfnResources } from '@aws-amplify/plugin-types'; +import { AuthResources } from '@aws-amplify/plugin-types'; +import { AuthRoleName } from '@aws-amplify/plugin-types'; +import { BackendOutputEntry } from '@aws-amplify/plugin-types'; +import { BackendOutputStorageStrategy } from '@aws-amplify/plugin-types'; import { BackendSecret } from '@aws-amplify/plugin-types'; +import { BackendSecretResolver } from '@aws-amplify/plugin-types'; import { ClientConfig } from '@aws-amplify/client-config'; import { ClientSchema } from '@aws-amplify/data-schema'; +import { ConstructContainer } from '@aws-amplify/plugin-types'; +import { ConstructContainerEntryGenerator } from '@aws-amplify/plugin-types'; import { ConstructFactory } from '@aws-amplify/plugin-types'; +import { ConstructFactoryGetInstanceProps } from '@aws-amplify/plugin-types'; import { defineAuth } from '@aws-amplify/backend-auth'; import { defineData } from '@aws-amplify/backend-data'; import { defineFunction } from '@aws-amplify/backend-function'; import { defineStorage } from '@aws-amplify/backend-storage'; +import { FunctionResources } from '@aws-amplify/plugin-types'; +import { GenerateContainerEntryProps } from '@aws-amplify/plugin-types'; +import { ImportPathVerifier } from '@aws-amplify/plugin-types'; import { ResourceProvider } from '@aws-amplify/plugin-types'; +import { SsmEnvironmentEntriesGenerator } from '@aws-amplify/plugin-types'; +import { SsmEnvironmentEntry } from '@aws-amplify/plugin-types'; import { Stack } from 'aws-cdk-lib'; export { a } +export { AuthCfnResources } + +export { AuthResources } + +export { AuthRoleName } + // @public export type Backend = BackendBase & { [K in keyof T]: ReturnType; @@ -29,8 +49,22 @@ export type BackendBase = { addOutput: (clientConfigPart: Partial) => void; }; +export { BackendOutputEntry } + +export { BackendOutputStorageStrategy } + +export { BackendSecretResolver } + export { ClientSchema } +export { ConstructContainer } + +export { ConstructContainerEntryGenerator } + +export { ConstructFactory } + +export { ConstructFactoryGetInstanceProps } + export { defineAuth } // @public @@ -47,9 +81,21 @@ export { defineFunction } export { defineStorage } +export { FunctionResources } + +export { GenerateContainerEntryProps } + +export { ImportPathVerifier } + +export { ResourceProvider } + // @public export const secret: (name: string) => BackendSecret; +export { SsmEnvironmentEntriesGenerator } + +export { SsmEnvironmentEntry } + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 4814f0392b..0de0e4db15 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,67 @@ # @aws-amplify/backend +## 0.13.0-beta.6 + +### Minor Changes + +- c9f03ee: Re-export some plugin-types from submodule export @aws-amplify/backend/types/platform + +### Patch Changes + +- Updated dependencies [91dae55] +- Updated dependencies [26cdffd] +- Updated dependencies [75f69ea] +- Updated dependencies [937086b] + - @aws-amplify/backend-data@0.10.0-beta.5 + - @aws-amplify/backend-function@0.8.0-beta.4 + - @aws-amplify/platform-core@0.5.0-beta.2 + - @aws-amplify/backend-auth@0.5.0-beta.5 + - @aws-amplify/client-config@0.9.0-beta.4 + - @aws-amplify/backend-output-storage@0.4.0-beta.2 + - @aws-amplify/backend-secret@0.4.5-beta.2 + - @aws-amplify/backend-storage@0.6.0-beta.4 + +## 0.13.0-beta.5 + +### Patch Changes + +- Updated dependencies [bdbf6e8] +- Updated dependencies [a777488] +- Updated dependencies [ab05ae0] +- Updated dependencies [268acd8] +- Updated dependencies [7f5edee] +- Updated dependencies [f999897] + - @aws-amplify/backend-function@0.8.0-beta.3 + - @aws-amplify/backend-data@0.10.0-beta.4 + - @aws-amplify/backend-auth@0.5.0-beta.4 + - @aws-amplify/backend-storage@0.6.0-beta.3 + +## 0.13.0-beta.4 + +### Patch Changes + +- 7857f0a: backend-data: add js resolver support +- 7dc3132: add aspect on root stack to valid role trust policies +- Updated dependencies [64e425c] +- Updated dependencies [912034e] +- Updated dependencies [7857f0a] +- Updated dependencies [5969a32] +- Updated dependencies [c760df4] +- Updated dependencies [916d3f0] +- Updated dependencies [79cff6d] +- Updated dependencies [318335d] +- Updated dependencies [215d65d] +- Updated dependencies [cec91d5] +- Updated dependencies [b0ba24d] +- Updated dependencies [3adf7df] +- Updated dependencies [aee7501] +- Updated dependencies [82006e5] + - @aws-amplify/backend-storage@0.6.0-beta.2 + - @aws-amplify/backend-data@0.10.0-beta.3 + - @aws-amplify/client-config@0.9.0-beta.3 + - @aws-amplify/backend-function@0.8.0-beta.2 + - @aws-amplify/backend-auth@0.5.0-beta.3 + ## 0.13.0-beta.3 ### Patch Changes diff --git a/packages/backend/api-extractor.json b/packages/backend/api-extractor.json index 0f56de03f6..cc2ebea8cf 100644 --- a/packages/backend/api-extractor.json +++ b/packages/backend/api-extractor.json @@ -1,3 +1,4 @@ { - "extends": "../../api-extractor.base.json" + "extends": "../../api-extractor.base.json", + "mainEntryPointFilePath": "/lib/index.internal.d.ts" } diff --git a/packages/backend/package.json b/packages/backend/package.json index b3088fd0a0..4ed15813eb 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend", - "version": "0.13.0-beta.3", + "version": "0.13.0-beta.6", "type": "module", "publishConfig": { "access": "public" @@ -10,6 +10,9 @@ "types": "./lib/index.d.ts", "import": "./lib/index.js", "require": "./lib/index.js" + }, + "./types/platform": { + "types": "./lib/types/platform.d.ts" } }, "imports": { @@ -21,16 +24,16 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/data-schema": "^0.13.2", - "@aws-amplify/backend-auth": "^0.5.0-beta.2", - "@aws-amplify/backend-function": "^0.8.0-beta.1", - "@aws-amplify/backend-data": "^0.10.0-beta.2", + "@aws-amplify/data-schema": "^0.13.15", + "@aws-amplify/backend-auth": "^0.5.0-beta.5", + "@aws-amplify/backend-function": "^0.8.0-beta.4", + "@aws-amplify/backend-data": "^0.10.0-beta.5", "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", - "@aws-amplify/backend-output-storage": "^0.4.0-beta.1", - "@aws-amplify/backend-secret": "^0.4.5-beta.1", - "@aws-amplify/backend-storage": "^0.6.0-beta.1", - "@aws-amplify/client-config": "^0.8.1-beta.2", - "@aws-amplify/platform-core": "^0.5.0-beta.1", + "@aws-amplify/backend-output-storage": "^0.4.0-beta.2", + "@aws-amplify/backend-secret": "^0.4.5-beta.2", + "@aws-amplify/backend-storage": "^0.6.0-beta.4", + "@aws-amplify/client-config": "^0.9.0-beta.4", + "@aws-amplify/platform-core": "^0.5.0-beta.2", "@aws-amplify/plugin-types": "^0.9.0-beta.0", "@aws-sdk/client-amplify": "^3.465.0" }, diff --git a/packages/backend/src/engine/amplify_stack.test.ts b/packages/backend/src/engine/amplify_stack.test.ts index a568c0264d..707cb7f537 100644 --- a/packages/backend/src/engine/amplify_stack.test.ts +++ b/packages/backend/src/engine/amplify_stack.test.ts @@ -3,6 +3,7 @@ import { App, NestedStack } from 'aws-cdk-lib'; import { AmplifyStack } from './amplify_stack.js'; import { Template } from 'aws-cdk-lib/assertions'; import assert from 'node:assert'; +import { FederatedPrincipal, Role } from 'aws-cdk-lib/aws-iam'; void describe('AmplifyStack', () => { void it('renames nested stack logical IDs to non-redundant value', () => { @@ -19,4 +20,66 @@ void describe('AmplifyStack', () => { assert.ok(actualStackLogicalId.startsWith('testName')); assert.ok(!actualStackLogicalId.includes('NestedStack')); }); + + void it('allows roles with properly configured cognito trust policies', () => { + const app = new App(); + const rootStack = new AmplifyStack(app, 'test-id'); + new Role(rootStack, 'correctRole', { + assumedBy: new FederatedPrincipal( + 'cognito-identity.amazonaws.com', + { + StringEquals: { + 'cognito-identity.amazonaws.com:aud': 'testIdpId', + }, + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': 'authenticated', + }, + }, + 'sts:AssumeRoleWithWebIdentity' + ), + }); + assert.doesNotThrow(() => Template.fromStack(rootStack)); + }); + + void it('throws on roles with cognito trust policy missing amr condition', () => { + const app = new App(); + const rootStack = new AmplifyStack(app, 'test-id'); + new Role(rootStack, 'missingAmrCondition', { + assumedBy: new FederatedPrincipal( + 'cognito-identity.amazonaws.com', + { + StringEquals: { + 'cognito-identity.amazonaws.com:aud': 'testIdpId', + }, + }, + 'sts:AssumeRoleWithWebIdentity' + ), + }); + + assert.throws(() => Template.fromStack(rootStack), { + message: + 'Cannot create a Role trust policy with Cognito that does not have a StringLike condition for cognito-identity.amazonaws.com:amr', + }); + }); + + void it('throws on roles with cognito trust policy missing aud condition', () => { + const app = new App(); + const rootStack = new AmplifyStack(app, 'test-id'); + new Role(rootStack, 'missingAudCondition', { + assumedBy: new FederatedPrincipal( + 'cognito-identity.amazonaws.com', + { + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': 'authenticated', + }, + }, + 'sts:AssumeRoleWithWebIdentity' + ), + }); + + assert.throws(() => Template.fromStack(rootStack), { + message: + 'Cannot create a Role trust policy with Cognito that does not have a StringEquals condition for cognito-identity.amazonaws.com:aud', + }); + }); }); diff --git a/packages/backend/src/engine/amplify_stack.ts b/packages/backend/src/engine/amplify_stack.ts index d54aa35573..4c7475658b 100644 --- a/packages/backend/src/engine/amplify_stack.ts +++ b/packages/backend/src/engine/amplify_stack.ts @@ -1,9 +1,19 @@ -import { CfnElement, Stack } from 'aws-cdk-lib'; +import { AmplifyFault } from '@aws-amplify/platform-core'; +import { Aspects, CfnElement, IAspect, Stack } from 'aws-cdk-lib'; +import { Role } from 'aws-cdk-lib/aws-iam'; +import { Construct, IConstruct } from 'constructs'; /** * Amplify-specific Stack implementation to handle cross-cutting concerns for all Amplify stacks */ export class AmplifyStack extends Stack { + /** + * Default constructor + */ + constructor(scope: Construct, id: string) { + super(scope, id); + Aspects.of(this).add(new CognitoRoleTrustPolicyValidator()); + } /** * Overrides Stack.allocateLogicalId to prevent redundant nested stack logical IDs */ @@ -20,3 +30,66 @@ export class AmplifyStack extends Stack { return defaultId; }; } + +class CognitoRoleTrustPolicyValidator implements IAspect { + visit = (node: IConstruct) => { + if (!(node instanceof Role)) { + return; + } + const assumeRolePolicyDocument = node.assumeRolePolicy?.toJSON(); + if (!assumeRolePolicyDocument) { + return; + } + + assumeRolePolicyDocument.Statement.forEach( + this.cognitoTrustPolicyStatementValidator + ); + }; + + private cognitoTrustPolicyStatementValidator = ({ + Action: action, + Condition: condition, + Effect: effect, + Principal: principal, + }: { + // These property names come from the IAM policy document which we do not control + /* eslint-disable @typescript-eslint/naming-convention */ + Action: string; + Condition?: Record>; + Effect: 'Allow' | 'Deny'; + Principal?: { Federated?: string }; + /* eslint-enable @typescript-eslint/naming-convention */ + }) => { + if (action !== 'sts:AssumeRoleWithWebIdentity') { + return; + } + if (principal?.Federated !== 'cognito-identity.amazonaws.com') { + return; + } + if (effect === 'Deny') { + return; + } + // if we got here, we have a policy that allows AssumeRoleWithWebIdentity with Cognito + // need to validate that the policy has an appropriate condition + + const audCondition = + condition?.StringEquals?.['cognito-identity.amazonaws.com:aud']; + if (typeof audCondition !== 'string' || audCondition.length === 0) { + throw new AmplifyFault('InvalidTrustPolicyFault', { + message: + 'Cannot create a Role trust policy with Cognito that does not have a StringEquals condition for cognito-identity.amazonaws.com:aud', + }); + } + + const amrCondition = + condition?.['ForAnyValue:StringLike']?.[ + 'cognito-identity.amazonaws.com:amr' + ]; + if (typeof amrCondition !== 'string' || amrCondition.length === 0) { + throw new AmplifyFault('InvalidTrustPolicyFault', { + message: + 'Cannot create a Role trust policy with Cognito that does not have a StringLike condition for cognito-identity.amazonaws.com:amr', + }); + } + }; +} diff --git a/packages/backend/src/index.internal.ts b/packages/backend/src/index.internal.ts new file mode 100644 index 0000000000..edcc5711d6 --- /dev/null +++ b/packages/backend/src/index.internal.ts @@ -0,0 +1,9 @@ +export * from './index.js'; + +/* + Api-extractor does not ([yet](https://github.com/microsoft/rushstack/issues/1596)) support multiple package entry points + Because this package has a submodule export, we are working around this issue by including that export here and directing api-extract to this entry point instead + This allows api-extractor to pick up the submodule exports in its analysis + */ + +export * from './types/platform.js'; diff --git a/packages/backend/src/types/platform.ts b/packages/backend/src/types/platform.ts new file mode 100644 index 0000000000..fc0acfe646 --- /dev/null +++ b/packages/backend/src/types/platform.ts @@ -0,0 +1,21 @@ +/** + * Subset of types exported from @aws-amplify/plugin-types that are useful when building additional abstractions around the `define*` functions. + */ +export { + AuthCfnResources, + AuthResources, + FunctionResources, + AuthRoleName, + ConstructFactory, + ResourceProvider, + ConstructFactoryGetInstanceProps, + ConstructContainer, + ConstructContainerEntryGenerator, + GenerateContainerEntryProps, + BackendSecretResolver, + SsmEnvironmentEntriesGenerator, + BackendOutputStorageStrategy, + BackendOutputEntry, + ImportPathVerifier, + SsmEnvironmentEntry, +} from '@aws-amplify/plugin-types'; diff --git a/packages/cli-core/API.md b/packages/cli-core/API.md index 30168c0331..7f77b222e2 100644 --- a/packages/cli-core/API.md +++ b/packages/cli-core/API.md @@ -21,17 +21,12 @@ export class AmplifyPrompter { }) => Promise; } -// @public -export enum COLOR { - // (undocumented) - RED = "31m" -} - // @public export const format: { runner: (binaryRunner: string) => { amplifyCommand: (command: string) => string; }; + error: (message: string) => string; note: (message: string) => string; command: (command: string) => string; success: (message: string) => string; @@ -53,7 +48,7 @@ export enum LogLevel { // @public export class PackageManagerControllerFactory { - constructor(cwd: string, printer: Printer); + constructor(cwd: string, printer: Printer, platform?: NodeJS.Platform); getPackageManagerController(): PackageManagerController; } @@ -62,11 +57,14 @@ export class Printer { constructor(minimumLogLevel: LogLevel, stdout?: NodeJS.WriteStream, stderr?: NodeJS.WriteStream, refreshRate?: number); indicateProgress(message: string, callback: () => Promise): Promise; log(message: string, level?: LogLevel, eol?: boolean): void; - print: (message: string, colorName?: COLOR) => void; + print: (message: string) => void; printNewLine: () => void; printRecords: >(...objects: T[]) => void; } +// @public (undocumented) +export const printer: Printer; + // @public (undocumented) export type RecordValue = string | number | string[] | Date; diff --git a/packages/cli-core/CHANGELOG.md b/packages/cli-core/CHANGELOG.md index 061c3742c5..19fc5c1dd6 100644 --- a/packages/cli-core/CHANGELOG.md +++ b/packages/cli-core/CHANGELOG.md @@ -1,5 +1,30 @@ # @aws-amplify/cli-core +## 0.5.0-beta.2 + +### Minor Changes + +- 3e34244: use `format` to replace `color` and remove `color`. + +### Patch Changes + +- ee247fd: use printer from cli-core +- 937086b: require "resolution" in AmplifyUserError options +- Updated dependencies [937086b] + - @aws-amplify/platform-core@0.5.0-beta.2 + +## 0.5.0-beta.1 + +### Minor Changes + +- b0ba24d: Generate type definition file for static environment variables for functions + +### Patch Changes + +- 3998cd3: Fix how paths is added to tsconfig +- 8d9a7a4: add error message for PNPM on windows +- 8d9a7a4: update PackageManagerControllerFactory to take Operation System platform information optionally + ## 0.4.1-beta.0 ### Patch Changes diff --git a/packages/cli-core/package.json b/packages/cli-core/package.json index 21ccd51d04..85debb5875 100644 --- a/packages/cli-core/package.json +++ b/packages/cli-core/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/cli-core", - "version": "0.4.1-beta.0", + "version": "0.5.0-beta.2", "type": "module", "publishConfig": { "access": "public" @@ -18,6 +18,7 @@ }, "license": "Apache-2.0", "dependencies": { + "@aws-amplify/platform-core": "^0.5.0-beta.2", "@inquirer/prompts": "^3.0.0", "execa": "^8.0.1", "kleur": "^4.1.5" diff --git a/packages/cli-core/src/colors.ts b/packages/cli-core/src/colors.ts deleted file mode 100644 index a5754a7452..0000000000 --- a/packages/cli-core/src/colors.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Simple utility to "color" the console output. -// Keeping it simple and avoiding using a 3p dep until needed - -/** - * Enum for colors that clients can use - * Use standard ANSI escape codes https://en.wikipedia.org/wiki/ANSI_escape_code#Colors - */ -export enum COLOR { - RED = '31m', -} - -/** - * Wraps a given string with a given color. - * @param colorName - from the enum COLOR - * @param message - string to be wrapped in the given color - * @returns colored string - */ -export const color = (colorName: COLOR, message: string) => - `\x1b[${colorName}${message}\x1b[0m`; diff --git a/packages/cli-core/src/format/format.ts b/packages/cli-core/src/format/format.ts index badc200020..6b38175322 100644 --- a/packages/cli-core/src/format/format.ts +++ b/packages/cli-core/src/format/format.ts @@ -1,5 +1,5 @@ import * as os from 'node:os'; -import { blue, bold, cyan, green, grey, underline } from 'kleur/colors'; +import { blue, bold, cyan, green, grey, red, underline } from 'kleur/colors'; /** * Formats various inputs into single string. @@ -13,6 +13,7 @@ export const format = { return cyan(`${binaryRunner} amplify ${command}`); }, }), + error: (message: string) => red(message), note: (message: string) => grey(message), command: (command: string) => cyan(command), success: (message: string) => green(message), diff --git a/packages/cli-core/src/index.ts b/packages/cli-core/src/index.ts index f24d2d872b..4ca21c8e71 100644 --- a/packages/cli-core/src/index.ts +++ b/packages/cli-core/src/index.ts @@ -1,5 +1,5 @@ export * from './prompter/amplify_prompts.js'; -export { COLOR } from './colors.js'; export * from './printer/printer.js'; +export * from './printer.js'; export * from './format/format.js'; export * from './package-manager-controller/package_manager_controller_factory.js'; diff --git a/packages/cli-core/src/package-manager-controller/package_manager_controller_factory.test.ts b/packages/cli-core/src/package-manager-controller/package_manager_controller_factory.test.ts index 06c1d717ce..b7d75c41f3 100644 --- a/packages/cli-core/src/package-manager-controller/package_manager_controller_factory.test.ts +++ b/packages/cli-core/src/package-manager-controller/package_manager_controller_factory.test.ts @@ -62,8 +62,25 @@ void describe('packageManagerControllerFactory', () => { assert.throws( () => packageManagerControllerFactory.getPackageManagerController(), - Error, - 'Package Manager unsupported is not supported.' + { + message: 'Package Manager unsupported is not supported.', + } + ); + }); + + void it('should throw an error for pnpm on Windows', () => { + const userAgent = 'pnpm/1.0.0 node/v15.0.0 darwin x64'; + process.env.npm_config_user_agent = userAgent; + const packageManagerControllerFactory = + new PackageManagerControllerFactory(packageRoot, printer, 'win32'); + + assert.throws( + () => packageManagerControllerFactory.getPackageManagerController(), + { + message: 'Amplify does not support PNPM on Windows.', + details: + 'Details: https://github.com/aws-amplify/amplify-backend/blob/main/packages/create-amplify/README.md', + } ); }); }); diff --git a/packages/cli-core/src/package-manager-controller/package_manager_controller_factory.ts b/packages/cli-core/src/package-manager-controller/package_manager_controller_factory.ts index 6170acbdd0..aa38d98281 100644 --- a/packages/cli-core/src/package-manager-controller/package_manager_controller_factory.ts +++ b/packages/cli-core/src/package-manager-controller/package_manager_controller_factory.ts @@ -1,4 +1,5 @@ import { type PackageManagerController } from '@aws-amplify/plugin-types'; +import { AmplifyUserError } from '@aws-amplify/platform-core'; import { Printer } from '../printer/printer.js'; import { NpmPackageManagerController } from './npm_package_manager_controller.js'; import { PnpmPackageManagerController } from './pnpm_package_manager_controller.js'; @@ -15,7 +16,8 @@ export class PackageManagerControllerFactory { */ constructor( private readonly cwd: string, - private readonly printer: Printer + private readonly printer: Printer, + private readonly platform = process.platform ) {} /** @@ -27,6 +29,16 @@ export class PackageManagerControllerFactory { case 'npm': return new NpmPackageManagerController(this.cwd); case 'pnpm': + if (this.platform === 'win32') { + const message = 'Amplify does not support PNPM on Windows.'; + const details = + 'Details: https://github.com/aws-amplify/amplify-backend/blob/main/packages/create-amplify/README.md'; + throw new AmplifyUserError('UnsupportedPackageManagerError', { + message, + details, + resolution: 'Use a supported package manager for your OS', + }); + } return new PnpmPackageManagerController(this.cwd); case 'yarn-classic': return new YarnClassicPackageManagerController(this.cwd); diff --git a/packages/cli-core/src/printer/printer.ts b/packages/cli-core/src/printer/printer.ts index 5d7bfa3bb1..85107b779e 100644 --- a/packages/cli-core/src/printer/printer.ts +++ b/packages/cli-core/src/printer/printer.ts @@ -1,4 +1,3 @@ -import { COLOR, color } from '../colors.js'; import { EOL } from 'os'; export type RecordValue = string | number | string[] | Date; @@ -39,12 +38,8 @@ export class Printer { /** * Prints a given message (with optional color) to output stream. */ - print = (message: string, colorName?: COLOR) => { - if (colorName) { - this.stdout.write(color(colorName, message)); - } else { - this.stdout.write(message); - } + print = (message: string) => { + this.stdout.write(message); }; /** diff --git a/packages/cli-core/tsconfig.json b/packages/cli-core/tsconfig.json index 2aab102e9b..107b4740f6 100644 --- a/packages/cli-core/tsconfig.json +++ b/packages/cli-core/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "lib" }, - "references": [] + "references": [{ "path": "../platform-core" }] } diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 16fa3e8f64..c5a4ace3ea 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,48 @@ # @aws-amplify/backend-cli +## 0.12.0-beta.6 + +### Patch Changes + +- 05c3c9b: Rename target format type and prop in model gen package +- beb1591: Update text to match sandbox default behavior +- 3e34244: use `format` to replace `color` and remove `color`. +- ee247fd: use printer from cli-core +- 937086b: require "resolution" in AmplifyUserError options +- Updated dependencies [05c3c9b] +- Updated dependencies [3e34244] +- Updated dependencies [ee247fd] +- Updated dependencies [937086b] +- Updated dependencies [b931980] + - @aws-amplify/model-generator@0.5.0-beta.3 + - @aws-amplify/cli-core@0.5.0-beta.2 + - @aws-amplify/sandbox@0.5.2-beta.5 + - @aws-amplify/backend-deployer@0.5.1-beta.2 + - @aws-amplify/platform-core@0.5.0-beta.2 + - @aws-amplify/deployed-backend-client@0.4.0-beta.3 + - @aws-amplify/client-config@0.9.0-beta.4 + - @aws-amplify/backend-secret@0.4.5-beta.2 + +## 0.12.0-beta.5 + +### Patch Changes + +- Updated dependencies [615a3e6] + - @aws-amplify/sandbox@0.5.2-beta.4 + +## 0.12.0-beta.4 + +### Patch Changes + +- Updated dependencies [3998cd3] +- Updated dependencies [79cff6d] +- Updated dependencies [8d9a7a4] +- Updated dependencies [b0ba24d] +- Updated dependencies [8d9a7a4] + - @aws-amplify/cli-core@0.5.0-beta.1 + - @aws-amplify/client-config@0.9.0-beta.3 + - @aws-amplify/sandbox@0.5.2-beta.3 + ## 0.12.0-beta.3 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 3f3289e54b..a5cb76a93d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-cli", - "version": "0.12.0-beta.3", + "version": "0.12.0-beta.6", "description": "Command line interface for various Amplify tools", "bin": { "amplify": "lib/amplify.js" @@ -29,16 +29,16 @@ }, "homepage": "https://github.com/aws-amplify/amplify-backend#readme", "dependencies": { - "@aws-amplify/backend-deployer": "^0.5.1-beta.1", + "@aws-amplify/backend-deployer": "^0.5.1-beta.2", "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", - "@aws-amplify/backend-secret": "^0.4.5-beta.1", - "@aws-amplify/cli-core": "^0.4.1-beta.0", - "@aws-amplify/client-config": "^0.8.1-beta.2", - "@aws-amplify/deployed-backend-client": "^0.4.0-beta.2", + "@aws-amplify/backend-secret": "^0.4.5-beta.2", + "@aws-amplify/cli-core": "^0.5.0-beta.2", + "@aws-amplify/client-config": "^0.9.0-beta.4", + "@aws-amplify/deployed-backend-client": "^0.4.0-beta.3", "@aws-amplify/form-generator": "^0.8.0-beta.1", - "@aws-amplify/model-generator": "^0.4.1-beta.2", - "@aws-amplify/platform-core": "^0.5.0-beta.1", - "@aws-amplify/sandbox": "^0.5.2-beta.2", + "@aws-amplify/model-generator": "^0.5.0-beta.3", + "@aws-amplify/platform-core": "^0.5.0-beta.2", + "@aws-amplify/sandbox": "^0.5.2-beta.5", "@aws-sdk/credential-provider-ini": "^3.465.0", "@aws-sdk/credential-providers": "^3.465.0", "@aws-sdk/region-config-resolver": "^3.465.0", diff --git a/packages/cli/src/client-config/client_config_generator_adapter.ts b/packages/cli/src/client-config/client_config_generator_adapter.ts index 7c652360da..6b631dbbb3 100644 --- a/packages/cli/src/client-config/client_config_generator_adapter.ts +++ b/packages/cli/src/client-config/client_config_generator_adapter.ts @@ -6,7 +6,7 @@ import { } from '@aws-amplify/client-config'; import { DeployedBackendIdentifier } from '@aws-amplify/deployed-backend-client'; import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; -import { printer } from '../printer.js'; +import { printer } from '@aws-amplify/cli-core'; /** * Adapts static generateClientConfigToFile from @aws-amplify/client-config call to make it injectable and testable. diff --git a/packages/cli/src/commands/configure/configure_profile_command.test.ts b/packages/cli/src/commands/configure/configure_profile_command.test.ts index 4931d402bd..145d4ebb18 100644 --- a/packages/cli/src/commands/configure/configure_profile_command.test.ts +++ b/packages/cli/src/commands/configure/configure_profile_command.test.ts @@ -3,10 +3,9 @@ import yargs, { CommandModule } from 'yargs'; import { TestCommandRunner } from '../../test-utils/command_runner.js'; import assert from 'node:assert'; import { ConfigureProfileCommand } from './configure_profile_command.js'; -import { AmplifyPrompter } from '@aws-amplify/cli-core'; +import { AmplifyPrompter, printer } from '@aws-amplify/cli-core'; import { Open } from '../open/open.js'; import { ProfileController } from './profile_controller.js'; -import { printer } from '../../printer.js'; const testAccessKeyId = 'testAccessKeyId'; const testSecretAccessKey = 'testSecretAccessKey'; diff --git a/packages/cli/src/commands/configure/configure_profile_command.ts b/packages/cli/src/commands/configure/configure_profile_command.ts index a3e3d862af..2f2a3272cb 100644 --- a/packages/cli/src/commands/configure/configure_profile_command.ts +++ b/packages/cli/src/commands/configure/configure_profile_command.ts @@ -1,11 +1,10 @@ import { Argv, CommandModule } from 'yargs'; -import { AmplifyPrompter } from '@aws-amplify/cli-core'; +import { AmplifyPrompter, printer } from '@aws-amplify/cli-core'; import { DEFAULT_PROFILE } from '@smithy/shared-ini-file-loader'; import { EOL } from 'os'; import { Open } from '../open/open.js'; import { ArgumentsKebabCase } from '../../kebab_case.js'; import { ProfileController } from './profile_controller.js'; -import { printer } from '../../printer.js'; const configureAccountUrl = 'https://docs.amplify.aws/gen2/start/account-setup/'; diff --git a/packages/cli/src/commands/configure/telemetry/configure_telemetry_command.test.ts b/packages/cli/src/commands/configure/telemetry/configure_telemetry_command.test.ts index 67dfe573a6..76fa5e5b27 100644 --- a/packages/cli/src/commands/configure/telemetry/configure_telemetry_command.test.ts +++ b/packages/cli/src/commands/configure/telemetry/configure_telemetry_command.test.ts @@ -7,7 +7,7 @@ import { USAGE_DATA_TRACKING_ENABLED, configControllerFactory, } from '@aws-amplify/platform-core'; -import { printer } from '../../../printer.js'; +import { printer } from '@aws-amplify/cli-core'; void describe('configure command', () => { const mockedConfigControllerSet = mock.fn(); diff --git a/packages/cli/src/commands/configure/telemetry/configure_telemetry_command.ts b/packages/cli/src/commands/configure/telemetry/configure_telemetry_command.ts index 6c7aada1fd..34093da1c0 100644 --- a/packages/cli/src/commands/configure/telemetry/configure_telemetry_command.ts +++ b/packages/cli/src/commands/configure/telemetry/configure_telemetry_command.ts @@ -3,7 +3,7 @@ import { USAGE_DATA_TRACKING_ENABLED, } from '@aws-amplify/platform-core'; import { Argv, CommandModule } from 'yargs'; -import { printer } from '../../../printer.js'; +import { printer } from '@aws-amplify/cli-core'; /** * Command to configure AWS Amplify profile. */ diff --git a/packages/cli/src/commands/info/info_command.test.ts b/packages/cli/src/commands/info/info_command.test.ts index a36f5e1440..9d99fc3e92 100644 --- a/packages/cli/src/commands/info/info_command.test.ts +++ b/packages/cli/src/commands/info/info_command.test.ts @@ -5,7 +5,7 @@ import { InfoCommand } from './info_command.js'; import { EnvironmentInfoProvider } from '../../info/env_info_provider.js'; import { CdkInfoProvider } from '../../info/cdk_info_provider.js'; import { TestCommandRunner } from '../../test-utils/command_runner.js'; -import { printer } from '../../printer.js'; +import { printer } from '@aws-amplify/cli-core'; import assert from 'node:assert'; import yargs from 'yargs'; diff --git a/packages/cli/src/commands/info/info_command.ts b/packages/cli/src/commands/info/info_command.ts index 1e1d16eeaa..50776b96d3 100644 --- a/packages/cli/src/commands/info/info_command.ts +++ b/packages/cli/src/commands/info/info_command.ts @@ -2,7 +2,7 @@ import * as os from 'node:os'; import { Argv, CommandModule } from 'yargs'; import { CdkInfoProvider } from '../../info/cdk_info_provider.js'; import { EnvironmentInfoProvider } from '../../info/env_info_provider.js'; -import { printer } from '../../printer.js'; +import { printer } from '@aws-amplify/cli-core'; /** * Represents the InfoCommand class. diff --git a/packages/cli/src/commands/open/open.ts b/packages/cli/src/commands/open/open.ts index cb78e3408b..b5967ef041 100644 --- a/packages/cli/src/commands/open/open.ts +++ b/packages/cli/src/commands/open/open.ts @@ -1,6 +1,6 @@ import opn, { Options } from 'open'; import { ChildProcess } from 'child_process'; -import { printer } from '../../printer.js'; +import { printer } from '@aws-amplify/cli-core'; /** * Helper class to open apps (URLs, files, executable). Cross-platform. diff --git a/packages/cli/src/commands/pipeline-deploy/pipeline_deploy_command_factory.ts b/packages/cli/src/commands/pipeline-deploy/pipeline_deploy_command_factory.ts index c759f37d75..91aa6ec7da 100644 --- a/packages/cli/src/commands/pipeline-deploy/pipeline_deploy_command_factory.ts +++ b/packages/cli/src/commands/pipeline-deploy/pipeline_deploy_command_factory.ts @@ -1,6 +1,9 @@ import { CommandModule } from 'yargs'; import { BackendDeployerFactory } from '@aws-amplify/backend-deployer'; -import { PackageManagerControllerFactory } from '@aws-amplify/cli-core'; +import { + PackageManagerControllerFactory, + printer, +} from '@aws-amplify/cli-core'; import { PipelineDeployCommand, @@ -8,7 +11,6 @@ import { } from './pipeline_deploy_command.js'; import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; import { ClientConfigGeneratorAdapter } from '../../client-config/client_config_generator_adapter.js'; -import { printer } from '../../printer.js'; /** * Creates pipeline deploy command diff --git a/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.test.ts b/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.test.ts index 3beaa74d7e..fee5a2dbe7 100644 --- a/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.test.ts +++ b/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, it, mock } from 'node:test'; -import { AmplifyPrompter } from '@aws-amplify/cli-core'; +import { AmplifyPrompter, printer } from '@aws-amplify/cli-core'; import yargs, { CommandModule } from 'yargs'; import { TestCommandRunner } from '../../../test-utils/command_runner.js'; import assert from 'node:assert'; @@ -9,7 +9,6 @@ import { SandboxSingletonFactory } from '@aws-amplify/sandbox'; import { createSandboxSecretCommand } from '../sandbox-secret/sandbox_secret_command_factory.js'; import { ClientConfigGeneratorAdapter } from '../../../client-config/client_config_generator_adapter.js'; import { CommandMiddleware } from '../../../command_middleware.js'; -import { printer } from '../../../printer.js'; void describe('sandbox delete command', () => { let commandRunner: TestCommandRunner; diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.test.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.test.ts index f8dd518ceb..c97d3e5bde 100644 --- a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.test.ts +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.test.ts @@ -9,7 +9,7 @@ import { getSecretClient, } from '@aws-amplify/backend-secret'; import { SandboxSecretGetCommand } from './sandbox_secret_get_command.js'; -import { printer } from '../../../printer.js'; +import { printer } from '@aws-amplify/cli-core'; const printRecordsMock = mock.method(printer, 'printRecords'); diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.ts index 3f54bf3248..d812c1becf 100644 --- a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.ts +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_get_command.ts @@ -2,7 +2,7 @@ import { Argv, CommandModule } from 'yargs'; import { SecretClient } from '@aws-amplify/backend-secret'; import { SandboxBackendIdResolver } from '../sandbox_id_resolver.js'; import { ArgumentsKebabCase } from '../../../kebab_case.js'; -import { printer } from '../../../printer.js'; +import { printer } from '@aws-amplify/cli-core'; /** * Command to get sandbox secret. diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.test.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.test.ts index 049a1ff6c0..2b8faa692d 100644 --- a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.test.ts +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.test.ts @@ -5,7 +5,7 @@ import assert from 'node:assert'; import { SandboxBackendIdResolver } from '../sandbox_id_resolver.js'; import { Secret, getSecretClient } from '@aws-amplify/backend-secret'; import { SandboxSecretListCommand } from './sandbox_secret_list_command.js'; -import { printer } from '../../../printer.js'; +import { printer } from '@aws-amplify/cli-core'; const testBackendId = 'testBackendId'; const testSandboxName = 'testSandboxName'; diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.ts index be4e3cf4d3..057ea05bfa 100644 --- a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.ts +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_list_command.ts @@ -1,7 +1,7 @@ import { CommandModule } from 'yargs'; import { SecretClient } from '@aws-amplify/backend-secret'; import { SandboxBackendIdResolver } from '../sandbox_id_resolver.js'; -import { printer } from '../../../printer.js'; +import { printer } from '@aws-amplify/cli-core'; /** * Command to list sandbox secrets. diff --git a/packages/cli/src/commands/sandbox/sandbox_command.test.ts b/packages/cli/src/commands/sandbox/sandbox_command.test.ts index f692ec54cf..27af646a2b 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command.test.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, it, mock } from 'node:test'; -import { AmplifyPrompter } from '@aws-amplify/cli-core'; +import { AmplifyPrompter, printer } from '@aws-amplify/cli-core'; import yargs, { CommandModule } from 'yargs'; import { TestCommandError, @@ -16,7 +16,6 @@ import { createSandboxSecretCommand } from './sandbox-secret/sandbox_secret_comm import { ClientConfigGeneratorAdapter } from '../../client-config/client_config_generator_adapter.js'; import { CommandMiddleware } from '../../command_middleware.js'; import path from 'path'; -import { printer } from '../../printer.js'; void describe('sandbox command factory', () => { void it('instantiate a sandbox command correctly', () => { diff --git a/packages/cli/src/commands/sandbox/sandbox_command.ts b/packages/cli/src/commands/sandbox/sandbox_command.ts index ca22f3b359..70fe89a2a3 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command.ts @@ -120,7 +120,7 @@ export class SandboxCommand .version(false) .option('dir-to-watch', { describe: - 'Directory to watch for file changes. All subdirectories and files will be included. defaults to the current directory.', + 'Directory to watch for file changes. All subdirectories and files will be included. Defaults to the amplify directory.', type: 'string', array: false, global: false, diff --git a/packages/cli/src/commands/sandbox/sandbox_command_factory.ts b/packages/cli/src/commands/sandbox/sandbox_command_factory.ts index e8e606ad8c..7170e49636 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command_factory.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command_factory.ts @@ -14,7 +14,7 @@ import { } from '@aws-amplify/platform-core'; import { SandboxEventHandlerFactory } from './sandbox_event_handler_factory.js'; import { CommandMiddleware } from '../../command_middleware.js'; -import { printer } from '../../printer.js'; +import { printer } from '@aws-amplify/cli-core'; /** * Creates wired sandbox command. diff --git a/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.test.ts b/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.test.ts index b9e4070ba4..a6444cc9e8 100644 --- a/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.test.ts +++ b/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.test.ts @@ -12,7 +12,7 @@ import { ClientConfigLifecycleHandler } from '../../client-config/client_config_ import fs from 'fs'; import fsp from 'fs/promises'; import path from 'node:path'; -import { printer } from '../../printer.js'; +import { printer } from '@aws-amplify/cli-core'; void describe('sandbox_event_handler_factory', () => { // client config mocks @@ -89,6 +89,7 @@ void describe('sandbox_event_handler_factory', () => { void it('calls the usage emitter on the failedDeployment event with AmplifyError', async () => { const testError = new AmplifyUserError('BackendBuildError', { message: 'test message', + resolution: 'test resolution', }); await Promise.all( eventFactory diff --git a/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.ts b/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.ts index 8e1438409c..a2e8700e8a 100644 --- a/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.ts +++ b/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.ts @@ -2,8 +2,7 @@ import { SandboxEventHandlerCreator } from './sandbox_command.js'; import { BackendIdentifier } from '@aws-amplify/plugin-types'; import { AmplifyError, UsageDataEmitter } from '@aws-amplify/platform-core'; import { DeployResult } from '@aws-amplify/backend-deployer'; -import { COLOR } from '@aws-amplify/cli-core'; -import { printer } from '../../printer.js'; +import { format, printer } from '@aws-amplify/cli-core'; /** * Coordinates creation of sandbox event handlers @@ -46,18 +45,17 @@ export class SandboxEventHandlerFactory { } catch (error) { // Don't crash sandbox if config cannot be generated, but print the error message printer.print( - 'Amplify configuration could not be generated.', - COLOR.RED + format.error('Amplify configuration could not be generated.') ); if (error instanceof Error) { - printer.print(error.message, COLOR.RED); + printer.print(format.error(error.message)); } else { try { - printer.print(JSON.stringify(error, null, 2), COLOR.RED); + printer.print(format.error(JSON.stringify(error, null, 2))); } catch { // fallback in case there's an error stringify the error // like with circular references. - printer.print('Unknown error', COLOR.RED); + printer.print(format.error('Unknown error')); } } } diff --git a/packages/cli/src/error_handler.test.ts b/packages/cli/src/error_handler.test.ts index 682460f0d8..771716002a 100644 --- a/packages/cli/src/error_handler.test.ts +++ b/packages/cli/src/error_handler.test.ts @@ -4,10 +4,9 @@ import { generateCommandFailureHandler, } from './error_handler.js'; import { Argv } from 'yargs'; -import { COLOR } from '@aws-amplify/cli-core'; +import { printer } from '@aws-amplify/cli-core'; import assert from 'node:assert'; import { InvalidCredentialError } from './error/credential_error.js'; -import { printer } from './printer.js'; const mockPrint = mock.method(printer, 'print'); @@ -36,10 +35,7 @@ void describe('generateCommandFailureHandler', () => { assert.equal(mockPrint.mock.callCount(), 1); assert.equal(mockShowHelp.mock.callCount(), 1); assert.equal(mockExit.mock.callCount(), 1); - assert.deepStrictEqual(mockPrint.mock.calls[0].arguments, [ - someMsg, - COLOR.RED, - ]); + assert.match(mockPrint.mock.calls[0].arguments[0], new RegExp(someMsg)); }); void it('prints message from error object', () => { @@ -48,8 +44,10 @@ void describe('generateCommandFailureHandler', () => { assert.equal(mockPrint.mock.callCount(), 1); assert.equal(mockShowHelp.mock.callCount(), 1); assert.equal(mockExit.mock.callCount(), 1); - assert.match(mockPrint.mock.calls[0].arguments[0], new RegExp(errMsg)); - assert.equal(mockPrint.mock.calls[0].arguments[1], COLOR.RED); + assert.match( + mockPrint.mock.calls[0].arguments[0] as string, + new RegExp(errMsg) + ); }); void it('handles a prompt force close error', () => { @@ -69,8 +67,10 @@ void describe('generateCommandFailureHandler', () => { ); assert.equal(mockExit.mock.callCount(), 1); assert.equal(mockPrint.mock.callCount(), 1); - assert.match(mockPrint.mock.calls[0].arguments[0], new RegExp(errMsg)); - assert.equal(mockPrint.mock.calls[0].arguments[1], COLOR.RED); + assert.match( + mockPrint.mock.calls[0].arguments[0] as string, + new RegExp(errMsg) + ); }); void it('prints error cause message, if any', () => { @@ -82,10 +82,9 @@ void describe('generateCommandFailureHandler', () => { assert.equal(mockExit.mock.callCount(), 1); assert.equal(mockPrint.mock.callCount(), 2); assert.match( - mockPrint.mock.calls[1].arguments[0], + mockPrint.mock.calls[1].arguments[0] as string, new RegExp(errorMessage) ); - assert.equal(mockPrint.mock.calls[1].arguments[1], COLOR.RED); }); }); diff --git a/packages/cli/src/error_handler.ts b/packages/cli/src/error_handler.ts index ded56ab88f..7815dd411a 100644 --- a/packages/cli/src/error_handler.ts +++ b/packages/cli/src/error_handler.ts @@ -1,6 +1,5 @@ -import { COLOR } from '@aws-amplify/cli-core'; +import { format, printer } from '@aws-amplify/cli-core'; import { InvalidCredentialError } from './error/credential_error.js'; -import { printer } from './printer.js'; import { EOL } from 'os'; import { Argv } from 'yargs'; @@ -80,16 +79,15 @@ const handleError = ( if (isUserForceClosePromptError(error)) { return; } - if (error instanceof InvalidCredentialError) { - printer.print(`${error.message}${EOL}`, COLOR.RED); + printer.print(format.error(`${error.message}${EOL}`)); return; } printMessagePreamble?.(); - printer.print(message || String(error), COLOR.RED); + printer.print(format.error(message || String(error))); if (errorHasCauseMessage(error)) { - printer.print(error.cause.message, COLOR.RED); + printer.print(format.error(error.cause.message)); } printer.printNewLine(); }; diff --git a/packages/cli/src/form-generation/form_generation_handler.ts b/packages/cli/src/form-generation/form_generation_handler.ts index 29a344630d..5f250b9e13 100644 --- a/packages/cli/src/form-generation/form_generation_handler.ts +++ b/packages/cli/src/form-generation/form_generation_handler.ts @@ -2,7 +2,7 @@ import { createLocalGraphqlFormGenerator } from '@aws-amplify/form-generator'; import { createGraphqlDocumentGenerator } from '@aws-amplify/model-generator'; import { DeployedBackendIdentifier } from '@aws-amplify/deployed-backend-client'; import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; -import { printer } from '../printer.js'; +import { printer } from '@aws-amplify/cli-core'; type FormGenerationParams = { modelsOutDir: string; @@ -31,7 +31,7 @@ export class FormGenerationHandler { credentialProvider, }); const modelsResult = await graphqlClientGenerator.generateModels({ - language: 'typescript', + targetFormat: 'typescript', }); await modelsResult.writeToDirectory(modelsOutDir, (message) => printer.log(message) diff --git a/packages/cli/src/printer.ts b/packages/cli/src/printer.ts deleted file mode 100644 index cc70ed21ed..0000000000 --- a/packages/cli/src/printer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { LogLevel, Printer } from '@aws-amplify/cli-core'; - -const minimumLogLevel = process.argv.includes('--debug') - ? LogLevel.DEBUG - : LogLevel.INFO; - -export const printer = new Printer(minimumLogLevel); diff --git a/packages/client-config/API.md b/packages/client-config/API.md index 323f21e7e6..c0ad494c27 100644 --- a/packages/client-config/API.md +++ b/packages/client-config/API.md @@ -9,8 +9,10 @@ import { DeployedBackendIdentifier } from '@aws-amplify/deployed-backend-client' // @public (undocumented) export type AnalyticsClientConfig = { + aws_mobile_analytics_app_id?: string; + aws_mobile_analytics_app_region?: string; Analytics?: { - AWSPinpoint: { + Pinpoint: { appId: string; region: string; }; diff --git a/packages/client-config/CHANGELOG.md b/packages/client-config/CHANGELOG.md index 543de3cbbc..2bbe113bdc 100644 --- a/packages/client-config/CHANGELOG.md +++ b/packages/client-config/CHANGELOG.md @@ -1,5 +1,22 @@ # @aws-amplify/client-config +## 0.9.0-beta.4 + +### Patch Changes + +- Updated dependencies [05c3c9b] +- Updated dependencies [937086b] +- Updated dependencies [b931980] + - @aws-amplify/model-generator@0.5.0-beta.3 + - @aws-amplify/platform-core@0.5.0-beta.2 + - @aws-amplify/deployed-backend-client@0.4.0-beta.3 + +## 0.9.0-beta.3 + +### Minor Changes + +- 79cff6d: fix(client-config): add legacy analytics configuration key + ## 0.8.1-beta.2 ### Patch Changes diff --git a/packages/client-config/package.json b/packages/client-config/package.json index c0defc4e86..9eefa276d2 100644 --- a/packages/client-config/package.json +++ b/packages/client-config/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/client-config", - "version": "0.8.1-beta.2", + "version": "0.9.0-beta.4", "type": "module", "publishConfig": { "access": "public" @@ -24,9 +24,9 @@ "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", - "@aws-amplify/deployed-backend-client": "^0.4.0-beta.2", - "@aws-amplify/model-generator": "^0.4.1-beta.2", - "@aws-amplify/platform-core": "^0.5.0-beta.1", + "@aws-amplify/deployed-backend-client": "^0.4.0-beta.3", + "@aws-amplify/model-generator": "^0.5.0-beta.3", + "@aws-amplify/platform-core": "^0.5.0-beta.2", "@aws-sdk/client-amplify": "^3.465.0", "@aws-sdk/client-cloudformation": "^3.465.0", "@aws-sdk/client-ssm": "^3.465.0", diff --git a/packages/client-config/src/client-config-types/analytics_client_config.ts b/packages/client-config/src/client-config-types/analytics_client_config.ts index 79020179a8..5884ea0cdc 100644 --- a/packages/client-config/src/client-config-types/analytics_client_config.ts +++ b/packages/client-config/src/client-config-types/analytics_client_config.ts @@ -1,6 +1,10 @@ export type AnalyticsClientConfig = { + // legacy + aws_mobile_analytics_app_id?: string; + aws_mobile_analytics_app_region?: string; + Analytics?: { - AWSPinpoint: { + Pinpoint: { appId: string; region: string; }; diff --git a/packages/client-config/src/client-config-writer/client_config_converter.test.ts b/packages/client-config/src/client-config-writer/client_config_converter.test.ts index 4d8266dd15..d5dff08df1 100644 --- a/packages/client-config/src/client-config-writer/client_config_converter.test.ts +++ b/packages/client-config/src/client-config-writer/client_config_converter.test.ts @@ -283,7 +283,7 @@ void describe('client config converter', () => { void it('converts analytics config', () => { const clientConfig: ClientConfig = { Analytics: { - AWSPinpoint: { + Pinpoint: { appId: 'test_pinpoint_id', region: 'us-west-2', }, diff --git a/packages/client-config/src/client-config-writer/client_config_converter.ts b/packages/client-config/src/client-config-writer/client_config_converter.ts index 8ce729fb3a..0c0faa573b 100644 --- a/packages/client-config/src/client-config-writer/client_config_converter.ts +++ b/packages/client-config/src/client-config-writer/client_config_converter.ts @@ -150,11 +150,11 @@ export class ClientConfigConverter { plugins: { awsPinpointAnalyticsPlugin: { pinpointAnalytics: { - region: clientConfig.Analytics.AWSPinpoint.region, - appId: clientConfig.Analytics.AWSPinpoint.appId, + region: clientConfig.Analytics.Pinpoint.region, + appId: clientConfig.Analytics.Pinpoint.appId, }, pinpointTargeting: { - region: clientConfig.Analytics.AWSPinpoint.region, + region: clientConfig.Analytics.Pinpoint.region, }, }, }, diff --git a/packages/create-amplify/CHANGELOG.md b/packages/create-amplify/CHANGELOG.md index 900e077368..8f3fc3c2bf 100644 --- a/packages/create-amplify/CHANGELOG.md +++ b/packages/create-amplify/CHANGELOG.md @@ -1,5 +1,30 @@ # create-amplify +## 0.7.0-beta.4 + +### Patch Changes + +- ee247fd: use printer from cli-core +- Updated dependencies [3e34244] +- Updated dependencies [ee247fd] +- Updated dependencies [937086b] + - @aws-amplify/cli-core@0.5.0-beta.2 + - @aws-amplify/platform-core@0.5.0-beta.2 + +## 0.7.0-beta.3 + +### Minor Changes + +- eec100e: Updates the create flow with colors, verbosity and better structure + +### Patch Changes + +- Updated dependencies [3998cd3] +- Updated dependencies [8d9a7a4] +- Updated dependencies [b0ba24d] +- Updated dependencies [8d9a7a4] + - @aws-amplify/cli-core@0.5.0-beta.1 + ## 0.6.1-beta.2 ### Patch Changes diff --git a/packages/create-amplify/README.md b/packages/create-amplify/README.md index 7fb0a2c91d..5e0875b641 100644 --- a/packages/create-amplify/README.md +++ b/packages/create-amplify/README.md @@ -5,3 +5,11 @@ create-amplify is a package for scaffolding an Amplify project by running `npm c ## Usage In a frontend project folder or empty folder, run `npm create amplify`. + +# Frequently Asked Questions + +1. Does Amplify support pnpm on Windows? + No. Amplify uses nested `node_modules`, but "pnpm does not create deep folders, it stores all packages flatly and uses symbolic links to create the dependency tree structure." See details: on [PNPM docs](https://pnpm.io/faq#but-the-nested-node_modules-approach-is-incompatible-with-windows). + +2. Does Amplify support [Yarn Plug'n'Play](https://yarnpkg.com/features/pnp)? + No. Please run `yarn config set nodeLinker node-modules` to use `node_modules` instead. diff --git a/packages/create-amplify/package.json b/packages/create-amplify/package.json index c397c2f69f..be88f0e55b 100644 --- a/packages/create-amplify/package.json +++ b/packages/create-amplify/package.json @@ -1,6 +1,6 @@ { "name": "create-amplify", - "version": "0.6.1-beta.2", + "version": "0.7.0-beta.4", "type": "module", "publishConfig": { "access": "public" @@ -16,8 +16,8 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/cli-core": "^0.4.1-beta.0", - "@aws-amplify/platform-core": "^0.5.0-beta.1", + "@aws-amplify/cli-core": "^0.5.0-beta.2", + "@aws-amplify/platform-core": "^0.5.0-beta.2", "@aws-amplify/plugin-types": "^0.9.0-beta.0", "execa": "^8.0.1", "kleur": "^4.1.5", diff --git a/packages/create-amplify/src/amplify_project_creator.test.ts b/packages/create-amplify/src/amplify_project_creator.test.ts index 893084179b..158d96115e 100644 --- a/packages/create-amplify/src/amplify_project_creator.test.ts +++ b/packages/create-amplify/src/amplify_project_creator.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, it, mock } from 'node:test'; import assert from 'assert'; import { PackageManagerController } from '@aws-amplify/plugin-types'; import { AmplifyProjectCreator } from './amplify_project_creator.js'; -import { printer } from './printer.js'; +import { printer } from '@aws-amplify/cli-core'; const logSpy = mock.method(printer, 'log'); const indicateProgressSpy = mock.method(printer, 'indicateProgress'); diff --git a/packages/create-amplify/src/amplify_project_creator.ts b/packages/create-amplify/src/amplify_project_creator.ts index 5c0a4e4c82..e2f9fbf023 100644 --- a/packages/create-amplify/src/amplify_project_creator.ts +++ b/packages/create-amplify/src/amplify_project_creator.ts @@ -1,10 +1,9 @@ import { EOL } from 'os'; -import { LogLevel, format } from '@aws-amplify/cli-core'; +import { LogLevel, format, printer } from '@aws-amplify/cli-core'; import { PackageManagerController } from '@aws-amplify/plugin-types'; import { ProjectRootValidator } from './project_root_validator.js'; import { GitIgnoreInitializer } from './gitignore_initializer.js'; import { InitialProjectFileGenerator } from './initial_project_file_generator.js'; -import { printer } from './printer.js'; const LEARN_MORE_USAGE_DATA_TRACKING_LINK = 'https://docs.amplify.aws/gen2/reference/telemetry'; diff --git a/packages/create-amplify/src/create_amplify.ts b/packages/create-amplify/src/create_amplify.ts index f0d36e9753..a5efef8c9e 100644 --- a/packages/create-amplify/src/create_amplify.ts +++ b/packages/create-amplify/src/create_amplify.ts @@ -10,13 +10,13 @@ import { LogLevel, PackageManagerControllerFactory, + printer, } from '@aws-amplify/cli-core'; import { ProjectRootValidator } from './project_root_validator.js'; import { AmplifyProjectCreator } from './amplify_project_creator.js'; import { getProjectRoot } from './get_project_root.js'; import { GitIgnoreInitializer } from './gitignore_initializer.js'; import { InitialProjectFileGenerator } from './initial_project_file_generator.js'; -import { printer } from './printer.js'; const projectRoot = await getProjectRoot(); diff --git a/packages/create-amplify/src/get_project_root.ts b/packages/create-amplify/src/get_project_root.ts index d89a68982b..63f0e0339c 100644 --- a/packages/create-amplify/src/get_project_root.ts +++ b/packages/create-amplify/src/get_project_root.ts @@ -1,9 +1,8 @@ import fsp from 'fs/promises'; import path from 'path'; import yargs from 'yargs'; -import { AmplifyPrompter, LogLevel } from '@aws-amplify/cli-core'; -import { printer } from './printer.js'; import { AmplifyUserError } from '@aws-amplify/platform-core'; +import { AmplifyPrompter, LogLevel, printer } from '@aws-amplify/cli-core'; /** * Returns the project root directory. diff --git a/packages/create-amplify/src/gitignore_initializer.test.ts b/packages/create-amplify/src/gitignore_initializer.test.ts index a7e7e4eb8b..1f74332530 100644 --- a/packages/create-amplify/src/gitignore_initializer.test.ts +++ b/packages/create-amplify/src/gitignore_initializer.test.ts @@ -3,7 +3,7 @@ import { GitIgnoreInitializer } from './gitignore_initializer.js'; import assert from 'assert'; import * as path from 'path'; import * as os from 'os'; -import { printer } from './printer.js'; +import { printer } from '@aws-amplify/cli-core'; void describe('GitIgnoreInitializer', () => { const logMock = mock.method(printer, 'log'); diff --git a/packages/create-amplify/src/gitignore_initializer.ts b/packages/create-amplify/src/gitignore_initializer.ts index 43e13fc347..1ada737b37 100644 --- a/packages/create-amplify/src/gitignore_initializer.ts +++ b/packages/create-amplify/src/gitignore_initializer.ts @@ -2,8 +2,7 @@ import { existsSync as _existsSync } from 'fs'; import _fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; -import { LogLevel } from '@aws-amplify/cli-core'; -import { printer } from './printer.js'; +import { LogLevel, printer } from '@aws-amplify/cli-core'; /** * Ensure that the .gitignore file exists with the correct contents in the current working directory diff --git a/packages/create-amplify/src/printer.ts b/packages/create-amplify/src/printer.ts deleted file mode 100644 index cc70ed21ed..0000000000 --- a/packages/create-amplify/src/printer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { LogLevel, Printer } from '@aws-amplify/cli-core'; - -const minimumLogLevel = process.argv.includes('--debug') - ? LogLevel.DEBUG - : LogLevel.INFO; - -export const printer = new Printer(minimumLogLevel); diff --git a/packages/deployed-backend-client/API.md b/packages/deployed-backend-client/API.md index a7fcf81461..a117039b50 100644 --- a/packages/deployed-backend-client/API.md +++ b/packages/deployed-backend-client/API.md @@ -114,6 +114,20 @@ export type BackendOutputCredentialsOptions = { credentials: AwsCredentialIdentityProvider; }; +// @public (undocumented) +export enum BackendStatus { + // (undocumented) + DELETE_FAILED = "DELETE_FAILED" +} + +// @public (undocumented) +export type BackendSummaryMetadata = { + name: string; + lastUpdated: Date | undefined; + status: BackendDeploymentStatus; + backendId: BackendIdentifier | undefined; +}; + // @public (undocumented) export enum ConflictResolutionMode { // (undocumented) @@ -126,7 +140,7 @@ export enum ConflictResolutionMode { // @public (undocumented) export type DeployedBackendClient = { - listSandboxes: (listSandboxesRequest?: ListSandboxesRequest) => Promise; + listBackends: (listBackendsRequest?: ListBackendsRequest) => ListBackendsResponse; deleteSandbox: (sandboxBackendIdentifier: Omit) => Promise; getBackendMetadata: (backendId: BackendIdentifier) => Promise; }; @@ -173,22 +187,14 @@ export type FunctionConfiguration = { }; // @public (undocumented) -export type ListSandboxesRequest = { - nextToken?: string; -}; - -// @public (undocumented) -export type ListSandboxesResponse = { - sandboxes: SandboxMetadata[]; - nextToken: string | undefined; +export type ListBackendsRequest = { + deploymentType: DeploymentType; + backendStatusFilters?: BackendStatus[]; }; // @public (undocumented) -export type SandboxMetadata = { - name: string; - lastUpdated: Date | undefined; - status: BackendDeploymentStatus; - backendId: BackendIdentifier | undefined; +export type ListBackendsResponse = { + getBackendSummaryByPage: () => AsyncGenerator; }; // @public (undocumented) diff --git a/packages/deployed-backend-client/CHANGELOG.md b/packages/deployed-backend-client/CHANGELOG.md index e8180106e2..97a6ba89f5 100644 --- a/packages/deployed-backend-client/CHANGELOG.md +++ b/packages/deployed-backend-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @aws-amplify/deployed-backend-client +## 0.4.0-beta.3 + +### Minor Changes + +- b931980: Add listBackends method to return a list of stacks for sandbox and branch deployments + +### Patch Changes + +- Updated dependencies [937086b] + - @aws-amplify/platform-core@0.5.0-beta.2 + ## 0.4.0-beta.2 ### Minor Changes diff --git a/packages/deployed-backend-client/package.json b/packages/deployed-backend-client/package.json index 117e1874c2..f06058b375 100644 --- a/packages/deployed-backend-client/package.json +++ b/packages/deployed-backend-client/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/deployed-backend-client", - "version": "0.4.0-beta.2", + "version": "0.4.0-beta.3", "type": "module", "publishConfig": { "access": "public" @@ -19,7 +19,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", - "@aws-amplify/platform-core": "^0.5.0-beta.1", + "@aws-amplify/platform-core": "^0.5.0-beta.2", "@aws-sdk/client-amplify": "^3.465.0", "@aws-sdk/client-cloudformation": "^3.465.0", "@aws-sdk/client-s3": "^3.465.0", diff --git a/packages/deployed-backend-client/src/deployed_backend_client.ts b/packages/deployed-backend-client/src/deployed_backend_client.ts index b24f2016d1..f6646ee63c 100644 --- a/packages/deployed-backend-client/src/deployed_backend_client.ts +++ b/packages/deployed-backend-client/src/deployed_backend_client.ts @@ -6,12 +6,13 @@ import { import { ApiAuthType, BackendMetadata, + BackendStatus, + BackendSummaryMetadata, ConflictResolutionMode, DeployedBackendClient, FunctionConfiguration, - ListSandboxesRequest, - ListSandboxesResponse, - SandboxMetadata, + ListBackendsRequest, + ListBackendsResponse, } from './deployed_backend_client_factory.js'; import { BackendIdentifierConversions } from '@aws-amplify/platform-core'; import { @@ -82,23 +83,36 @@ export class DefaultDeployedBackendClient implements DeployedBackendClient { return this.buildBackendMetadata(stackName); }; + listBackends = ( + listBackendsRequest?: ListBackendsRequest + ): ListBackendsResponse => { + const backends = this.listBackendsInternal(listBackendsRequest); + return { + getBackendSummaryByPage: () => backends, + }; + }; + /** - * Returns Amplify Sandboxes for the account and region. The number of sandboxes returned can vary + * Returns a list of stacks for specific deployment type and status + * @yields */ - listSandboxes = async ( - listSandboxesRequest?: ListSandboxesRequest - ): Promise => { - const stackMetadata: SandboxMetadata[] = []; - let nextToken = listSandboxesRequest?.nextToken; - + private async *listBackendsInternal( + listBackendsRequest?: ListBackendsRequest + ) { + const stackMetadata: BackendSummaryMetadata[] = []; + let nextToken; + const deploymentType = listBackendsRequest?.deploymentType; + const statusFilter = listBackendsRequest?.backendStatusFilters + ? listBackendsRequest?.backendStatusFilters + : []; do { - const listStacksResponse = await this.listStacks(nextToken); + const listStacksResponse = await this.listStacks(nextToken, statusFilter); + const stackMetadataPromises = listStacksResponse.stackSummaries .filter((stackSummary: StackSummary) => { - return stackSummary.StackStatus !== StackStatus.DELETE_COMPLETE; - }) - .filter((stackSummary: StackSummary) => { - return this.isSandboxStack(stackSummary.StackName); + return ( + this.getBackendStackType(stackSummary.StackName) === deploymentType + ); }) .map(async (stackSummary: StackSummary) => { const deploymentType = await this.tryGetDeploymentType(stackSummary); @@ -121,23 +135,24 @@ export class DefaultDeployedBackendClient implements DeployedBackendClient { stackMetadataPromises ); const filteredMetadata = stackMetadataResolvedPromises.filter( - (stackMetadata) => stackMetadata.deploymentType === 'sandbox' + (stackMetadata) => stackMetadata.deploymentType === deploymentType ); stackMetadata.push(...filteredMetadata); nextToken = listStacksResponse.nextToken; - } while (stackMetadata.length === 0 && nextToken); - return { - sandboxes: stackMetadata, - nextToken, - }; - }; + if (stackMetadata.length !== 0) { + yield stackMetadata; + } + } while (stackMetadata.length === 0 && nextToken); + } - private isSandboxStack = (stackName: string | undefined): boolean => { + private getBackendStackType = ( + stackName: string | undefined + ): string | undefined => { const backendIdentifier = BackendIdentifierConversions.fromStackName(stackName); - return backendIdentifier?.type === 'sandbox'; + return backendIdentifier?.type; }; private tryGetDeploymentType = async ( @@ -166,13 +181,22 @@ export class DefaultDeployedBackendClient implements DeployedBackendClient { }; private listStacks = async ( - nextToken: string | undefined + nextToken: string | undefined, + stackStatusFilter: BackendStatus[] ): Promise<{ stackSummaries: StackSummary[]; nextToken: string | undefined; }> => { const stacks: ListStacksCommandOutput = await this.cfnClient.send( - new ListStacksCommand({ NextToken: nextToken }) + new ListStacksCommand({ + NextToken: nextToken, + StackStatusFilter: + stackStatusFilter.length > 0 + ? stackStatusFilter + : Object.values(StackStatus).filter( + (status) => status !== StackStatus.DELETE_COMPLETE + ), + }) ); nextToken = stacks.NextToken; return { stackSummaries: stacks.StackSummaries ?? [], nextToken }; diff --git a/packages/deployed-backend-client/src/deployed_backend_client_factory.ts b/packages/deployed-backend-client/src/deployed_backend_client_factory.ts index a50997870f..340dbaa282 100644 --- a/packages/deployed-backend-client/src/deployed_backend_client_factory.ts +++ b/packages/deployed-backend-client/src/deployed_backend_client_factory.ts @@ -26,15 +26,16 @@ export enum ApiAuthType { AMAZON_COGNITO_USER_POOLS = 'AMAZON_COGNITO_USER_POOLS', } -export type SandboxMetadata = { +export type BackendSummaryMetadata = { name: string; lastUpdated: Date | undefined; status: BackendDeploymentStatus; backendId: BackendIdentifier | undefined; }; -export type ListSandboxesRequest = { - nextToken?: string; +export type ListBackendsRequest = { + deploymentType: DeploymentType; + backendStatusFilters?: BackendStatus[]; }; export type DeployedBackendResource = { @@ -81,9 +82,8 @@ export type FunctionConfiguration = { functionName: string; }; -export type ListSandboxesResponse = { - sandboxes: SandboxMetadata[]; - nextToken: string | undefined; +export type ListBackendsResponse = { + getBackendSummaryByPage: () => AsyncGenerator; }; export enum BackendDeploymentStatus { @@ -95,10 +95,14 @@ export enum BackendDeploymentStatus { UNKNOWN = 'UNKNOWN', } +export enum BackendStatus { + DELETE_FAILED = 'DELETE_FAILED', +} + export type DeployedBackendClient = { - listSandboxes: ( - listSandboxesRequest?: ListSandboxesRequest - ) => Promise; + listBackends: ( + listBackendsRequest?: ListBackendsRequest + ) => ListBackendsResponse; deleteSandbox: ( sandboxBackendIdentifier: Omit ) => Promise; diff --git a/packages/deployed-backend-client/src/deployed_backend_client_list_delete_failed_stacks.test.ts b/packages/deployed-backend-client/src/deployed_backend_client_list_delete_failed_stacks.test.ts new file mode 100644 index 0000000000..bae2dd246f --- /dev/null +++ b/packages/deployed-backend-client/src/deployed_backend_client_list_delete_failed_stacks.test.ts @@ -0,0 +1,259 @@ +import { beforeEach, describe, it, mock } from 'node:test'; +import assert from 'node:assert'; +import { + CloudFormation, + DescribeStacksCommand, + ListStacksCommand, + StackStatus, +} from '@aws-sdk/client-cloudformation'; +import { platformOutputKey } from '@aws-amplify/backend-output-schemas'; +import { DefaultBackendOutputClient } from './backend_output_client.js'; +import { DefaultDeployedBackendClient } from './deployed_backend_client.js'; +import { BackendStatus } from './deployed_backend_client_factory.js'; +import { + BackendOutputClientError, + BackendOutputClientErrorType, + StackIdentifier, +} from './index.js'; +import { AmplifyClient } from '@aws-sdk/client-amplify'; +import { S3 } from '@aws-sdk/client-s3'; +import { DeployedResourcesEnumerator } from './deployed-backend-client/deployed_resources_enumerator.js'; +import { StackStatusMapper } from './deployed-backend-client/stack_status_mapper.js'; +import { ArnGenerator } from './deployed-backend-client/arn_generator.js'; +import { ArnParser } from './deployed-backend-client/arn_parser.js'; + +const listStacksMock = { + NextToken: undefined, + StackSummaries: [ + { + StackName: 'amplify-123-name-branch-testHash', + StackStatus: StackStatus.DELETE_FAILED, + CreationTime: new Date(0), + LastUpdatedTime: new Date(1), + }, + ], +}; + +const getOutputMockResponse = { + [platformOutputKey]: { + payload: { + deploymentType: 'branch', + }, + }, +}; + +void describe('Deployed Backend Client list delete failed stacks', () => { + const mockCfnClient = new CloudFormation(); + const mockS3Client = new S3(); + const cfnClientSendMock = mock.method(mockCfnClient, 'send'); + let deployedBackendClient: DefaultDeployedBackendClient; + const listStacksMockFn = mock.fn(); + const mockBackendOutputClient = new DefaultBackendOutputClient( + mockCfnClient, + new AmplifyClient() + ); + const getOutputMock = mock.method(mockBackendOutputClient, 'getOutput'); + const returnedDeleteFailedStacks = [ + { + deploymentType: 'branch', + backendId: { + namespace: '123', + name: 'name', + type: 'branch', + hash: 'testHash', + }, + name: 'amplify-123-name-branch-testHash', + lastUpdated: new Date(1), + status: 'FAILED', + }, + ]; + + beforeEach(() => { + getOutputMock.mock.mockImplementation( + (backendIdentifier: StackIdentifier) => { + if (backendIdentifier.stackName === 'amplify-test-not-a-sandbox') { + return { + ...getOutputMockResponse, + [platformOutputKey]: { + payload: { + deploymentType: 'branch', + }, + }, + }; + } + return getOutputMockResponse; + } + ); + + getOutputMock.mock.resetCalls(); + listStacksMockFn.mock.resetCalls(); + listStacksMockFn.mock.mockImplementation(() => { + return listStacksMock; + }); + cfnClientSendMock.mock.resetCalls(); + const mockSend = (request: ListStacksCommand | DescribeStacksCommand) => { + if (request instanceof ListStacksCommand) { + return listStacksMockFn(request.input); + } + if (request instanceof DescribeStacksCommand) { + const matchingStack = listStacksMock.StackSummaries.find((stack) => { + return stack.StackName === request.input.StackName; + }); + const stack = matchingStack; + return { + Stacks: [stack], + }; + } + throw request; + }; + + cfnClientSendMock.mock.mockImplementation(mockSend); + const deployedResourcesEnumerator = new DeployedResourcesEnumerator( + new StackStatusMapper(), + new ArnGenerator(), + new ArnParser() + ); + mock.method(deployedResourcesEnumerator, 'listDeployedResources', () => []); + deployedBackendClient = new DefaultDeployedBackendClient( + mockCfnClient, + mockS3Client, + mockBackendOutputClient, + deployedResourcesEnumerator, + new StackStatusMapper(), + new ArnParser() + ); + }); + + void it('does not paginate listBackends when one page contains delete failed stacks', async () => { + const failedStacks = deployedBackendClient.listBackends({ + deploymentType: 'branch', + backendStatusFilters: [BackendStatus.DELETE_FAILED], + }); + + for await (const stacks of failedStacks.getBackendSummaryByPage()) { + assert.deepEqual(stacks, returnedDeleteFailedStacks); + } + + assert.equal(listStacksMockFn.mock.callCount(), 1); + }); + + void it('paginates listBackends when first page contains no failed stacks', async () => { + listStacksMockFn.mock.mockImplementationOnce(() => { + return { + StackSummaries: [], + NextToken: 'abc', + }; + }); + const failedStacks = deployedBackendClient.listBackends({ + deploymentType: 'branch', + backendStatusFilters: [BackendStatus.DELETE_FAILED], + }); + assert.deepEqual( + (await failedStacks.getBackendSummaryByPage().next()).value, + returnedDeleteFailedStacks + ); + + assert.deepEqual( + (await failedStacks.getBackendSummaryByPage().next()).done, + true + ); + + assert.equal(listStacksMockFn.mock.callCount(), 2); + }); + + void it('paginates listBackends when one page contains stacks, but it gets filtered due to not deleted failed status', async () => { + listStacksMockFn.mock.mockImplementationOnce(() => { + return { + StackSummaries: [ + { + StackStatus: StackStatus.CREATE_COMPLETE, + }, + ], + NextToken: 'abc', + }; + }); + const failedStacks = deployedBackendClient.listBackends({ + deploymentType: 'branch', + backendStatusFilters: [BackendStatus.DELETE_FAILED], + }); + assert.deepEqual( + (await failedStacks.getBackendSummaryByPage().next()).value, + returnedDeleteFailedStacks + ); + + assert.equal(listStacksMockFn.mock.callCount(), 2); + }); + + void it('paginates listBackends when one page contains stacks, but it gets filtered due to sandbox deploymentType', async () => { + listStacksMockFn.mock.mockImplementationOnce(() => { + return { + StackSummaries: [ + { + StackName: 'amplify-test-not-a-branch', + }, + ], + NextToken: 'abc', + }; + }); + const failedStacks = deployedBackendClient.listBackends({ + deploymentType: 'branch', + backendStatusFilters: [BackendStatus.DELETE_FAILED], + }); + assert.deepEqual( + (await failedStacks.getBackendSummaryByPage().next()).value, + returnedDeleteFailedStacks + ); + + assert.equal(listStacksMockFn.mock.callCount(), 2); + }); + + void it('paginates listBackends when one page contains a stack, but it gets filtered due to not having gen2 outputs', async () => { + getOutputMock.mock.mockImplementationOnce(() => { + throw new BackendOutputClientError( + BackendOutputClientErrorType.METADATA_RETRIEVAL_ERROR, + 'Test metadata retrieval error' + ); + }); + listStacksMockFn.mock.mockImplementationOnce(() => { + return { + StackSummaries: [ + { + StackName: 'amplify-123-name-branch-testHash', + }, + ], + NextToken: 'abc', + }; + }); + const failedStacks = deployedBackendClient.listBackends({ + deploymentType: 'branch', + backendStatusFilters: [BackendStatus.DELETE_FAILED], + }); + assert.deepEqual( + (await failedStacks.getBackendSummaryByPage().next()).value, + returnedDeleteFailedStacks + ); + + assert.equal(listStacksMockFn.mock.callCount(), 2); + }); + + void it('does not paginate listBackends when one page throws an unexpected error fetching gen2 outputs', async () => { + getOutputMock.mock.mockImplementationOnce(() => { + throw new Error('Unexpected Error!'); + }); + listStacksMockFn.mock.mockImplementationOnce(() => { + return { + StackSummaries: [ + { + StackName: 'amplify-123-name-branch-testHash', + }, + ], + NextToken: 'abc', + }; + }); + const listBackendsPromise = deployedBackendClient.listBackends({ + deploymentType: 'branch', + backendStatusFilters: [BackendStatus.DELETE_FAILED], + }); + await assert.rejects(listBackendsPromise.getBackendSummaryByPage().next()); + }); +}); diff --git a/packages/deployed-backend-client/src/deployed_backend_client_list_sandboxes.test.ts b/packages/deployed-backend-client/src/deployed_backend_client_list_sandboxes.test.ts index 8a3c0e92c1..12ff6321c1 100644 --- a/packages/deployed-backend-client/src/deployed_backend_client_list_sandboxes.test.ts +++ b/packages/deployed-backend-client/src/deployed_backend_client_list_sandboxes.test.ts @@ -4,7 +4,6 @@ import { CloudFormation, DescribeStacksCommand, ListStacksCommand, - ListStacksCommandInput, StackStatus, } from '@aws-sdk/client-cloudformation'; import { BackendDeploymentStatus } from './deployed_backend_client_factory.js'; @@ -125,33 +124,37 @@ void describe('Deployed Backend Client list sandboxes', () => { ); }); - void it('does not paginate listSandboxes when one page contains sandboxes', async () => { - const sandboxes = await deployedBackendClient.listSandboxes(); - assert.deepEqual(sandboxes, { - nextToken: undefined, - sandboxes: returnedSandboxes, + void it('does not paginate listBackends when one page contains sandboxes', async () => { + const sandboxes = deployedBackendClient.listBackends({ + deploymentType: 'sandbox', }); + assert.deepEqual( + (await sandboxes.getBackendSummaryByPage().next()).value, + returnedSandboxes + ); assert.equal(listStacksMockFn.mock.callCount(), 1); }); - void it('paginates listSandboxes when first page contains no sandboxes', async () => { + void it('paginates listBackends when first page contains no sandboxes', async () => { listStacksMockFn.mock.mockImplementationOnce(() => { return { StackSummaries: [], NextToken: 'abc', }; }); - const sandboxes = await deployedBackendClient.listSandboxes(); - assert.deepEqual(sandboxes, { - nextToken: undefined, - sandboxes: returnedSandboxes, + const sandboxes = deployedBackendClient.listBackends({ + deploymentType: 'sandbox', }); + assert.deepEqual( + (await sandboxes.getBackendSummaryByPage().next()).value, + returnedSandboxes + ); assert.equal(listStacksMockFn.mock.callCount(), 2); }); - void it('paginates listSandboxes when one page contains sandboxes, but it gets filtered due to deleted status', async () => { + void it('paginates listBackends when one page contains sandboxes, but it gets filtered due to deleted status', async () => { listStacksMockFn.mock.mockImplementationOnce(() => { return { StackSummaries: [ @@ -162,16 +165,18 @@ void describe('Deployed Backend Client list sandboxes', () => { NextToken: 'abc', }; }); - const sandboxes = await deployedBackendClient.listSandboxes(); - assert.deepEqual(sandboxes, { - nextToken: undefined, - sandboxes: returnedSandboxes, + const sandboxes = deployedBackendClient.listBackends({ + deploymentType: 'sandbox', }); + assert.deepEqual( + (await sandboxes.getBackendSummaryByPage().next()).value, + returnedSandboxes + ); assert.equal(listStacksMockFn.mock.callCount(), 2); }); - void it('paginates listSandboxes when one page contains sandboxes, but it gets filtered due to branch deploymentType', async () => { + void it('paginates listBackends when one page contains sandboxes, but it gets filtered due to branch deploymentType', async () => { listStacksMockFn.mock.mockImplementationOnce(() => { return { StackSummaries: [ @@ -182,16 +187,18 @@ void describe('Deployed Backend Client list sandboxes', () => { NextToken: 'abc', }; }); - const sandboxes = await deployedBackendClient.listSandboxes(); - assert.deepEqual(sandboxes, { - nextToken: undefined, - sandboxes: returnedSandboxes, + const sandboxes = deployedBackendClient.listBackends({ + deploymentType: 'sandbox', }); + assert.deepEqual( + (await sandboxes.getBackendSummaryByPage().next()).value, + returnedSandboxes + ); assert.equal(listStacksMockFn.mock.callCount(), 2); }); - void it('paginates listSandboxes when one page contains a stack, but it gets filtered due to not having gen2 outputs', async () => { + void it('paginates listBackends when one page contains a stack, but it gets filtered due to not having gen2 outputs', async () => { getOutputMock.mock.mockImplementationOnce(() => { throw new BackendOutputClientError( BackendOutputClientErrorType.METADATA_RETRIEVAL_ERROR, @@ -208,16 +215,18 @@ void describe('Deployed Backend Client list sandboxes', () => { NextToken: 'abc', }; }); - const sandboxes = await deployedBackendClient.listSandboxes(); - assert.deepEqual(sandboxes, { - nextToken: undefined, - sandboxes: returnedSandboxes, + const sandboxes = deployedBackendClient.listBackends({ + deploymentType: 'sandbox', }); + assert.deepEqual( + (await sandboxes.getBackendSummaryByPage().next()).value, + returnedSandboxes + ); assert.equal(listStacksMockFn.mock.callCount(), 2); }); - void it('does not paginate listSandboxes when one page throws an unexpected error fetching gen2 outputs', async () => { + void it('does not paginate listBackends when one page throws an unexpected error fetching gen2 outputs', async () => { getOutputMock.mock.mockImplementationOnce(() => { throw new Error('Unexpected Error!'); }); @@ -231,46 +240,9 @@ void describe('Deployed Backend Client list sandboxes', () => { NextToken: 'abc', }; }); - const listSandboxesPromise = deployedBackendClient.listSandboxes(); - await assert.rejects(listSandboxesPromise); - }); - - void it('includes a nextToken when there are more pages', async () => { - listStacksMockFn.mock.mockImplementation(() => { - return { - StackSummaries: listStacksMock.StackSummaries, - NextToken: 'abc', - }; - }); - const sandboxes = await deployedBackendClient.listSandboxes(); - assert.deepEqual(sandboxes, { - nextToken: 'abc', - sandboxes: returnedSandboxes, - }); - - assert.equal(listStacksMockFn.mock.callCount(), 1); - }); - - void it('accepts a nextToken to get the next page', async () => { - listStacksMockFn.mock.mockImplementation( - (input: ListStacksCommandInput) => { - if (!input.NextToken) { - return { - StackSummaries: listStacksMock.StackSummaries, - NextToken: 'abc', - }; - } - return listStacksMock; - } - ); - const sandboxes = await deployedBackendClient.listSandboxes({ - nextToken: 'abc', - }); - assert.deepEqual(sandboxes, { - nextToken: undefined, - sandboxes: returnedSandboxes, + const listBackendsPromise = deployedBackendClient.listBackends({ + deploymentType: 'sandbox', }); - - assert.equal(listStacksMockFn.mock.callCount(), 1); + await assert.rejects(listBackendsPromise.getBackendSummaryByPage().next()); }); }); diff --git a/packages/integration-tests/CHANGELOG.md b/packages/integration-tests/CHANGELOG.md index 62c5e9ad70..94f66fbaf8 100644 --- a/packages/integration-tests/CHANGELOG.md +++ b/packages/integration-tests/CHANGELOG.md @@ -1,5 +1,29 @@ # @aws-amplify/integration-tests +## 0.5.0-beta.3 + +### Patch Changes + +- 26cdffd: backend-data: add support for first-class defineFunction + +## 0.5.0-beta.2 + +### Patch Changes + +- 7f5edee: Ensure typed shim files contain only the function name + +## 0.5.0-beta.1 + +### Minor Changes + +- cec91d5: Add dynamic environment variables to function type definition files + +### Patch Changes + +- 912034e: limit defineData call to one +- 3998cd3: Fix how paths is added to tsconfig +- 318335d: Ensure resource access env vars are added to function typed shim files + ## 0.4.4-beta.0 ### Patch Changes diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index ea4474cdd4..8d436f3935 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -1,17 +1,18 @@ { "name": "@aws-amplify/integration-tests", "private": true, - "version": "0.4.4-beta.0", + "version": "0.5.0-beta.3", "type": "module", "devDependencies": { - "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.2", - "@aws-amplify/backend": "^0.13.0-beta.3", - "@aws-amplify/backend-secret": "^0.4.5-beta.1", - "@aws-amplify/client-config": "^0.8.1-beta.2", - "@aws-amplify/data-schema": "^0.13.2", - "@aws-amplify/platform-core": "^0.5.0-beta.1", + "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.5", + "@aws-amplify/backend": "^0.13.0-beta.6", + "@aws-amplify/backend-secret": "^0.4.5-beta.2", + "@aws-amplify/client-config": "^0.9.0-beta.4", + "@aws-amplify/data-schema": "^0.13.15", + "@aws-amplify/platform-core": "^0.5.0-beta.2", "@aws-sdk/client-amplify": "^3.465.0", "@aws-sdk/client-cloudformation": "^3.465.0", + "@aws-sdk/client-iam": "^3.465.0", "@aws-sdk/client-lambda": "^3.465.0", "@aws-sdk/client-s3": "^3.465.0", "@aws-sdk/credential-providers": "^3.465.0", diff --git a/packages/integration-tests/src/package_manager_sanity_checks.test.ts b/packages/integration-tests/src/package_manager_sanity_checks.test.ts index 39f01491a0..09b15d3684 100644 --- a/packages/integration-tests/src/package_manager_sanity_checks.test.ts +++ b/packages/integration-tests/src/package_manager_sanity_checks.test.ts @@ -74,6 +74,9 @@ void describe('getting started happy path', async () => { }); void it('creates new project and deploy them without an error', async () => { + if (packageManager === 'pnpm' && process.platform === 'win32') { + return; + } // TODO: remove the condition once GA https://github.com/aws-amplify/amplify-backend/issues/1013 if (packageManager === 'yarn-classic') { await execa('yarn', ['add', 'create-amplify@beta'], { cwd: tempDir }); @@ -130,4 +133,23 @@ void describe('getting started happy path', async () => { assert.ok(clientConfigStats.isFile()); }); + + void it('creates new project and deploy them without an error', async () => { + if (packageManager === 'pnpm' && process.platform === 'win32') { + await assert.rejects( + execa('pnpm', ['create', 'amplify@beta', '--yes'], { + // TODO: remove the @beta tag once GA https://github.com/aws-amplify/amplify-backend/issues/1013 + cwd: tempDir, + }), + (error) => { + const errorMessage = error instanceof Error ? error.message : ''; + assert.match( + errorMessage, + /Amplify does not support PNPM on Windows./ + ); + return true; + } + ); + } + }); }); diff --git a/packages/integration-tests/src/process-controller/predicated_action_macros.ts b/packages/integration-tests/src/process-controller/predicated_action_macros.ts index febbb40c7c..53e4d95272 100644 --- a/packages/integration-tests/src/process-controller/predicated_action_macros.ts +++ b/packages/integration-tests/src/process-controller/predicated_action_macros.ts @@ -1,5 +1,6 @@ import { PredicatedActionBuilder } from './predicated_action_queue_builder.js'; import { PlatformDeploymentThresholds } from '../test-project-setup/test_project_base.js'; +import { CopyDefinition } from './types.js'; /** * Convenience predicated actions that can be used to build up more complex CLI flows. @@ -42,11 +43,11 @@ export const rejectCleanupSandbox = () => .sendNo(); /** - * Reusable predicated action: Wait for sandbox to become idle and then update the - * backend code which should trigger sandbox again + * Reusable predicated action: Wait for sandbox to become idle, + * then perform the specified file replacements in the backend code which will trigger sandbox again */ -export const updateFileContent = (from: URL, to: URL) => { - return waitForSandboxToBecomeIdle().updateFileContent(from, to); +export const replaceFiles = (replacements: CopyDefinition[]) => { + return waitForSandboxToBecomeIdle().replaceFiles(replacements); }; /** diff --git a/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts b/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts index 2e8b2a18c2..0b5506db6a 100644 --- a/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts +++ b/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts @@ -8,6 +8,7 @@ import fs from 'fs/promises'; import { killExecaProcess } from './execa_process_killer.js'; import { ExecaChildProcess } from 'execa'; +import { CopyDefinition } from './types.js'; export const CONTROL_C = '\x03'; /** @@ -68,13 +69,15 @@ export class PredicatedActionBuilder { * Update the last predicated action to update backend code by copying files from * `from` location to `to` location. */ - updateFileContent = (from: URL, to: URL) => { + replaceFiles = (replacements: CopyDefinition[]) => { this.getLastPredicatedAction().then = { actionType: ActionType.UPDATE_FILE_CONTENT, action: async () => { - await fs.cp(from, to, { - recursive: true, - }); + for (const { source, destination } of replacements) { + await fs.cp(source, destination, { + recursive: true, + }); + } }, }; return this; diff --git a/packages/integration-tests/src/process-controller/types.ts b/packages/integration-tests/src/process-controller/types.ts new file mode 100644 index 0000000000..f1a75bc851 --- /dev/null +++ b/packages/integration-tests/src/process-controller/types.ts @@ -0,0 +1,7 @@ +/** + * Defines a source and destination path tuple for copying file(s) from one location to another + */ +export type CopyDefinition = { + source: URL; + destination: URL; +}; diff --git a/packages/integration-tests/src/test-e2e/deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment.test.ts index 6ead6e7973..52ec8a14e2 100644 --- a/packages/integration-tests/src/test-e2e/deployment.test.ts +++ b/packages/integration-tests/src/test-e2e/deployment.test.ts @@ -16,7 +16,7 @@ import { ensureDeploymentTimeLessThan, interruptSandbox, rejectCleanupSandbox, - updateFileContent, + replaceFiles, } from '../process-controller/predicated_action_macros.js'; import assert from 'node:assert'; import { TestBranch, amplifyAppPool } from '../amplify_app_pool.js'; @@ -149,7 +149,7 @@ void describe('deployment tests', { concurrency: testConcurrencyLevel }, () => { const updates = await testProject.getUpdates(); for (const update of updates) { processController - .do(updateFileContent(update.sourceFile, update.projectFile)) + .do(replaceFiles(update.replacements)) .do(ensureDeploymentTimeLessThan(update.deployThresholdSec)); } diff --git a/packages/integration-tests/src/test-in-memory/backend_submodule_type_exports.test.ts b/packages/integration-tests/src/test-in-memory/backend_submodule_type_exports.test.ts new file mode 100644 index 0000000000..ddbf6fbb76 --- /dev/null +++ b/packages/integration-tests/src/test-in-memory/backend_submodule_type_exports.test.ts @@ -0,0 +1,41 @@ +/** + * If this "test" builds, then it means that these types are available in the types/platform submodule export from @aws-amplify/backend + */ +import { + AuthCfnResources, + AuthResources, + AuthRoleName, + BackendOutputEntry, + BackendOutputStorageStrategy, + BackendSecretResolver, + ConstructContainer, + ConstructContainerEntryGenerator, + ConstructFactory, + ConstructFactoryGetInstanceProps, + FunctionResources, + GenerateContainerEntryProps, + ImportPathVerifier, + ResourceProvider, + SsmEnvironmentEntriesGenerator, + SsmEnvironmentEntry, +} from '@aws-amplify/backend/types/platform'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type UseAllTheThings = { + thing1?: AuthCfnResources; + thing2?: AuthResources; + thing3?: AuthRoleName; + thing4?: BackendOutputEntry; + thing5?: BackendOutputStorageStrategy; + thing6?: BackendSecretResolver; + thing7?: ConstructContainer; + thing8?: ConstructContainerEntryGenerator; + thing9?: ConstructFactory; + thing10?: ConstructFactoryGetInstanceProps; + thing11?: FunctionResources; + thing12?: GenerateContainerEntryProps; + thing13?: ImportPathVerifier; + thing14?: ResourceProvider; + thing15?: SsmEnvironmentEntriesGenerator; + thing16?: SsmEnvironmentEntry; +}; diff --git a/packages/integration-tests/src/test-in-memory/data_storage_auth_with_triggers.test.ts b/packages/integration-tests/src/test-in-memory/data_storage_auth_with_triggers.test.ts index f74c9db9fc..a08fc6cc6a 100644 --- a/packages/integration-tests/src/test-in-memory/data_storage_auth_with_triggers.test.ts +++ b/packages/integration-tests/src/test-in-memory/data_storage_auth_with_triggers.test.ts @@ -52,6 +52,7 @@ void it('data storage auth with triggers', () => { assertExpectedLogicalIds(templates.defaultNodeFunc, 'AWS::Lambda::Function', [ 'defaultNodeFunctionlambda5C194062', 'echoFunclambdaE17DCA46', + 'handler2lambda1B9C7EFF', 'node16Functionlambda97ECC775', 'onUploadlambdaA252C959', 'onDeletelambda96BB6F15', diff --git a/packages/integration-tests/src/test-project-setup/create_empty_amplify_project.ts b/packages/integration-tests/src/test-project-setup/create_empty_amplify_project.ts index 7c26772903..b240c3fd17 100644 --- a/packages/integration-tests/src/test-project-setup/create_empty_amplify_project.ts +++ b/packages/integration-tests/src/test-project-setup/create_empty_amplify_project.ts @@ -16,6 +16,7 @@ export const createEmptyAmplifyProject = async ( projectName: string; projectRoot: string; projectAmplifyDir: string; + projectDotAmplifyDir: string; }> => { const projectRoot = await fs.mkdtemp(path.join(parentDir, projectDirName)); const projectName = `${TEST_PROJECT_PREFIX}-${projectDirName}-${shortUuid()}`; @@ -27,7 +28,10 @@ export const createEmptyAmplifyProject = async ( const projectAmplifyDir = path.join(projectRoot, 'amplify'); await fs.mkdir(projectAmplifyDir); + const projectDotAmplifyDir = path.join(projectRoot, '.amplify'); + await fs.mkdir(projectDotAmplifyDir); + await setupDirAsEsmModule(projectAmplifyDir); - return { projectName, projectRoot, projectAmplifyDir }; + return { projectName, projectRoot, projectAmplifyDir, projectDotAmplifyDir }; }; diff --git a/packages/integration-tests/src/test-project-setup/data_storage_auth_with_triggers.ts b/packages/integration-tests/src/test-project-setup/data_storage_auth_with_triggers.ts index a44daf5e02..25070b1ee6 100644 --- a/packages/integration-tests/src/test-project-setup/data_storage_auth_with_triggers.ts +++ b/packages/integration-tests/src/test-project-setup/data_storage_auth_with_triggers.ts @@ -15,6 +15,7 @@ import { createAmplifySharedSecretName, } from '../shared_secret.js'; import { HeadBucketCommand, S3Client } from '@aws-sdk/client-s3'; +import { GetRoleCommand, IAMClient } from '@aws-sdk/client-iam'; /** * Creates test projects with data, storage, and auth categories. @@ -32,12 +33,17 @@ export class DataStorageAuthWithTriggerTestProjectCreator private readonly secretClient: SecretClient, private readonly lambdaClient: LambdaClient, private readonly s3Client: S3Client, + private readonly iamClient: IAMClient, private readonly resourceFinder: DeployedResourcesFinder ) {} createProject = async (e2eProjectDir: string): Promise => { - const { projectName, projectRoot, projectAmplifyDir } = - await createEmptyAmplifyProject(this.name, e2eProjectDir); + const { + projectName, + projectRoot, + projectAmplifyDir, + projectDotAmplifyDir, + } = await createEmptyAmplifyProject(this.name, e2eProjectDir); const project = new DataStorageAuthWithTriggerTestProject( projectName, @@ -47,6 +53,7 @@ export class DataStorageAuthWithTriggerTestProjectCreator this.secretClient, this.lambdaClient, this.s3Client, + this.iamClient, this.resourceFinder ); await fs.cp( @@ -56,6 +63,12 @@ export class DataStorageAuthWithTriggerTestProjectCreator recursive: true, } ); + + // copy .amplify folder with typedef file from source project + await fs.cp(project.sourceProjectDotAmplifyDirPath, projectDotAmplifyDir, { + recursive: true, + }); + return project; }; } @@ -76,12 +89,17 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { import.meta.url ); - private readonly sourceProjectUpdateDirPath: URL = new URL( - `${this.sourceProjectDirPath}/update-1`, + readonly sourceProjectDotAmplifyDirSuffix = `${this.sourceProjectDirPath}/.amplify`; + + readonly sourceProjectDotAmplifyDirPath: URL = new URL( + this.sourceProjectDotAmplifyDirSuffix, import.meta.url ); - private readonly dataResourceFileSuffix = 'data/resource.ts'; + private readonly sourceProjectUpdateDirPath: URL = new URL( + `${this.sourceProjectDirPath}/hotswap-update-files`, + import.meta.url + ); private readonly testSecretNames = [ 'googleId', @@ -95,6 +113,7 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { private amplifySharedSecret: string; private testBucketName: string; + private testRoleNames: string[]; /** * Create a test project instance. @@ -107,6 +126,7 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { private readonly secretClient: SecretClient, private readonly lambdaClient: LambdaClient, private readonly s3Client: S3Client, + private readonly iamClient: IAMClient, private readonly resourceFinder: DeployedResourcesFinder ) { super(name, projectDirPath, projectAmplifyDirPath, cfnClient); @@ -143,19 +163,19 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { * @inheritdoc */ override async getUpdates(): Promise { - const sourceDataResourceFile = pathToFileURL( - path.join( - fileURLToPath(this.sourceProjectUpdateDirPath), - this.dataResourceFileSuffix - ) - ); - const dataResourceFile = pathToFileURL( - path.join(this.projectAmplifyDirPath, this.dataResourceFileSuffix) - ); return [ { - sourceFile: sourceDataResourceFile, - projectFile: dataResourceFile, + replacements: [this.getUpdateReplacementDefinition('data/resource.ts')], + deployThresholdSec: { + onWindows: 40, + onOther: 30, + }, + }, + { + replacements: [ + this.getUpdateReplacementDefinition('func-src/handler.ts'), + this.getUpdateReplacementDefinition('function.ts'), + ], deployThresholdSec: { onWindows: 40, onOther: 30, @@ -208,8 +228,34 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { ); // store the bucket name in the class so we can assert that it is deleted properly when the stack is torn down this.testBucketName = bucketName[0]; + + // store the roles associated with this deployment so we can assert that they are deleted when the stack is torn down + this.testRoleNames = await this.resourceFinder.findByBackendIdentifier( + backendId, + 'AWS::IAM::Role' + ); + + // ensure typed shim files are generated by checking for onDelete's typed shim + const typedShimStats = await fs.stat( + path.join(this.projectDirPath, '.amplify', 'function-env', 'onDelete.ts') + ); + + assert.ok(typedShimStats.isFile()); } + private getUpdateReplacementDefinition = (suffix: string) => ({ + source: this.getSourcePath(suffix), + destination: this.getTestProjectPath(suffix), + }); + + private getSourcePath = (suffix: string) => + pathToFileURL( + path.join(fileURLToPath(this.sourceProjectUpdateDirPath), suffix) + ); + + private getTestProjectPath = (suffix: string) => + pathToFileURL(path.join(this.projectAmplifyDirPath, suffix)); + private setUpDeployEnvironment = async ( backendId: BackendIdentifier ): Promise => { @@ -256,6 +302,7 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { private assertExpectedCleanup = async () => { await this.waitForBucketDeletion(this.testBucketName); + await this.assertRolesDoNotExist(this.testRoleNames); }; /** @@ -290,4 +337,54 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { throw err; } }; + + private assertRolesDoNotExist = async (roleNames: string[]) => { + const TIMEOUT_MS = 1000 * 60 * 5; // IAM Role stabilization can take up to 2 minutes and we are waiting in between each GetRole call to avoid throttling + const startTime = Date.now(); + + const remainingRoles = new Set(roleNames); + + while (Date.now() - startTime < TIMEOUT_MS && remainingRoles.size > 0) { + // iterate over a copy of the roles set to avoid confusing concurrent modification behavior + for (const roleName of Array.from(remainingRoles)) { + try { + const roleExists = await this.checkRoleExists(roleName); + if (!roleExists) { + remainingRoles.delete(roleName); + } + } catch (err) { + if (err instanceof Error) { + console.log( + `Got error [${err.name}] while polling for deletion of [${roleName}].` + ); + } + // continue polling + } + + // wait a bit between each call to help avoid throttling + await new Promise((resolve) => setTimeout(resolve, 200)); + } + } + if (remainingRoles.size > 0) { + assert.fail( + `Timed out waiting for role deletion. Remaining roles were [${Array.from( + remainingRoles + ).join(', ')}]` + ); + } + // if we got here all the roles were cleaned up within the timeout + }; + + private checkRoleExists = async (roleName: string): Promise => { + try { + await this.iamClient.send(new GetRoleCommand({ RoleName: roleName })); + // if GetRole returns without error, the role exits + return true; + } catch (err) { + if (err instanceof Error && err.name === 'NoSuchEntityException') { + return false; + } + throw err; + } + }; } diff --git a/packages/integration-tests/src/test-project-setup/setup_dir_as_esm_module.ts b/packages/integration-tests/src/test-project-setup/setup_dir_as_esm_module.ts index c0032396e2..0f8bcd9dc1 100644 --- a/packages/integration-tests/src/test-project-setup/setup_dir_as_esm_module.ts +++ b/packages/integration-tests/src/test-project-setup/setup_dir_as_esm_module.ts @@ -29,4 +29,20 @@ export const setupDirAsEsmModule = async (absoluteDirPath: string) => { stdio: 'inherit', cwd: absoluteDirPath, }); + + const pathsObj = { + // The path here is coupled with backend-function's generated typedef file path + '@env/*': ['../.amplify/function-env/*'], + }; + + const tsConfigPath = path.resolve(absoluteDirPath, 'tsconfig.json'); + const tsConfigContent = (await fs.readFile(tsConfigPath, 'utf-8')).replace( + /\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, // Removes all comments + '' + ); + const tsConfigObject = JSON.parse(tsConfigContent); + + // Add paths object and overwrite the tsconfig file + tsConfigObject.compilerOptions.paths = pathsObj; + await fs.writeFile(tsConfigPath, JSON.stringify(tsConfigObject), 'utf-8'); }; diff --git a/packages/integration-tests/src/test-project-setup/test_project_base.ts b/packages/integration-tests/src/test-project-setup/test_project_base.ts index 868a539d1e..29d9ede29e 100644 --- a/packages/integration-tests/src/test-project-setup/test_project_base.ts +++ b/packages/integration-tests/src/test-project-setup/test_project_base.ts @@ -18,6 +18,7 @@ import { } from '@aws-sdk/client-cloudformation'; import fsp from 'fs/promises'; import assert from 'node:assert'; +import { CopyDefinition } from '../process-controller/types.js'; export type PlatformDeploymentThresholds = { onWindows: number; @@ -28,8 +29,10 @@ export type PlatformDeploymentThresholds = { * Keeps test project update info. */ export type TestProjectUpdate = { - sourceFile: URL; - projectFile: URL; + /** + * An array of source and destination objects. All replacements will be part of the update operation + */ + replacements: CopyDefinition[]; /** * Define a threshold for the hotswap deployment time * Windows has a separate threshold because it is consistently slower than other platforms diff --git a/packages/integration-tests/src/test-project-setup/test_project_creator.ts b/packages/integration-tests/src/test-project-setup/test_project_creator.ts index 73f8ad7e48..cdf7cf5ba3 100644 --- a/packages/integration-tests/src/test-project-setup/test_project_creator.ts +++ b/packages/integration-tests/src/test-project-setup/test_project_creator.ts @@ -8,6 +8,7 @@ import { DeployedResourcesFinder } from '../find_deployed_resource.js'; import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; import { CustomOutputsTestProjectCreator } from './custom_outputs.js'; import { S3Client } from '@aws-sdk/client-s3'; +import { IAMClient } from '@aws-sdk/client-iam'; export type TestProjectCreator = { readonly name: string; @@ -23,6 +24,7 @@ export const getTestProjectCreators = (): TestProjectCreator[] => { const cfnClient = new CloudFormationClient(e2eToolingClientConfig); const lambdaClient = new LambdaClient(e2eToolingClientConfig); const s3Client = new S3Client(e2eToolingClientConfig); + const iamClient = new IAMClient(e2eToolingClientConfig); const resourceFinder = new DeployedResourcesFinder(cfnClient); const secretClient = getSecretClient(e2eToolingClientConfig); testProjectCreators.push( @@ -31,6 +33,7 @@ export const getTestProjectCreators = (): TestProjectCreator[] => { secretClient, lambdaClient, s3Client, + iamClient, resourceFinder ), new MinimalWithTypescriptIdiomTestProjectCreator(cfnClient), diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/.amplify/function-env/defaultNodeFunction.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/.amplify/function-env/defaultNodeFunction.ts new file mode 100644 index 0000000000..a077cd1963 --- /dev/null +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/.amplify/function-env/defaultNodeFunction.ts @@ -0,0 +1,89 @@ +/** + * This file is here to make Typescript happy for initial type checking and will be overwritten when tests run + */ +export const env = process.env as LambdaProvidedEnvVars & AmplifyBackendEnvVars; + +/** Lambda runtime environment variables, see https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime */ +type LambdaProvidedEnvVars = { + /** The handler location configured on the function. */ + _HANDLER: string; + + /** The X-Ray tracing header. This environment variable changes with each invocation. */ + _X_AMZN_TRACE_ID: string; + + /** The default AWS Region where the Lambda function is executed. */ + AWS_DEFAULT_REGION: string; + + /** The AWS Region where the Lambda function is executed. If defined, this value overrides the AWS_DEFAULT_REGION. */ + AWS_REGION: string; + + /** The runtime identifier, prefixed by AWS_Lambda_ (for example, AWS_Lambda_java8). */ + AWS_EXECUTION_ENV: string; + + /** The name of the function. */ + AWS_LAMBDA_FUNCTION_NAME: string; + + /** The amount of memory available to the function in MB. */ + AWS_LAMBDA_FUNCTION_MEMORY_SIZE: string; + + /** The version of the function being executed. */ + AWS_LAMBDA_FUNCTION_VERSION: string; + + /** The initialization type of the function, which is on-demand, provisioned-concurrency, or snap-start. */ + AWS_LAMBDA_INITIALIZATION_TYPE: string; + + /** The name of the Amazon CloudWatch Logs group for the function. */ + AWS_LAMBDA_LOG_GROUP_NAME: string; + + /** The name of the Amazon CloudWatch Logs stream for the function. */ + AWS_LAMBDA_LOG_STREAM_NAME: string; + + /** AWS access key. */ + AWS_ACCESS_KEY: string; + + /** AWS access key ID. */ + AWS_ACCESS_KEY_ID: string; + + /** AWS secret access key. */ + AWS_SECRET_ACCESS_KEY: string; + + /** AWS Session token. */ + AWS_SESSION_TOKEN: string; + + /** The host and port of the runtime API. */ + AWS_LAMBDA_RUNTIME_API: string; + + /** The path to your Lambda function code. */ + LAMBDA_TASK_ROOT: string; + + /** The path to runtime libraries. */ + LAMBDA_RUNTIME_DIR: string; + + /** The locale of the runtime. */ + LANG: string; + + /** The execution path. */ + PATH: string; + + /** The system library path. */ + LD_LIBRARY_PATH: string; + + /** The Node.js library path. */ + NODE_PATH: string; + + /** For X-Ray tracing, Lambda sets this to LOG_ERROR to avoid throwing runtime errors from the X-Ray SDK. */ + AWS_XRAY_CONTEXT_MISSING: string; + + /** For X-Ray tracing, the IP address and port of the X-Ray daemon. */ + AWS_XRAY_DAEMON_ADDRESS: string; + + /** The environment's time zone. */ + TZ: string; +}; + +/** Amplify backend environment variables available at runtime, this includes environment variables defined in `defineFunction` and by cross resource mechanisms */ +type AmplifyBackendEnvVars = { + TEST_SECRET: string; + TEST_SHARED_SECRET: string; + testName_BUCKET_NAME: string; +}; diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/data/echo/handler2.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/data/echo/handler2.ts new file mode 100644 index 0000000000..a74409aa20 --- /dev/null +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/data/echo/handler2.ts @@ -0,0 +1,6 @@ +/** + * Hello world lambda used for testing + */ +export const handler = async () => { + return 'hello world lambda'; +}; diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/data/js_custom_fn.js b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/data/js_custom_fn.js new file mode 100644 index 0000000000..4491ae45ee --- /dev/null +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/data/js_custom_fn.js @@ -0,0 +1,15 @@ +import * as ddb from '@aws-appsync/utils/dynamodb'; + +/** + * JS resolver used by e2e test + */ +export const request = (ctx) => { + return ddb.get({ key: { id: ctx.args.id } }); +}; +/** + * JS resolver used by e2e test + */ +export const response = (ctx) => ({ + ...ctx.result, + content: 'overwritten by JS Resolver', +}); diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/data/resource.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/data/resource.ts index bfef3eb862..35c8039b53 100644 --- a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/data/resource.ts +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/data/resource.ts @@ -20,14 +20,43 @@ const schema = a.schema({ executionDuration: a.float(), }), + customQuery: a + .query() + .arguments({ id: a.string() }) + .returns(a.ref('Todo')) + .authorization([a.allow.private()]) + .handler( + // provisions JS resolver + a.handler.custom({ + dataSource: a.ref('Todo'), + entry: './js_custom_fn.js', + }) + ), + echo: a .query() .arguments({ content: a.string() }) .returns(a.ref('EchoResponse')) .authorization([a.allow.private()]) - .function('echo'), + .handler(a.handler.function('echo')), + + echoInline: a + .query() + .arguments({ content: a.string() }) + .returns(a.ref('EchoResponse')) + .authorization([a.allow.private()]) + .handler( + a.handler.function( + defineFunction({ + entry: './echo/handler2.ts', + }) + ) + ), }) as never; // Not 100% sure why TS is complaining here. The error I'm getting is "The inferred type of 'schema' references an inaccessible 'unique symbol' type. A type annotation is necessary." +// ^ appears to be caused by these 2 rules in tsconfig.base.json: https://github.com/aws-amplify/amplify-backend/blob/8d9a7a4c3033c474b0fc78379cdd4c1854d890ce/tsconfig.base.json#L7-L8 +// Possibly something to do with all the `references` in the nested configs. Using the same tsconfig in a new amplify app doesn't cause the error. + export type Schema = ClientSchema; export const data = defineData({ diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/func-src/response_generator.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/func-src/response_generator.ts index 05a137a105..e573e415d5 100644 --- a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/func-src/response_generator.ts +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/func-src/response_generator.ts @@ -1,13 +1,18 @@ import { Amplify } from 'aws-amplify'; import { downloadData, uploadData } from 'aws-amplify/storage'; +/** + * This import is for tests to use the generated type generation file. + * Currently we only use defaultNodeFunction because node16Function has the same environment variables at runtime. + */ +import { env } from '@env/defaultNodeFunction.js'; // Configure the Amplify client with the storage and auth loaded from the lambda execution role Amplify.configure( { Storage: { S3: { - bucket: process.env.testName_BUCKET_NAME, - region: process.env.AWS_REGION, + bucket: env.testName_BUCKET_NAME, + region: env.AWS_REGION, }, }, }, @@ -16,12 +21,9 @@ Amplify.configure( credentialsProvider: { getCredentialsAndIdentityId: async () => ({ credentials: { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - accessKeyId: process.env.AWS_ACCESS_KEY_ID!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - sessionToken: process.env.AWS_SESSION_TOKEN!, + accessKeyId: env.AWS_ACCESS_KEY_ID, + secretAccessKey: env.AWS_SECRET_ACCESS_KEY, + sessionToken: env.AWS_SESSION_TOKEN, }, // this can be anything identityId: '1234567890', @@ -40,8 +42,8 @@ Amplify.configure( export const getResponse = async () => { return { s3TestContent: await s3RoundTrip(), - testSecret: process.env.TEST_SECRET, - testSharedSecret: process.env.TEST_SHARED_SECRET, + testSecret: env.TEST_SECRET, + testSharedSecret: env.TEST_SHARED_SECRET, }; }; @@ -51,17 +53,35 @@ export const getResponse = async () => { */ const s3RoundTrip = async (): Promise => { const filename = 'test.txt'; - await uploadData({ - key: filename, - data: 'this is some test content', - options: { - accessLevel: 'guest', - }, - }).result; + await retry( + () => + uploadData({ + key: filename, + data: 'this is some test content', + options: { + accessLevel: 'guest', + }, + }).result + ); - const downloadResult = await downloadData({ - key: filename, - options: { accessLevel: 'guest' }, - }).result; + const downloadResult = await retry( + () => + downloadData({ + key: filename, + options: { accessLevel: 'guest' }, + }).result + ); return downloadResult.body.text() as Promise; }; + +// executes action and if it throws, executes the action again after a second +// if the action fails a second time, the error is re-thrown +const retry = async (action: () => Promise) => { + try { + return action(); + } catch (err) { + console.log(err); + await new Promise((resolve) => setTimeout(resolve, 1000)); + return action(); + } +}; diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/README.md b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/README.md new file mode 100644 index 0000000000..e1dc0f7a0d --- /dev/null +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/README.md @@ -0,0 +1,2 @@ +This directory contains files that are copied to test project destinations during sandbox to exercise hotswap behavior. +See the `getUpdates()` method definition in `data_storage_auth_with_triggers.ts` for more detail on how the copy behavior is orchestrated. diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/update-1/data/resource.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/data/resource.ts similarity index 69% rename from packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/update-1/data/resource.ts rename to packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/data/resource.ts index 4bd805c2b0..4ff22e3255 100644 --- a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/update-1/data/resource.ts +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/data/resource.ts @@ -20,12 +20,38 @@ const schema = a.schema({ executionDuration: a.float(), }), + customQuery: a + .query() + .arguments({ id: a.string() }) + .returns(a.ref('Todo')) + .authorization([a.allow.private()]) + .handler( + // provisions JS resolver + a.handler.custom({ + dataSource: a.ref('Todo'), + entry: './js_custom_fn.js', + }) + ), + echo: a .query() .arguments({ content: a.string() }) .returns(a.ref('EchoResponse')) .authorization([a.allow.private()]) - .function('echo'), + .handler(a.handler.function('echo')), + + echoInline: a + .query() + .arguments({ content: a.string() }) + .returns(a.ref('EchoResponse')) + .authorization([a.allow.private()]) + .handler( + a.handler.function( + defineFunction({ + entry: './echo/handler2.ts', + }) + ) + ), }) as never; // Not 100% sure why TS is complaining here. The error I'm getting is "The inferred type of 'schema' references an inaccessible 'unique symbol' type. A type annotation is necessary." export type Schema = ClientSchema; diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/func-src/handler.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/func-src/handler.ts new file mode 100644 index 0000000000..1419106b33 --- /dev/null +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/func-src/handler.ts @@ -0,0 +1,9 @@ +// we have to use ts-ignore instead of ts-expect-error because when the tsc check as part of the deployment runs, there will no longer be an error +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore Ignoring TS here because this code will be hotswapped in for the original handler code. The destination location contains the response_generator dependency +import { getResponse } from './response_generator.js'; + +/** + * Non-functional change to the lambda but it triggers a sandbox hotswap + */ +export const handler = () => getResponse(); diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/function.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/function.ts new file mode 100644 index 0000000000..6032d8c60f --- /dev/null +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/function.ts @@ -0,0 +1,38 @@ +import { defineFunction, secret } from '@aws-amplify/backend'; +import { amplifySharedSecretNameKey } from '../../../shared_secret.js'; + +export const defaultNodeFunc = defineFunction({ + name: 'defaultNodeFunction', + entry: './func-src/handler.ts', + environment: { + TEST_SECRET: secret('amazonSecret'), + TEST_SHARED_SECRET: secret( + process.env[amplifySharedSecretNameKey] as string + ), + // adding another env var to check that function config updates can be hotswapped + NEW_ENV_VAR: 'someValue', + }, + timeoutSeconds: 5, +}); + +export const node16Func = defineFunction({ + name: 'node16Function', + entry: './func-src/handler_node16.ts', + environment: { + TEST_SECRET: secret('amazonSecret'), + TEST_SHARED_SECRET: secret( + process.env[amplifySharedSecretNameKey] as string + ), + }, + timeoutSeconds: 5, + runtime: 16, +}); + +export const onDelete = defineFunction({ + name: 'onDelete', + entry: './func-src/handler.ts', +}); +export const onUpload = defineFunction({ + name: 'onUpload', + entry: './func-src/handler.ts', +}); diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/tsconfig.json b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/tsconfig.json new file mode 100644 index 0000000000..3aca228e27 --- /dev/null +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "paths": { + "@env/*": ["./.amplify/function-env/*"] + } + }, + "include": ["../../**/*", ".amplify/**/*"] +} diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/update-1/function.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/update-1/function.ts deleted file mode 100644 index 8edb64116a..0000000000 --- a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/update-1/function.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { defineFunction } from '@aws-amplify/backend'; - -export const defaultNodeFunc = defineFunction({ - entry: './func-src/handler.ts', -}); diff --git a/packages/integration-tests/tsconfig.json b/packages/integration-tests/tsconfig.json index 41674022b2..2a92240ab1 100644 --- a/packages/integration-tests/tsconfig.json +++ b/packages/integration-tests/tsconfig.json @@ -6,7 +6,8 @@ { "path": "../backend" }, { "path": "../backend-secret" }, { "path": "../client-config" }, - { "path": "../platform-core" } + { "path": "../platform-core" }, + { "path": "./src/test-projects/data-storage-auth-with-triggers-ts" } ], "exclude": ["**/node_modules", "**/lib", "src/e2e-tests"] } diff --git a/packages/model-generator/API.md b/packages/model-generator/API.md index 4a5566911a..e98d6403cf 100644 --- a/packages/model-generator/API.md +++ b/packages/model-generator/API.md @@ -15,7 +15,7 @@ export const createGraphqlDocumentGenerator: ({ backendIdentifier, credentialPro // @public (undocumented) export type DocumentGenerationParameters = { - language: TargetLanguage; + targetFormat: StatementsTarget; maxDepth?: number; typenameIntrospection?: boolean; relativeTypesPath?: string; @@ -166,9 +166,6 @@ export type ModelsGenerationParameters = { handleListNullabilityTransparently?: boolean; }; -// @public (undocumented) -export type TargetLanguage = StatementsTarget; - // @public (undocumented) export type TypesGenerationParameters = { target: TypesTarget; diff --git a/packages/model-generator/CHANGELOG.md b/packages/model-generator/CHANGELOG.md index 95ec6e9dd1..839b7073cc 100644 --- a/packages/model-generator/CHANGELOG.md +++ b/packages/model-generator/CHANGELOG.md @@ -1,5 +1,16 @@ # @aws-amplify/model-generator +## 0.5.0-beta.3 + +### Minor Changes + +- 05c3c9b: Rename target format type and prop in model gen package + +### Patch Changes + +- Updated dependencies [b931980] + - @aws-amplify/deployed-backend-client@0.4.0-beta.3 + ## 0.4.1-beta.2 ### Patch Changes diff --git a/packages/model-generator/package.json b/packages/model-generator/package.json index 55f711da80..acc5e8f5d3 100644 --- a/packages/model-generator/package.json +++ b/packages/model-generator/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/model-generator", - "version": "0.4.1-beta.2", + "version": "0.5.0-beta.3", "type": "module", "publishConfig": { "access": "public" @@ -19,7 +19,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", - "@aws-amplify/deployed-backend-client": "^0.4.0-beta.2", + "@aws-amplify/deployed-backend-client": "^0.4.0-beta.3", "@aws-amplify/graphql-generator": "^0.2.4", "@aws-amplify/graphql-types-generator": "^3.4.4", "@aws-sdk/client-appsync": "^3.465.0", diff --git a/packages/model-generator/src/generate_api_code.test.ts b/packages/model-generator/src/generate_api_code.test.ts index 2a1bc7b0a3..3300adc6cc 100644 --- a/packages/model-generator/src/generate_api_code.test.ts +++ b/packages/model-generator/src/generate_api_code.test.ts @@ -65,7 +65,7 @@ void describe('generateAPICode', () => { assert.deepEqual( (generateModels.mock.calls[0].arguments as unknown[])[0], { - language: 'typescript', + targetFormat: 'typescript', maxDepth: undefined, typenameIntrospection: undefined, } @@ -112,7 +112,7 @@ void describe('generateAPICode', () => { assert.deepEqual( (generateModels.mock.calls[0].arguments as unknown[])[0], { - language: 'typescript', + targetFormat: 'typescript', maxDepth: 3, typenameIntrospection: false, } @@ -158,7 +158,7 @@ void describe('generateAPICode', () => { assert.deepEqual( (generateModels.mock.calls[0].arguments as unknown[])[0], { - language: 'typescript', + targetFormat: 'typescript', maxDepth: undefined, relativeTypesPath: './API', typenameIntrospection: undefined, diff --git a/packages/model-generator/src/generate_api_code.ts b/packages/model-generator/src/generate_api_code.ts index 1e17da2700..f747d90e81 100644 --- a/packages/model-generator/src/generate_api_code.ts +++ b/packages/model-generator/src/generate_api_code.ts @@ -145,7 +145,7 @@ export class ApiCodeGenerator { props: GenerateGraphqlCodegenOptions ): Promise { const generateModelsParams: DocumentGenerationParameters = { - language: props.statementTarget, + targetFormat: props.statementTarget, maxDepth: props.maxDepth, typenameIntrospection: props.typeNameIntrospection, }; diff --git a/packages/model-generator/src/graphql_document_generator.test.ts b/packages/model-generator/src/graphql_document_generator.test.ts index f10a184e11..83b57cdd0a 100644 --- a/packages/model-generator/src/graphql_document_generator.test.ts +++ b/packages/model-generator/src/graphql_document_generator.test.ts @@ -12,7 +12,7 @@ void describe('client generator', () => { }) ); await assert.rejects(() => - generator.generateModels({ language: 'typescript' }) + generator.generateModels({ targetFormat: 'typescript' }) ); }); }); diff --git a/packages/model-generator/src/graphql_document_generator.ts b/packages/model-generator/src/graphql_document_generator.ts index af3c09545c..82ee5c33c0 100644 --- a/packages/model-generator/src/graphql_document_generator.ts +++ b/packages/model-generator/src/graphql_document_generator.ts @@ -19,7 +19,7 @@ export class AppSyncGraphqlDocumentGenerator private resultBuilder: (fileMap: Record) => GenerationResult ) {} generateModels = async ({ - language, + targetFormat, maxDepth, typenameIntrospection, relativeTypesPath, @@ -32,7 +32,7 @@ export class AppSyncGraphqlDocumentGenerator const generatedStatements = generateStatements({ schema, - target: language, + target: targetFormat, maxDepth, typenameIntrospection, relativeTypesPath, diff --git a/packages/model-generator/src/model_generator.ts b/packages/model-generator/src/model_generator.ts index fcf970b1c7..72a666e6db 100644 --- a/packages/model-generator/src/model_generator.ts +++ b/packages/model-generator/src/model_generator.ts @@ -3,10 +3,9 @@ import { StatementsTarget, TypesTarget, } from '@aws-amplify/graphql-generator'; -export type TargetLanguage = StatementsTarget; export type DocumentGenerationParameters = { - language: TargetLanguage; + targetFormat: StatementsTarget; maxDepth?: number; typenameIntrospection?: boolean; relativeTypesPath?: string; diff --git a/packages/platform-core/API.md b/packages/platform-core/API.md index ca3bacbf1c..4d918aca5e 100644 --- a/packages/platform-core/API.md +++ b/packages/platform-core/API.md @@ -54,9 +54,14 @@ export class AmplifyFault extends AmplifyError { // @public export class AmplifyUserError extends AmplifyError { - constructor(name: T, options: AmplifyErrorOptions, cause?: Error); + constructor(name: T, options: AmplifyUserErrorOptions, cause?: Error); } +// @public +export type AmplifyUserErrorOptions = Omit & { + resolution: string; +}; + // @public export class BackendIdentifierConversions { static fromStackName(stackName?: string): BackendIdentifier | undefined; diff --git a/packages/platform-core/CHANGELOG.md b/packages/platform-core/CHANGELOG.md index a77d5651cf..f83136c546 100644 --- a/packages/platform-core/CHANGELOG.md +++ b/packages/platform-core/CHANGELOG.md @@ -1,5 +1,11 @@ # @aws-amplify/platform-core +## 0.5.0-beta.2 + +### Minor Changes + +- 937086b: require "resolution" in AmplifyUserError options + ## 0.5.0-beta.1 ### Minor Changes diff --git a/packages/platform-core/package.json b/packages/platform-core/package.json index 0dad5e9598..5fb042e78b 100644 --- a/packages/platform-core/package.json +++ b/packages/platform-core/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/platform-core", - "version": "0.5.0-beta.1", + "version": "0.5.0-beta.2", "type": "commonjs", "publishConfig": { "access": "public" diff --git a/packages/platform-core/src/errors/amplify_error.test.ts b/packages/platform-core/src/errors/amplify_error.test.ts index 27fcf8e897..26f2fb6cc8 100644 --- a/packages/platform-core/src/errors/amplify_error.test.ts +++ b/packages/platform-core/src/errors/amplify_error.test.ts @@ -7,9 +7,14 @@ void describe('amplify error', () => { void it('serialize and deserialize correctly', () => { const testError = new AmplifyUserError( 'SyntaxError', - { message: 'test error message', details: 'test error details' }, + { + message: 'test error message', + details: 'test error details', + resolution: 'test resolution', + }, new AmplifyUserError('AccessDeniedError', { message: 'some downstream error message', + resolution: 'test resolution', }) ); const sampleStderr = `some random stderr diff --git a/packages/platform-core/src/errors/amplify_error.ts b/packages/platform-core/src/errors/amplify_error.ts index 57f1a80041..e286caffb3 100644 --- a/packages/platform-core/src/errors/amplify_error.ts +++ b/packages/platform-core/src/errors/amplify_error.ts @@ -107,3 +107,11 @@ export type AmplifyErrorOptions = { // CloudFormation or NodeJS error codes code?: string; }; + +/** + * Same as AmplifyErrorOptions except resolution is required + */ +export type AmplifyUserErrorOptions = Omit< + AmplifyErrorOptions, + 'resolution' +> & { resolution: string }; diff --git a/packages/platform-core/src/errors/amplify_user_error.ts b/packages/platform-core/src/errors/amplify_user_error.ts index 296cacfcd4..eb45fc1ac5 100644 --- a/packages/platform-core/src/errors/amplify_user_error.ts +++ b/packages/platform-core/src/errors/amplify_user_error.ts @@ -1,4 +1,4 @@ -import { AmplifyError, AmplifyErrorOptions } from './amplify_error'; +import { AmplifyError, AmplifyUserErrorOptions } from './amplify_error'; /** * Base class for all Amplify user errors @@ -19,7 +19,7 @@ export class AmplifyUserError< * throw new AmplifyError(...,...,error); * } */ - constructor(name: T, options: AmplifyErrorOptions, cause?: Error) { + constructor(name: T, options: AmplifyUserErrorOptions, cause?: Error) { super(name, 'ERROR', options, cause); } } diff --git a/packages/platform-core/src/usage-data/usage_data_emitter.test.ts b/packages/platform-core/src/usage-data/usage_data_emitter.test.ts index 2353a1ce99..0054232702 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter.test.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter.test.ts @@ -83,6 +83,7 @@ void describe('UsageDataEmitter', () => { 'BackendBuildError', { message: 'some error message', + resolution: 'test resolution', }, new Error('some downstream exception') ); diff --git a/packages/plugin-types/API.md b/packages/plugin-types/API.md index 3f08dd5e01..44ade899ca 100644 --- a/packages/plugin-types/API.md +++ b/packages/plugin-types/API.md @@ -182,8 +182,8 @@ export type ResourceAccessAcceptor = { }; // @public (undocumented) -export type ResourceAccessAcceptorFactory = { - getResourceAccessAcceptor: (...roleName: RoleName extends string ? [RoleName] : []) => ResourceAccessAcceptor; +export type ResourceAccessAcceptorFactory = { + getResourceAccessAcceptor: (...roleIdentifier: RoleIdentifier extends string ? [RoleIdentifier] : []) => ResourceAccessAcceptor; }; // @public diff --git a/packages/plugin-types/src/resource_access_acceptor.ts b/packages/plugin-types/src/resource_access_acceptor.ts index b5293cc1b4..3c124b482f 100644 --- a/packages/plugin-types/src/resource_access_acceptor.ts +++ b/packages/plugin-types/src/resource_access_acceptor.ts @@ -23,14 +23,14 @@ export type ResourceAccessAcceptor = { }; export type ResourceAccessAcceptorFactory< - RoleName extends string | undefined = undefined + RoleIdentifier extends string | undefined = undefined > = { /** - * This type is a little wonky but basically it's saying that if RoleName is undefined, then this is a function with no props - * And if RoleName is a string then this is a function with a single roleName prop + * This type is a little wonky but basically it's saying that if RoleIdentifier is undefined, then this is a function with no props + * And if RoleIdentifier is a string then this is a function with a single roleIdentifier prop * See https://github.com/Microsoft/TypeScript/pull/24897 */ getResourceAccessAcceptor: ( - ...roleName: RoleName extends string ? [RoleName] : [] + ...roleIdentifier: RoleIdentifier extends string ? [RoleIdentifier] : [] ) => ResourceAccessAcceptor; }; diff --git a/packages/sandbox/CHANGELOG.md b/packages/sandbox/CHANGELOG.md index 58be13e643..1dda89802c 100644 --- a/packages/sandbox/CHANGELOG.md +++ b/packages/sandbox/CHANGELOG.md @@ -1,5 +1,40 @@ # @aws-amplify/sandbox +## 0.5.2-beta.5 + +### Patch Changes + +- 3e34244: use `format` to replace `color` and remove `color`. +- 937086b: require "resolution" in AmplifyUserError options +- Updated dependencies [3e34244] +- Updated dependencies [ee247fd] +- Updated dependencies [937086b] +- Updated dependencies [b931980] + - @aws-amplify/cli-core@0.5.0-beta.2 + - @aws-amplify/backend-deployer@0.5.1-beta.2 + - @aws-amplify/platform-core@0.5.0-beta.2 + - @aws-amplify/deployed-backend-client@0.4.0-beta.3 + - @aws-amplify/client-config@0.9.0-beta.4 + - @aws-amplify/backend-secret@0.4.5-beta.2 + +## 0.5.2-beta.4 + +### Patch Changes + +- 615a3e6: upgrade @parcel/watcher wo use the latest version + +## 0.5.2-beta.3 + +### Patch Changes + +- Updated dependencies [3998cd3] +- Updated dependencies [79cff6d] +- Updated dependencies [8d9a7a4] +- Updated dependencies [b0ba24d] +- Updated dependencies [8d9a7a4] + - @aws-amplify/cli-core@0.5.0-beta.1 + - @aws-amplify/client-config@0.9.0-beta.3 + ## 0.5.2-beta.2 ### Patch Changes diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json index 3b4f42bb79..054a3e4596 100644 --- a/packages/sandbox/package.json +++ b/packages/sandbox/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/sandbox", - "version": "0.5.2-beta.2", + "version": "0.5.2-beta.5", "type": "module", "publishConfig": { "access": "public" @@ -18,16 +18,16 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-deployer": "^0.5.1-beta.1", - "@aws-amplify/backend-secret": "^0.4.5-beta.1", - "@aws-amplify/cli-core": "^0.4.1-beta.0", - "@aws-amplify/client-config": "^0.8.1-beta.2", - "@aws-amplify/deployed-backend-client": "^0.4.0-beta.2", - "@aws-amplify/platform-core": "^0.5.0-beta.1", + "@aws-amplify/backend-deployer": "^0.5.1-beta.2", + "@aws-amplify/backend-secret": "^0.4.5-beta.2", + "@aws-amplify/cli-core": "^0.5.0-beta.2", + "@aws-amplify/client-config": "^0.9.0-beta.4", + "@aws-amplify/deployed-backend-client": "^0.4.0-beta.3", + "@aws-amplify/platform-core": "^0.5.0-beta.2", + "@aws-sdk/client-cloudformation": "^3.465.0", "@aws-sdk/credential-providers": "^3.465.0", "@aws-sdk/types": "^3.465.0", - "@aws-sdk/client-cloudformation": "^3.465.0", - "@parcel/watcher": "2.1.0", + "@parcel/watcher": "^2.4.1", "debounce-promise": "^3.1.2", "glob": "^10.2.7", "open": "^9.1.0", diff --git a/packages/sandbox/src/file_watching_sandbox.test.ts b/packages/sandbox/src/file_watching_sandbox.test.ts index 30985b44a0..5c3bb2d12e 100644 --- a/packages/sandbox/src/file_watching_sandbox.test.ts +++ b/packages/sandbox/src/file_watching_sandbox.test.ts @@ -532,6 +532,7 @@ void describe('Sandbox using local project name resolver', () => { Promise.reject( new AmplifyUserError('CFNUpdateNotSupportedError', { message: 'some error message', + resolution: 'test resolution', }) ), { times: 1 } //mock implementation once @@ -574,6 +575,7 @@ void describe('Sandbox using local project name resolver', () => { Promise.reject( new AmplifyUserError('CFNUpdateNotSupportedError', { message: 'some error message', + resolution: 'test resolution', }) ), { times: 1 } //mock implementation once diff --git a/packages/sandbox/src/file_watching_sandbox.ts b/packages/sandbox/src/file_watching_sandbox.ts index 68a3cf31b2..f2edd02b14 100644 --- a/packages/sandbox/src/file_watching_sandbox.ts +++ b/packages/sandbox/src/file_watching_sandbox.ts @@ -21,9 +21,9 @@ import { } from '@aws-sdk/client-cloudformation'; import { AmplifyPrompter, - COLOR, LogLevel, Printer, + format, } from '@aws-amplify/cli-core'; import { FilesChangesTracker, @@ -221,7 +221,7 @@ export class FileWatchingSandbox extends EventEmitter implements Sandbox { this.emit('successfulDeployment', deployResult); } catch (error) { // Print a meaningful message - this.printer.print(this.getErrorMessage(error), COLOR.RED); + this.printer.print(format.error(this.getErrorMessage(error))); this.emit('failedDeployment', error); // If the error is because of a non-allowed destructive change such as @@ -335,8 +335,9 @@ export class FileWatchingSandbox extends EventEmitter implements Sandbox { options: SandboxOptions ) => { this.printer.print( - '[Sandbox] We cannot deploy your new changes. You can either revert them or recreate your sandbox with the new changes (deleting all user data)', - COLOR.RED + format.error( + '[Sandbox] We cannot deploy your new changes. You can either revert them or recreate your sandbox with the new changes (deleting all user data)' + ) ); // offer to recreate the sandbox with new properties const answer = await AmplifyPrompter.yesOrNo({ diff --git a/scripts/update_tsconfig_refs.ts b/scripts/update_tsconfig_refs.ts index d29f5e6e15..c9c168e3db 100644 --- a/scripts/update_tsconfig_refs.ts +++ b/scripts/update_tsconfig_refs.ts @@ -24,6 +24,22 @@ type PackageInfo = { const packagePaths = globSync('./packages/*'); +type TsconfigReference = { + path: string; +}; + +// Additional references for specific packages that are not package dependencies +const additionalRefs: Record = { + '@aws-amplify/integration-tests': [ + // Added to allow tsc to work with nested tsconfig + { path: './src/test-projects/data-storage-auth-with-triggers-ts' }, + ], + '@aws-amplify/backend-data': [ + // Added to allow tsc to work with nested tsconfig - prevents generating inline sourcemaps in an asset we deploy for customers + { path: './src/assets' }, + ], +}; + // First collect information about all the packages in the repo const repoPackagesInfoRecord: Record = {}; @@ -61,16 +77,23 @@ const updatePromises = Object.values(repoPackagesInfoRecord).map( ]) ); + // collect any additional references to add for the package + const additionalRefsToAdd = + additionalRefs[packageJson.name as string] ?? []; + // construct the references array in tsconfig for inter-repo dependencies - tsconfig.references = allDeps - .filter((dep) => dep in repoPackagesInfoRecord) - .reduce( - (accumulator: unknown[], dep) => - accumulator.concat({ - path: repoPackagesInfoRecord[dep].relativeReferencePath, - }), - [] - ); + tsconfig.references = [ + ...allDeps + .filter((dep) => dep in repoPackagesInfoRecord) + .reduce( + (accumulator: unknown[], dep) => + accumulator.concat({ + path: repoPackagesInfoRecord[dep].relativeReferencePath, + }), + [] + ), + ...additionalRefsToAdd, + ]; // write out the tsconfig file using prettier formatting const prettierConfig = await prettier.resolveConfig(tsconfigPath);