From 8d9a7a4c3033c474b0fc78379cdd4c1854d890ce Mon Sep 17 00:00:00 2001 From: MJ Zhang <0618@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:26:01 -0800 Subject: [PATCH 01/41] feat(cli-core): throw error if detect using pnpm on Windows (#1072) * feat: add error message for pnpm on windows * PoC: test pnpm on windows * exclude pnpm on windows * include pnpm on windows * add e2e test for pnpm win32 * fix package manager controller factory test * add unit test for pnpm on Windows * update API.md * add changeset * remove temporary action trigger * ran prettier * change verbiage and update README * update sanity_check test message * use AmplifyUserError * refactor pnpm windows e2e test * run prettier * Update packages/cli-core/src/package-manager-controller/package_manager_controller_factory.ts Co-authored-by: Kamil Sobol * Update packages/integration-tests/src/package_manager_sanity_checks.test.ts Co-authored-by: Edward Foyle * use error detail * fix unit test --------- Co-authored-by: Kamil Sobol Co-authored-by: Edward Foyle --- .changeset/new-kings-beg.md | 5 +++++ .changeset/rude-toys-visit.md | 5 +++++ .github/workflows/health_checks.yml | 3 --- package-lock.json | 1 + packages/cli-core/API.md | 2 +- packages/cli-core/package.json | 1 + ...package_manager_controller_factory.test.ts | 21 ++++++++++++++++-- .../package_manager_controller_factory.ts | 13 ++++++++++- packages/cli-core/tsconfig.json | 2 +- packages/create-amplify/README.md | 8 +++++++ .../src/package_manager_sanity_checks.test.ts | 22 +++++++++++++++++++ 11 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 .changeset/new-kings-beg.md create mode 100644 .changeset/rude-toys-visit.md 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/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/.github/workflows/health_checks.yml b/.github/workflows/health_checks.yml index 33300bb895..c30790dfb0 100644 --- a/.github/workflows/health_checks.yml +++ b/.github/workflows/health_checks.yml @@ -173,9 +173,6 @@ jobs: os: [ubuntu-latest, macos-latest, amplify-backend_windows-latest_8-core] 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 }} diff --git a/package-lock.json b/package-lock.json index 5717e2a65b..7784eeaf74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23526,6 +23526,7 @@ "version": "0.4.1-beta.0", "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" diff --git a/packages/cli-core/API.md b/packages/cli-core/API.md index 30168c0331..96cd39ce63 100644 --- a/packages/cli-core/API.md +++ b/packages/cli-core/API.md @@ -53,7 +53,7 @@ export enum LogLevel { // @public export class PackageManagerControllerFactory { - constructor(cwd: string, printer: Printer); + constructor(cwd: string, printer: Printer, platform?: NodeJS.Platform); getPackageManagerController(): PackageManagerController; } diff --git a/packages/cli-core/package.json b/packages/cli-core/package.json index 21ccd51d04..01060e151a 100644 --- a/packages/cli-core/package.json +++ b/packages/cli-core/package.json @@ -18,6 +18,7 @@ }, "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" 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..6418e90f48 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,15 @@ 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, + }); + } return new PnpmPackageManagerController(this.cwd); case 'yarn-classic': return new YarnClassicPackageManagerController(this.cwd); 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/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/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; + } + ); + } + }); }); From 5969a325612f8e300652c560b0e3f6c091a2a0c4 Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Wed, 28 Feb 2024 15:46:56 -0800 Subject: [PATCH 02/41] Storage access default to deny parent prefix actions on subpath (#1070) --- .changeset/great-timers-invent.md | 5 + .eslint_dictionary.json | 1 + packages/backend-storage/API.md | 26 +- .../backend-storage/src/access_builder.ts | 4 +- .../src/action_to_resources_map.ts | 72 -- .../src/storage_access_orchestrator.test.ts | 648 ++++++++++++++++++ .../src/storage_access_orchestrator.ts | 253 +++++++ .../src/storage_access_policy_arbiter.test.ts | 321 --------- .../src/storage_access_policy_arbiter.ts | 105 --- .../src/storage_access_policy_factory.test.ts | 294 +++++++- .../src/storage_access_policy_factory.ts | 36 +- .../storage_container_entry_generator.test.ts | 135 +--- .../src/storage_container_entry_generator.ts | 43 +- packages/backend-storage/src/types.ts | 18 +- .../src/validate_storage_access_paths.test.ts | 16 +- .../src/validate_storage_access_paths.ts | 59 +- 16 files changed, 1330 insertions(+), 706 deletions(-) create mode 100644 .changeset/great-timers-invent.md delete mode 100644 packages/backend-storage/src/action_to_resources_map.ts create mode 100644 packages/backend-storage/src/storage_access_orchestrator.test.ts create mode 100644 packages/backend-storage/src/storage_access_orchestrator.ts delete mode 100644 packages/backend-storage/src/storage_access_policy_arbiter.test.ts delete mode 100644 packages/backend-storage/src/storage_access_policy_arbiter.ts 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/.eslint_dictionary.json b/.eslint_dictionary.json index a05c850890..ffc7be9c91 100644 --- a/.eslint_dictionary.json +++ b/.eslint_dictionary.json @@ -126,6 +126,7 @@ "subcommand", "subcommands", "submodule", + "subpath", "syncable", "timestamps", "tmpdir", diff --git a/packages/backend-storage/API.md b/packages/backend-storage/API.md index dd358abc4f..2f351aaa3b 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,16 +34,11 @@ 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; -}; - -// @public (undocumented) export type StorageAccessBuilder = { - to: (actions: StorageAction[]) => StorageAccessDefinition; + authenticated: StorageActionBuilder; + guest: StorageActionBuilder; + owner: StorageActionBuilder; + resource: (other: ConstructFactory) => StorageActionBuilder; }; // @public (undocumented) @@ -56,12 +48,20 @@ export type StorageAccessDefinition = { ownerPlaceholderSubstitution: string; }; +// @public (undocumented) +export type StorageAccessGenerator = (allow: StorageAccessBuilder) => StorageAccessRecord; + // @public (undocumented) export type StorageAccessRecord = Record; // @public (undocumented) export type StorageAction = 'read' | 'write' | 'delete'; +// @public (undocumented) +export type StorageActionBuilder = { + to: (actions: StorageAction[]) => StorageAccessDefinition; +}; + // @public export type StoragePath = `/${string}/*`; diff --git a/packages/backend-storage/src/access_builder.ts b/packages/backend-storage/src/access_builder.ts index 0be1cb70ae..149b8da589 100644 --- a/packages/backend-storage/src/access_builder.ts +++ b/packages/backend-storage/src/access_builder.ts @@ -4,9 +4,9 @@ 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, 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/storage_access_orchestrator.test.ts b/packages/backend-storage/src/storage_access_orchestrator.test.ts new file mode 100644 index 0000000000..904e7a17e5 --- /dev/null +++ b/packages/backend-storage/src/storage_access_orchestrator.test.ts @@ -0,0 +1,648 @@ +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 { ownerPathPartToken } 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: ['read', 'write'], + getResourceAccessAcceptor: () => ({ + identifier: 'testResourceAccessAcceptor', + acceptResourceAccess: acceptResourceAccessMock, + }), + ownerPlaceholderSubstitution: '*', + }, + ], + }), + {} 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: ['read', 'write'], + getResourceAccessAcceptor: () => ({ + identifier: 'testResourceAccessAcceptor', + acceptResourceAccess: acceptResourceAccessMock, + }), + ownerPlaceholderSubstitution: '*', + }, + ], + }), + {} 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: ['read', 'write', 'delete'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub, + ownerPlaceholderSubstitution: '*', + }, + ], + '/another/prefix/*': [ + { + actions: ['read'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub, + ownerPlaceholderSubstitution: '*', + }, + ], + }), + {} 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: ['read', 'write', 'delete'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub1, + ownerPlaceholderSubstitution: '*', + }, + { + actions: ['read'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub2, + ownerPlaceholderSubstitution: '*', + }, + ], + '/another/prefix/*': [ + { + actions: ['read', 'delete'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub2, + ownerPlaceholderSubstitution: '*', + }, + ], + }), + {} 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/${ownerPathPartToken}/*`]: [ + { + actions: ['read', 'write'], + getResourceAccessAcceptor: () => ({ + identifier: 'testResourceAccessAcceptor', + acceptResourceAccess: acceptResourceAccessMock, + }), + ownerPlaceholderSubstitution: '{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: ['read', 'write'], + getResourceAccessAcceptor: () => ({ + identifier: 'resourceAccessAcceptor1', + acceptResourceAccess: acceptResourceAccessMock1, + }), + ownerPlaceholderSubstitution: '*', + }, + ], + '/foo/bar/*': [ + { + actions: ['read'], + getResourceAccessAcceptor: () => ({ + identifier: 'resourceAccessAcceptor2', + acceptResourceAccess: acceptResourceAccessMock2, + }), + ownerPlaceholderSubstitution: '*', + }, + ], + }), + {} 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/{owner}/*': [ + { + actions: ['write', 'delete'], + getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, + ownerPlaceholderSubstitution: '{ownerSub}', + }, + { + actions: ['read'], + getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, + ownerPlaceholderSubstitution: '*', + }, + ], + }), + {} 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/{ownerSub}/*`, + }, + { + Action: 's3:DeleteObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/foo/{ownerSub}/*`, + }, + { + 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: ['read', 'write'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub1, + ownerPlaceholderSubstitution: '*', + }, + ], + // acceptor1 should be denied read and write on this path + // acceptor2 should have only read on this path + '/foo/bar/*': [ + { + actions: ['read'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub2, + ownerPlaceholderSubstitution: '{ownerSub}', + }, + ], + // 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: ['read'], + ownerPlaceholderSubstitution: '*', + getResourceAccessAcceptor: getResourceAccessAcceptorStub1, + }, + ], + // acceptor 1 is denied write on this path (read still allowed) + // acceptor 2 has read/write/delete on path with ownerSub + '/other/{owner}/*': [ + { + actions: ['read', 'write', 'delete'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub2, + ownerPlaceholderSubstitution: '{ownerSub}', + }, + { + actions: ['read'], + getResourceAccessAcceptor: getResourceAccessAcceptorStub1, + ownerPlaceholderSubstitution: '*', + }, + ], + }), + {} 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/{ownerSub}/*`, + ], + }, + { + Action: 's3:PutObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/other/{ownerSub}/*`, + }, + { + Action: 's3:DeleteObject', + Effect: 'Allow', + Resource: `${bucket.bucketArn}/other/{ownerSub}/*`, + }, + ], + 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: ['read'], + getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, + ownerPlaceholderSubstitution: '*', + }, + { + actions: ['write'], + getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, + ownerPlaceholderSubstitution: '{ownerSub}', + }, + { + actions: ['delete'], + getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, + ownerPlaceholderSubstitution: '*', + }, + ], + }), + {} 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 + ); + }); + }); +}); + +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..c111bc422d --- /dev/null +++ b/packages/backend-storage/src/storage_access_orchestrator.ts @@ -0,0 +1,253 @@ +import { + ConstructFactoryGetInstanceProps, + ResourceAccessAcceptor, + SsmEnvironmentEntry, +} from '@aws-amplify/plugin-types'; +import { + StorageAccessBuilder, + StorageAccessGenerator, + StorageAction, + StoragePath, +} from './types.js'; +import { ownerPathPartToken } 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'; + +/* 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< + StorageAction, + { 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( + ownerPathPartToken, + permission.ownerPlaceholderSubstitution + ) as StoragePath; + + // set an entry that maps this permission to the resource acceptor + this.addAccessDefinition( + resourceAccessAcceptor, + permission.actions, + 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: StorageAction[], + 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..90463fd6a8 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([ + ['read', { 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([ + [ + 'read', + { + 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([ + ['read', { 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/*'])], + ['read', { 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,265 @@ void describe('StorageAccessPolicyFactory', () => { }, }); }); + + void it('handles deny on single action', () => { + const bucketPolicyFactory = new StorageAccessPolicyFactory(bucket); + const policy = bucketPolicyFactory.createPolicy( + new Map([ + ['read', { 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([ + [ + 'read', + { + 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([ + [ + 'read', + { + 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/*', + ], + ], + }, + ], + }, + ], + }, + }); + }); }); 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..1c77af10de 100644 --- a/packages/backend-storage/src/storage_access_policy_factory.ts +++ b/packages/backend-storage/src/storage_access_policy_factory.ts @@ -1,5 +1,5 @@ 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'; @@ -29,7 +29,10 @@ export class StorageAccessPolicyFactory { } createPolicy = ( - permissions: Readonly>>> + permissions: Map< + StorageAction, + { allow: Set; deny: Set } + > ) => { if (permissions.size === 0) { throw new AmplifyFault('EmptyPolicyFault', { @@ -39,19 +42,38 @@ 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 + action: StorageAction, + effect: Effect ) => new PolicyStatement({ + effect, actions: actionMap[action], resources: Array.from(s3Prefixes).map( (s3Prefix) => `${this.bucket.bucketArn}${s3Prefix}` 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..528fe2d85e 100644 --- a/packages/backend-storage/src/types.ts +++ b/packages/backend-storage/src/types.ts @@ -17,7 +17,7 @@ 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; }; /** @@ -26,20 +26,22 @@ export type AmplifyStorageFactoryProps = Omit< * 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; + owner: 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, 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..7bb9794cbc 100644 --- a/packages/backend-storage/src/validate_storage_access_paths.test.ts +++ b/packages/backend-storage/src/validate_storage_access_paths.test.ts @@ -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/*].`, @@ -74,11 +80,17 @@ void describe('validateStorageAccessPaths', () => { }); }); - 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/{owner}/*', '/foo/*']), { - message: `Wildcard conflict detected with an ${ownerPathPartToken} token.`, + message: `A path cannot be a prefix of another path that contains the ${ownerPathPartToken} token.`, + } + ); + assert.throws( + () => validateStorageAccessPaths(['/foo/bar/{owner}/*', '/foo/*']), + { + message: `A path cannot be a prefix of another path that contains the ${ownerPathPartToken} 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..032f772e67 100644 --- a/packages/backend-storage/src/validate_storage_access_paths.ts +++ b/packages/backend-storage/src/validate_storage_access_paths.ts @@ -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,15 +57,28 @@ 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 validateOwnerTokenRules = (path: string, otherPrefixes: string[]) => { + // if there's no owner token in the path, this validation is a noop + if (!path.includes(ownerPathPartToken)) { + return; + } + + if (otherPrefixes.length > 0) { + throw new AmplifyUserError('InvalidStorageAccessPathError', { + message: `A path cannot be a prefix of another path that contains the ${ownerPathPartToken} 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(ownerPathPartToken); if (ownerSplit.length > 2) { @@ -90,30 +110,19 @@ const validatePathWithOwnerToken = (path: string, allPaths: string[]) => { 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.`, - }); - } }; /** * 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('*', '')) + ); From cec91d5bc1fb94574050e6e2bfde8d87008baf58 Mon Sep 17 00:00:00 2001 From: Roshane Pascual Date: Wed, 28 Feb 2024 17:02:09 -0800 Subject: [PATCH 03/41] add dynamic environment variables to function type definition files (#1076) * add dynamic environment variables to function type definition files * add comments * rename method and move file generation to * group env vars into logical chunks * nit fixes * fix to get all defined env variables * move tsconfig options to test project dir * update update_tsconfig_refs script * rename DefinedEnvVars, nit fixes, and expanded on comments --- .changeset/popular-bobcats-provide.md | 6 ++ .gitignore | 2 +- packages/backend-function/src/factory.ts | 11 +-- .../src/function_env_translator.ts | 17 ++++ .../src/function_env_type_generator.test.ts | 37 +++++++- .../src/function_env_type_generator.ts | 48 +++++++--- .../create_empty_amplify_project.ts | 6 +- .../data_storage_auth_with_triggers.ts | 21 ++++- .../setup_dir_as_esm_module.ts | 16 ++++ .../function-env/defaultNodeFunction.ts | 87 +++++++++++++++++++ .../amplify/func-src/response_generator.ts | 20 +++-- .../tsconfig.json | 9 ++ packages/integration-tests/tsconfig.json | 3 +- scripts/update_tsconfig_refs.ts | 37 ++++++-- 14 files changed, 272 insertions(+), 48 deletions(-) create mode 100644 .changeset/popular-bobcats-provide.md create mode 100644 packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/.amplify/function-env/defaultNodeFunction.ts create mode 100644 packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/tsconfig.json 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/.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/packages/backend-function/src/factory.ts b/packages/backend-function/src/factory.ts index ebc7ae2597..b2821cb874 100644 --- a/packages/backend-function/src/factory.ts +++ b/packages/backend-function/src/factory.ts @@ -26,7 +26,6 @@ import { FunctionOutput, functionOutputKey, } from '@aws-amplify/backend-output-schemas'; -import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js'; /** * Entry point for defining a function in the Amplify ecosystem @@ -292,7 +291,7 @@ class AmplifyFunction this.functionEnvironmentTranslator = new FunctionEnvironmentTranslator( functionLambda, - props['environment'], + props.environment, backendSecretResolver ); @@ -301,14 +300,6 @@ 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 []; - }, - }); } getResourceAccessAcceptor = () => ({ diff --git a/packages/backend-function/src/function_env_translator.ts b/packages/backend-function/src/function_env_translator.ts index d43b683d01..4ffd458f7a 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,6 +17,9 @@ export class FunctionEnvironmentTranslator { private readonly ssmValuePlaceholderText = ''; + // List of environment variable names for typed shim generation + private readonly amplifyBackendEnvVarNames: string[] = []; + /** * Initialize translated environment variable records */ @@ -42,6 +46,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 +82,17 @@ 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[] => { + new FunctionEnvironmentTypeGenerator( + this.lambda.node.id, + this.amplifyBackendEnvVarNames + ).generateTypedProcessEnvShim(); + 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..cd24d64555 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 dynamic 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', ['TEST_ENV']); + const sampleStaticEnv = 'TEST_ENV: string;'; + + 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() @@ -38,10 +69,10 @@ void describe('FunctionEnvironmentTypeGenerator', () => { void it('generated type definition file has valid syntax', async () => { const targetDirectory = await fsp.mkdtemp('func_env_type_gen_test'); const functionEnvironmentTypeGenerator = - new FunctionEnvironmentTypeGenerator('testFunction'); + new FunctionEnvironmentTypeGenerator('testFunction', ['TEST_ENV']); const filePath = `${process.cwd()}/.amplify/function-env/testFunction.ts`; - functionEnvironmentTypeGenerator.generateTypeDefFile(); + functionEnvironmentTypeGenerator.generateTypedProcessEnvShim(); // 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..c8f1dbca67 100644 --- a/packages/backend-function/src/function_env_type_generator.ts +++ b/packages/backend-function/src/function_env_type_generator.ts @@ -1,25 +1,33 @@ 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, + private readonly amplifyBackendEnvVars: string[] = [] + ) { + this.typeDefFilePath = `${process.cwd()}/.amplify/function-env/${ + this.functionName + }.ts`; } /** - * Generate a type definition file + * Generate a typed process.env shim */ - generateTypeDefFile() { + generateTypedProcessEnvShim() { + const lambdaEnvVarTypeName = 'LambdaProvidedEnvVars'; + const amplifyBackendEnvVarTypeName = 'AmplifyBackendEnvVars'; + const declarations = []; const typeDefFileDirname = path.dirname(this.typeDefFilePath); @@ -27,18 +35,34 @@ export class FunctionEnvironmentTypeGenerator { fs.mkdirSync(typeDefFileDirname, { recursive: true }); } + // Add Lambda runtime environment variables to the typed shim + 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(`type ${amplifyBackendEnvVarTypeName} = {`); + this.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}};`; + `/** Lambda runtime environment variables, see https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime */${EOL}` + + `export const env = process.env as ${lambdaEnvVarTypeName} & ${amplifyBackendEnvVarTypeName};${EOL}${EOL}${declarations.join( + EOL + )}`; fs.writeFileSync(this.typeDefFilePath, content); } 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..9ada0b7b48 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 @@ -36,8 +36,12 @@ export class DataStorageAuthWithTriggerTestProjectCreator ) {} 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, @@ -56,6 +60,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,6 +86,13 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { import.meta.url ); + readonly sourceProjectDotAmplifyDirSuffix = `${this.sourceProjectDirPath}/.amplify`; + + readonly sourceProjectDotAmplifyDirPath: URL = new URL( + this.sourceProjectDotAmplifyDirSuffix, + import.meta.url + ); + private readonly sourceProjectUpdateDirPath: URL = new URL( `${this.sourceProjectDirPath}/update-1`, import.meta.url 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-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..2629c283da --- /dev/null +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/.amplify/function-env/defaultNodeFunction.ts @@ -0,0 +1,87 @@ +/** + * This file is here to make Typescript happy for initial type checking and will be overwritten when tests run + */ +/** Lambda runtime environment variables, see https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime */ +export const env = process.env as LambdaProvidedEnvVars & DefinedEnvVars; + +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; +}; + +type DefinedEnvVars = { + TEST_SECRET: string; + TEST_SHARED_SECRET: string; +}; 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..98d947b744 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,5 +1,10 @@ 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( @@ -7,7 +12,7 @@ Amplify.configure( Storage: { S3: { bucket: process.env.testName_BUCKET_NAME, - region: process.env.AWS_REGION, + 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, }; }; 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/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/scripts/update_tsconfig_refs.ts b/scripts/update_tsconfig_refs.ts index d29f5e6e15..26f29e68fd 100644 --- a/scripts/update_tsconfig_refs.ts +++ b/scripts/update_tsconfig_refs.ts @@ -24,6 +24,18 @@ 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' }, + ], +}; + // First collect information about all the packages in the repo const repoPackagesInfoRecord: Record = {}; @@ -61,16 +73,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); From 79cff6d6840b49366f6c21d090f77c45d485a8ed Mon Sep 17 00:00:00 2001 From: Amplifiyer <51211245+Amplifiyer@users.noreply.github.com> Date: Thu, 29 Feb 2024 13:56:53 +0100 Subject: [PATCH 04/41] fix(client-config): add legacy analytics configuration keys (#1080) * fix(client-config): add legacy analytics configuration keys * change to minor as there is a breaking API change --- .changeset/modern-files-arrive.md | 5 +++++ packages/client-config/API.md | 4 +++- .../src/client-config-types/analytics_client_config.ts | 6 +++++- .../client-config-writer/client_config_converter.test.ts | 2 +- .../src/client-config-writer/client_config_converter.ts | 6 +++--- 5 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 .changeset/modern-files-arrive.md 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/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/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, }, }, }, From 64e425cda04e48991b3f530df0387c34e0f37bdc Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Thu, 29 Feb 2024 10:40:54 -0800 Subject: [PATCH 05/41] Fix typo in cognito placeholder token in S3 access policy (#1084) --- .changeset/chatty-icons-mix.md | 5 +++++ packages/backend-storage/src/access_builder.test.ts | 2 +- packages/backend-storage/src/access_builder.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/chatty-icons-mix.md 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/packages/backend-storage/src/access_builder.test.ts b/packages/backend-storage/src/access_builder.test.ts index eafb365e28..5b616bca01 100644 --- a/packages/backend-storage/src/access_builder.test.ts +++ b/packages/backend-storage/src/access_builder.test.ts @@ -102,7 +102,7 @@ void describe('storageAccessBuilder', () => { ]); assert.equal( accessDefinition.ownerPlaceholderSubstitution, - '${cognito-identity.amazon.com:sub}' + '${cognito-identity.amazonaws.com:sub}' ); assert.equal( accessDefinition.getResourceAccessAcceptor(stubGetInstanceProps), diff --git a/packages/backend-storage/src/access_builder.ts b/packages/backend-storage/src/access_builder.ts index 149b8da589..0035f8d3eb 100644 --- a/packages/backend-storage/src/access_builder.ts +++ b/packages/backend-storage/src/access_builder.ts @@ -25,7 +25,7 @@ export const roleAccessBuilder: StorageAccessBuilder = { to: (actions) => ({ getResourceAccessAcceptor: getAuthRoleResourceAccessAcceptor, actions, - ownerPlaceholderSubstitution: '${cognito-identity.amazon.com:sub}', + ownerPlaceholderSubstitution: '${cognito-identity.amazonaws.com:sub}', }), }, resource: (other) => ({ From 7dc3132a00cedcbadd6fbaf35b5eda477390c171 Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Thu, 29 Feb 2024 15:21:36 -0800 Subject: [PATCH 06/41] Add role trust policy validator aspect to root stack (#1083) --- .changeset/late-worms-rule.md | 5 ++ .../backend/src/engine/amplify_stack.test.ts | 63 ++++++++++++++++ packages/backend/src/engine/amplify_stack.ts | 75 ++++++++++++++++++- 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 .changeset/late-worms-rule.md 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/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', + }); + } + }; +} From 9ad867e17d216b2292fb4f942a84f728e2471a5f Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Thu, 29 Feb 2024 16:53:19 -0800 Subject: [PATCH 07/41] update publish job to depend on e2e and unit test jobs (#1092) --- .github/workflows/health_checks.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/health_checks.yml b/.github/workflows/health_checks.yml index c30790dfb0..8028b41fe2 100644 --- a/.github/workflows/health_checks.yml +++ b/.github/workflows/health_checks.yml @@ -302,7 +302,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 From 280d678cd0a56729c9380c128894f4404ee4178f Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Fri, 1 Mar 2024 09:13:09 -0800 Subject: [PATCH 08/41] Verify IAM roles are deleted when e2e stacks are torn down (#1086) --- .changeset/proud-feet-hide.md | 2 + package-lock.json | 910 +++++++++--------- packages/integration-tests/package.json | 1 + .../data_storage_auth_with_triggers.ts | 62 ++ .../test_project_creator.ts | 3 + 5 files changed, 532 insertions(+), 446 deletions(-) create mode 100644 .changeset/proud-feet-hide.md 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/package-lock.json b/package-lock.json index 7784eeaf74..90e7db8031 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { @@ -10562,11 +10579,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 +10608,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 +10623,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 +10641,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 +10656,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 +10718,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 +10741,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 +10768,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 +10798,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 +10811,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 +10828,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 +10855,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 +10867,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 +10879,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 +10893,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 +10908,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 +10920,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 +10932,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 +10945,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 +10957,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 +10980,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 +10998,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 +11014,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 +11025,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 +11089,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 +11104,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 +11121,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 +11145,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 +11157,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 +11170,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 +11331,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": { @@ -23846,6 +23863,7 @@ "@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", diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index ea4474cdd4..37b69ec045 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -12,6 +12,7 @@ "@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", 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 9ada0b7b48..00d6451fa0 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,6 +33,7 @@ export class DataStorageAuthWithTriggerTestProjectCreator private readonly secretClient: SecretClient, private readonly lambdaClient: LambdaClient, private readonly s3Client: S3Client, + private readonly iamClient: IAMClient, private readonly resourceFinder: DeployedResourcesFinder ) {} @@ -51,6 +53,7 @@ export class DataStorageAuthWithTriggerTestProjectCreator this.secretClient, this.lambdaClient, this.s3Client, + this.iamClient, this.resourceFinder ); await fs.cp( @@ -112,6 +115,7 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { private amplifySharedSecret: string; private testBucketName: string; + private testRoleNames: string[]; /** * Create a test project instance. @@ -124,6 +128,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); @@ -225,6 +230,12 @@ 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' + ); } private setUpDeployEnvironment = async ( @@ -273,6 +284,7 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { private assertExpectedCleanup = async () => { await this.waitForBucketDeletion(this.testBucketName); + await this.assertRolesDoNotExist(this.testRoleNames); }; /** @@ -307,4 +319,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/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), From 7857f0a6106cec35cf605531f363542646e7ef13 Mon Sep 17 00:00:00 2001 From: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Fri, 1 Mar 2024 13:21:52 -0500 Subject: [PATCH 09/41] add JS resolver support (#1095) --- .changeset/fluffy-books-dance.md | 6 + package-lock.json | 20 +-- packages/backend-data/package.json | 4 +- .../src/assets/js_resolver_handler.ts | 12 ++ .../backend-data/src/assets/tsconfig.json | 10 ++ .../src/convert_js_resolvers.test.ts | 161 ++++++++++++++++++ .../backend-data/src/convert_js_resolvers.ts | 109 ++++++++++++ packages/backend-data/src/convert_schema.ts | 11 +- packages/backend-data/src/factory.ts | 13 +- packages/backend-data/tsconfig.json | 4 +- packages/backend/package.json | 2 +- packages/integration-tests/package.json | 2 +- .../data_storage_auth_with_triggers.ts | 4 +- .../amplify/data/js_custom_fn.js | 15 ++ .../amplify/data/resource.ts | 16 ++ scripts/update_tsconfig_refs.ts | 4 + 16 files changed, 371 insertions(+), 22 deletions(-) create mode 100644 .changeset/fluffy-books-dance.md create mode 100644 packages/backend-data/src/assets/js_resolver_handler.ts create mode 100644 packages/backend-data/src/assets/tsconfig.json create mode 100644 packages/backend-data/src/convert_js_resolvers.test.ts create mode 100644 packages/backend-data/src/convert_js_resolvers.ts create mode 100644 packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/data/js_custom_fn.js 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/package-lock.json b/package-lock.json index 90e7db8031..50da30ab96 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.8", + "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema/-/data-schema-0.13.8.tgz", + "integrity": "sha512-167euk2z2QGC25wCUMMREVY7FKDmgK9FOfQVt3Oz1hIy1awL2PFPPbad+8Egf989a+gtGWbNdx3zrx97XLu4vw==", "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.6", + "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema-types/-/data-schema-types-0.7.6.tgz", + "integrity": "sha512-g5LmvLEJ5BDtfxRT6QK/+HhTfGGADbF7gbxtWPbhP9hRYwDai0A9ARJ6qETLPzUE/1vXtURlJV+MLutsDrIkPA==", "dependencies": { "rxjs": "^7.8.1" } @@ -23344,7 +23344,7 @@ "@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/data-schema": "^0.13.8", "@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" @@ -23384,12 +23384,12 @@ "@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.6", "@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.8", "@aws-amplify/platform-core": "^0.5.0-beta.1" }, "peerDependencies": { @@ -23859,7 +23859,7 @@ "@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/data-schema": "^0.13.8", "@aws-amplify/platform-core": "^0.5.0-beta.1", "@aws-sdk/client-amplify": "^3.465.0", "@aws-sdk/client-cloudformation": "^3.465.0", diff --git a/packages/backend-data/package.json b/packages/backend-data/package.json index 7ea364a482..01423ae013 100644 --- a/packages/backend-data/package.json +++ b/packages/backend-data/package.json @@ -18,7 +18,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-amplify/data-schema": "^0.13.2", + "@aws-amplify/data-schema": "^0.13.8", "@aws-amplify/backend-platform-test-stubs": "^0.3.3-beta.0", "@aws-amplify/platform-core": "^0.5.0-beta.1" }, @@ -31,6 +31,6 @@ "@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.6" } } 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_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.ts b/packages/backend-data/src/factory.ts index 1573d5651d..b16d753d80 100644 --- a/packages/backend-data/src/factory.ts +++ b/packages/backend-data/src/factory.ts @@ -11,7 +11,7 @@ import { AmplifyData } 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 { convertSchemaToCDK, isModelSchema } from './convert_schema.js'; import { FunctionInstanceProvider, buildConstructFactoryFunctionInstanceProvider, @@ -25,6 +25,7 @@ import { } from './convert_authorization_modes.js'; import { validateAuthorizationModes } from './validate_authorization_modes.js'; import { AmplifyUserError } from '@aws-amplify/platform-core'; +import { convertJsResolverDefinition } from './convert_js_resolvers.js'; /** * Singleton factory for AmplifyGraphqlApi constructs that can be used in Amplify project files. @@ -145,7 +146,11 @@ class DataGenerator implements ConstructContainerEntryGenerator { ); let amplifyGraphqlDefinition; + let jsFunctions; try { + if (isModelSchema(this.props.schema)) { + ({ jsFunctions } = this.props.schema.transform()); + } amplifyGraphqlDefinition = convertSchemaToCDK(this.props.schema); } catch (error) { throw new AmplifyUserError( @@ -160,7 +165,7 @@ class DataGenerator implements ConstructContainerEntryGenerator { ); } - return new AmplifyData(scope, this.defaultName, { + const amplifyApi = new AmplifyData(scope, this.defaultName, { apiName: this.props.name, definition: amplifyGraphqlDefinition, authorizationModes, @@ -168,6 +173,10 @@ class DataGenerator implements ConstructContainerEntryGenerator { functionNameMap, translationBehavior: { sandboxModeEnabled }, }); + + convertJsResolverDefinition(scope, amplifyApi, jsFunctions); + + return amplifyApi; }; } 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/package.json b/packages/backend/package.json index b3088fd0a0..505dd8d232 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -21,7 +21,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/data-schema": "^0.13.2", + "@aws-amplify/data-schema": "^0.13.8", "@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", diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 37b69ec045..69cf1523dd 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -8,7 +8,7 @@ "@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/data-schema": "^0.13.8", "@aws-amplify/platform-core": "^0.5.0-beta.1", "@aws-sdk/client-amplify": "^3.465.0", "@aws-sdk/client-cloudformation": "^3.465.0", 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 00d6451fa0..6500407750 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 @@ -179,8 +179,8 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { sourceFile: sourceDataResourceFile, projectFile: dataResourceFile, deployThresholdSec: { - onWindows: 40, - onOther: 30, + onWindows: 135, + onOther: 125, }, }, ]; 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..0adc7025b6 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,6 +20,19 @@ 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() }) @@ -28,6 +41,9 @@ const schema = a.schema({ .function('echo'), }) 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/scripts/update_tsconfig_refs.ts b/scripts/update_tsconfig_refs.ts index 26f29e68fd..c9c168e3db 100644 --- a/scripts/update_tsconfig_refs.ts +++ b/scripts/update_tsconfig_refs.ts @@ -34,6 +34,10 @@ const additionalRefs: Record = { // 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 From 318335d7bae7b0aa20dda23f455030d72b9c55b1 Mon Sep 17 00:00:00 2001 From: Roshane Pascual Date: Fri, 1 Mar 2024 10:53:54 -0800 Subject: [PATCH 10/41] ensure resource access env vars are added to typed shim files (#1091) --- .changeset/modern-terms-stare.md | 6 ++++++ .../src/function_env_type_generator.test.ts | 2 +- .../src/function_env_type_generator.ts | 14 +++++++++----- .../.amplify/function-env/defaultNodeFunction.ts | 8 +++++--- .../amplify/func-src/response_generator.ts | 2 +- 5 files changed, 22 insertions(+), 10 deletions(-) create mode 100644 .changeset/modern-terms-stare.md 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/packages/backend-function/src/function_env_type_generator.test.ts b/packages/backend-function/src/function_env_type_generator.test.ts index cd24d64555..18686aedc7 100644 --- a/packages/backend-function/src/function_env_type_generator.test.ts +++ b/packages/backend-function/src/function_env_type_generator.test.ts @@ -36,7 +36,7 @@ void describe('FunctionEnvironmentTypeGenerator', () => { mock.restoreAll(); }); - void it('generates a type definition file with dynamic environment variables', () => { + 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); diff --git a/packages/backend-function/src/function_env_type_generator.ts b/packages/backend-function/src/function_env_type_generator.ts index c8f1dbca67..3a9e243dcd 100644 --- a/packages/backend-function/src/function_env_type_generator.ts +++ b/packages/backend-function/src/function_env_type_generator.ts @@ -36,6 +36,9 @@ export class FunctionEnvironmentTypeGenerator { } // 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]} */`; @@ -50,6 +53,9 @@ export class FunctionEnvironmentTypeGenerator { * 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} = {`); this.amplifyBackendEnvVars.forEach((envName) => { const declaration = `${envName}: string;`; @@ -58,11 +64,9 @@ export class FunctionEnvironmentTypeGenerator { }); declarations.push(`};${EOL}`); - const content = - `/** Lambda runtime environment variables, see https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime */${EOL}` + - `export const env = process.env as ${lambdaEnvVarTypeName} & ${amplifyBackendEnvVarTypeName};${EOL}${EOL}${declarations.join( - 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/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 index 2629c283da..a077cd1963 100644 --- 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 @@ -1,9 +1,9 @@ /** * This file is here to make Typescript happy for initial type checking and will be overwritten when tests run */ -/** Lambda runtime environment variables, see https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime */ -export const env = process.env as LambdaProvidedEnvVars & DefinedEnvVars; +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; @@ -81,7 +81,9 @@ type LambdaProvidedEnvVars = { TZ: string; }; -type DefinedEnvVars = { +/** 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/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 98d947b744..5b55333908 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 @@ -11,7 +11,7 @@ Amplify.configure( { Storage: { S3: { - bucket: process.env.testName_BUCKET_NAME, + bucket: env.testName_BUCKET_NAME, region: env.AWS_REGION, }, }, From 5d3e5bd728ccce36bb6b7bae03ad1a1acd63d191 Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Mon, 4 Mar 2024 10:34:26 -0800 Subject: [PATCH 11/41] Retry storage upload/download in e2e lambda (#1098) --- .changeset/shy-horses-act.md | 2 + .../amplify/func-src/response_generator.ts | 40 ++++++++++++++----- 2 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 .changeset/shy-horses-act.md 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/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 5b55333908..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 @@ -53,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(); + } +}; From 82006e54fb25125ff9ebc5b495946a9733b2bbc5 Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Mon, 4 Mar 2024 12:13:37 -0800 Subject: [PATCH 12/41] Add "list" action to storage resource access (#1099) --- .changeset/short-olives-bow.md | 5 + packages/backend-storage/API.md | 4 +- packages/backend-storage/src/private_types.ts | 7 ++ .../src/storage_access_orchestrator.test.ts | 107 +++++++++++++++--- .../src/storage_access_orchestrator.ts | 18 ++- .../src/storage_access_policy_factory.test.ts | 67 +++++++++-- .../src/storage_access_policy_factory.ts | 53 +++++++-- packages/backend-storage/src/types.ts | 17 ++- 8 files changed, 234 insertions(+), 44 deletions(-) create mode 100644 .changeset/short-olives-bow.md 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/packages/backend-storage/API.md b/packages/backend-storage/API.md index 2f351aaa3b..786dbb0a52 100644 --- a/packages/backend-storage/API.md +++ b/packages/backend-storage/API.md @@ -54,8 +54,8 @@ export type StorageAccessGenerator = (allow: StorageAccessBuilder) => StorageAcc // @public (undocumented) export type StorageAccessRecord = Record; -// @public (undocumented) -export type StorageAction = 'read' | 'write' | 'delete'; +// @public +export type StorageAction = 'read' | 'get' | 'list' | 'write' | 'delete'; // @public (undocumented) export type StorageActionBuilder = { 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 index 904e7a17e5..d0c1ecf7c6 100644 --- a/packages/backend-storage/src/storage_access_orchestrator.test.ts +++ b/packages/backend-storage/src/storage_access_orchestrator.test.ts @@ -30,7 +30,7 @@ void describe('StorageAccessOrchestrator', () => { () => ({ '/test/prefix/*': [ { - actions: ['read', 'write'], + actions: ['get', 'write'], getResourceAccessAcceptor: () => ({ identifier: 'testResourceAccessAcceptor', acceptResourceAccess: acceptResourceAccessMock, @@ -59,7 +59,7 @@ void describe('StorageAccessOrchestrator', () => { () => ({ '/test/prefix/*': [ { - actions: ['read', 'write'], + actions: ['get', 'write'], getResourceAccessAcceptor: () => ({ identifier: 'testResourceAccessAcceptor', acceptResourceAccess: acceptResourceAccessMock, @@ -109,14 +109,14 @@ void describe('StorageAccessOrchestrator', () => { () => ({ '/test/prefix/*': [ { - actions: ['read', 'write', 'delete'], + actions: ['get', 'write', 'delete'], getResourceAccessAcceptor: getResourceAccessAcceptorStub, ownerPlaceholderSubstitution: '*', }, ], '/another/prefix/*': [ { - actions: ['read'], + actions: ['get'], getResourceAccessAcceptor: getResourceAccessAcceptorStub, ownerPlaceholderSubstitution: '*', }, @@ -176,19 +176,19 @@ void describe('StorageAccessOrchestrator', () => { () => ({ '/test/prefix/*': [ { - actions: ['read', 'write', 'delete'], + actions: ['get', 'write', 'delete'], getResourceAccessAcceptor: getResourceAccessAcceptorStub1, ownerPlaceholderSubstitution: '*', }, { - actions: ['read'], + actions: ['get'], getResourceAccessAcceptor: getResourceAccessAcceptorStub2, ownerPlaceholderSubstitution: '*', }, ], '/another/prefix/*': [ { - actions: ['read', 'delete'], + actions: ['get', 'delete'], getResourceAccessAcceptor: getResourceAccessAcceptorStub2, ownerPlaceholderSubstitution: '*', }, @@ -262,7 +262,7 @@ void describe('StorageAccessOrchestrator', () => { () => ({ [`/test/${ownerPathPartToken}/*`]: [ { - actions: ['read', 'write'], + actions: ['get', 'write'], getResourceAccessAcceptor: () => ({ identifier: 'testResourceAccessAcceptor', acceptResourceAccess: acceptResourceAccessMock, @@ -309,7 +309,7 @@ void describe('StorageAccessOrchestrator', () => { () => ({ '/foo/*': [ { - actions: ['read', 'write'], + actions: ['get', 'write'], getResourceAccessAcceptor: () => ({ identifier: 'resourceAccessAcceptor1', acceptResourceAccess: acceptResourceAccessMock1, @@ -319,7 +319,7 @@ void describe('StorageAccessOrchestrator', () => { ], '/foo/bar/*': [ { - actions: ['read'], + actions: ['get'], getResourceAccessAcceptor: () => ({ identifier: 'resourceAccessAcceptor2', acceptResourceAccess: acceptResourceAccessMock2, @@ -400,7 +400,7 @@ void describe('StorageAccessOrchestrator', () => { ownerPlaceholderSubstitution: '{ownerSub}', }, { - actions: ['read'], + actions: ['get'], getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, ownerPlaceholderSubstitution: '*', }, @@ -460,7 +460,7 @@ void describe('StorageAccessOrchestrator', () => { // acceptor2 should not have any rules for this path '/foo/*': [ { - actions: ['read', 'write'], + actions: ['get', 'write'], getResourceAccessAcceptor: getResourceAccessAcceptorStub1, ownerPlaceholderSubstitution: '*', }, @@ -469,7 +469,7 @@ void describe('StorageAccessOrchestrator', () => { // acceptor2 should have only read on this path '/foo/bar/*': [ { - actions: ['read'], + actions: ['get'], getResourceAccessAcceptor: getResourceAccessAcceptorStub2, ownerPlaceholderSubstitution: '{ownerSub}', }, @@ -478,7 +478,7 @@ void describe('StorageAccessOrchestrator', () => { // acceptor2 should not have any rules for this path '/foo/baz/*': [ { - actions: ['read'], + actions: ['get'], ownerPlaceholderSubstitution: '*', getResourceAccessAcceptor: getResourceAccessAcceptorStub1, }, @@ -487,12 +487,12 @@ void describe('StorageAccessOrchestrator', () => { // acceptor 2 has read/write/delete on path with ownerSub '/other/{owner}/*': [ { - actions: ['read', 'write', 'delete'], + actions: ['get', 'write', 'delete'], getResourceAccessAcceptor: getResourceAccessAcceptorStub2, ownerPlaceholderSubstitution: '{ownerSub}', }, { - actions: ['read'], + actions: ['get'], getResourceAccessAcceptor: getResourceAccessAcceptorStub1, ownerPlaceholderSubstitution: '*', }, @@ -584,7 +584,7 @@ void describe('StorageAccessOrchestrator', () => { () => ({ '/foo/*': [ { - actions: ['read'], + actions: ['get'], getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, ownerPlaceholderSubstitution: '*', }, @@ -635,6 +635,79 @@ void describe('StorageAccessOrchestrator', () => { 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, + ownerPlaceholderSubstitution: '*', + }, + { + actions: ['list'], + getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, + ownerPlaceholderSubstitution: '*', + }, + ], + '/other/baz/*': [ + { + actions: ['read'], + getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, + ownerPlaceholderSubstitution: '*', + }, + ], + }), + {} 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 + ); + }); }); }); diff --git a/packages/backend-storage/src/storage_access_orchestrator.ts b/packages/backend-storage/src/storage_access_orchestrator.ts index c111bc422d..b3494bce04 100644 --- a/packages/backend-storage/src/storage_access_orchestrator.ts +++ b/packages/backend-storage/src/storage_access_orchestrator.ts @@ -6,13 +6,13 @@ import { import { StorageAccessBuilder, StorageAccessGenerator, - StorageAction, StoragePath, } from './types.js'; import { ownerPathPartToken } 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 */ @@ -36,7 +36,7 @@ export class StorageAccessOrchestrator { { acceptor: ResourceAccessAcceptor; accessMap: Map< - StorageAction, + InternalStorageAction, { allow: Set; deny: Set } >; } @@ -104,10 +104,20 @@ export class StorageAccessOrchestrator { permission.ownerPlaceholderSubstitution ) 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, - permission.actions, + noDuplicateActions, prefix ); }); @@ -124,7 +134,7 @@ export class StorageAccessOrchestrator { */ private addAccessDefinition = ( resourceAccessAcceptor: ResourceAccessAcceptor, - actions: StorageAction[], + actions: InternalStorageAction[], s3Prefix: StoragePath ) => { const acceptorToken = resourceAccessAcceptor.identifier; 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 90463fd6a8..653f3f108b 100644 --- a/packages/backend-storage/src/storage_access_policy_factory.test.ts +++ b/packages/backend-storage/src/storage_access_policy_factory.test.ts @@ -21,7 +21,7 @@ void describe('StorageAccessPolicyFactory', () => { const bucketPolicyFactory = new StorageAccessPolicyFactory(bucket); const policy = bucketPolicyFactory.createPolicy( new Map([ - ['read', { allow: new Set(['/some/prefix/*']), deny: new Set() }], + ['get', { allow: new Set(['/some/prefix/*']), deny: new Set() }], ]) ); @@ -135,7 +135,7 @@ void describe('StorageAccessPolicyFactory', () => { const policy = bucketPolicyFactory.createPolicy( new Map([ [ - 'read', + 'get', { allow: new Set(['/some/prefix/*', '/another/path/*']), deny: new Set(), @@ -191,7 +191,7 @@ void describe('StorageAccessPolicyFactory', () => { const bucketPolicyFactory = new StorageAccessPolicyFactory(bucket); const policy = bucketPolicyFactory.createPolicy( new Map([ - ['read', { allow: new Set(['/some/prefix/*']), deny: new Set() }], + ['get', { allow: new Set(['/some/prefix/*']), deny: new Set() }], ['write', { allow: new Set(['/another/path/*']), deny: new Set() }], ]) ); @@ -244,7 +244,7 @@ void describe('StorageAccessPolicyFactory', () => { const bucketPolicyFactory = new StorageAccessPolicyFactory(bucket); const policy = bucketPolicyFactory.createPolicy( new Map([ - ['read', { allow: new Set(['/some/prefix/*']), deny: new Set() }], + ['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() }], ]) @@ -312,7 +312,7 @@ void describe('StorageAccessPolicyFactory', () => { const bucketPolicyFactory = new StorageAccessPolicyFactory(bucket); const policy = bucketPolicyFactory.createPolicy( new Map([ - ['read', { allow: new Set(['/foo/*', '/foo/bar/*']), deny: new Set() }], + ['get', { allow: new Set(['/foo/*', '/foo/bar/*']), deny: new Set() }], [ 'write', { allow: new Set(['/foo/*']), deny: new Set(['/foo/bar/*']) }, @@ -397,7 +397,7 @@ void describe('StorageAccessPolicyFactory', () => { const policy = bucketPolicyFactory.createPolicy( new Map([ [ - 'read', + 'get', { allow: new Set(['/foo/*']), deny: new Set(['/foo/bar/*']), @@ -489,7 +489,7 @@ void describe('StorageAccessPolicyFactory', () => { const policy = bucketPolicyFactory.createPolicy( new Map([ [ - 'read', + 'get', { allow: new Set(['/foo/*']), deny: new Set(['/foo/bar/*', '/other/path/*', '/something/else/*']), @@ -566,6 +566,59 @@ void describe('StorageAccessPolicyFactory', () => { }, }); }); + + 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 1c77af10de..1d6cb43f8e 100644 --- a/packages/backend-storage/src/storage_access_policy_factory.ts +++ b/packages/backend-storage/src/storage_access_policy_factory.ts @@ -3,6 +3,7 @@ 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[]; @@ -30,7 +31,7 @@ export class StorageAccessPolicyFactory { createPolicy = ( permissions: Map< - StorageAction, + InternalStorageAction, { allow: Set; deny: Set } > ) => { @@ -69,20 +70,48 @@ export class StorageAccessPolicyFactory { private getStatement = ( s3Prefixes: Readonly>, - action: StorageAction, + action: InternalStorageAction, effect: Effect - ) => - new PolicyStatement({ - effect, - actions: actionMap[action], - resources: Array.from(s3Prefixes).map( - (s3Prefix) => `${this.bucket.bucketArn}${s3Prefix}` - ), - }); + ) => { + 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/types.ts b/packages/backend-storage/src/types.ts index 528fe2d85e..871f71146a 100644 --- a/packages/backend-storage/src/types.ts +++ b/packages/backend-storage/src/types.ts @@ -62,9 +62,22 @@ export type StorageAccessDefinition = { ownerPlaceholderSubstitution: 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}/*`; From 4f1b1688e3aa658d53dc6102e908044f13e42761 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 14:08:20 -0800 Subject: [PATCH 13/41] Version Packages (beta) (#1054) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 23 ++++++++++++++++++++++ packages/auth-construct/CHANGELOG.md | 6 ++++++ packages/auth-construct/package.json | 2 +- packages/backend-auth/CHANGELOG.md | 8 ++++++++ packages/backend-auth/package.json | 4 ++-- packages/backend-data/CHANGELOG.md | 7 +++++++ packages/backend-data/package.json | 2 +- packages/backend-function/CHANGELOG.md | 11 +++++++++++ packages/backend-function/package.json | 2 +- packages/backend-storage/CHANGELOG.md | 15 ++++++++++++++ packages/backend-storage/package.json | 2 +- packages/backend/CHANGELOG.md | 26 +++++++++++++++++++++++++ packages/backend/package.json | 12 ++++++------ packages/cli-core/CHANGELOG.md | 12 ++++++++++++ packages/cli-core/package.json | 2 +- packages/cli/CHANGELOG.md | 13 +++++++++++++ packages/cli/package.json | 8 ++++---- packages/client-config/CHANGELOG.md | 6 ++++++ packages/client-config/package.json | 2 +- packages/create-amplify/CHANGELOG.md | 14 +++++++++++++ packages/create-amplify/package.json | 4 ++-- packages/integration-tests/CHANGELOG.md | 12 ++++++++++++ packages/integration-tests/package.json | 8 ++++---- packages/sandbox/CHANGELOG.md | 12 ++++++++++++ packages/sandbox/package.json | 6 +++--- 25 files changed, 192 insertions(+), 27 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index a133e8886e..20dda29fbc 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -30,17 +30,40 @@ "brave-carrots-glow", "brave-pets-clean", "brown-otters-smoke", + "chatty-icons-mix", + "cyan-steaks-repeat", "eighty-rings-pull", "five-fireants-shout", + "fluffy-books-dance", "four-donuts-jump", "friendly-kids-lick", "giant-feet-sing", + "great-timers-invent", "khaki-panthers-grin", + "khaki-pants-sniff", + "late-worms-rule", "lemon-peas-sin", "light-cougars-give", + "little-books-press", "loud-sheep-occur", + "lucky-trainers-matter", "mean-frogs-visit", "mighty-experts-compare", + "modern-files-arrive", + "modern-terms-stare", + "new-kings-beg", + "polite-kiwis-brake", + "popular-bobcats-provide", + "pretty-cups-jog", + "pretty-lobsters-prove", + "proud-feet-hide", + "quiet-pets-scream", + "quiet-shirts-hug", + "rude-toys-visit", + "short-bulldogs-punch", + "short-olives-bow", + "shy-horses-act", + "silver-needles-rush", "smooth-penguins-joke", "smooth-tigers-double", "sour-rice-listen", diff --git a/packages/auth-construct/CHANGELOG.md b/packages/auth-construct/CHANGELOG.md index b1b2dcf00f..f388c901fa 100644 --- a/packages/auth-construct/CHANGELOG.md +++ b/packages/auth-construct/CHANGELOG.md @@ -1,5 +1,11 @@ # @aws-amplify/auth-construct-alpha +## 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..97f0626447 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.3", "type": "commonjs", "publishConfig": { "access": "public" diff --git a/packages/backend-auth/CHANGELOG.md b/packages/backend-auth/CHANGELOG.md index 1045a434a5..4ae77b8d4e 100644 --- a/packages/backend-auth/CHANGELOG.md +++ b/packages/backend-auth/CHANGELOG.md @@ -1,5 +1,13 @@ # @aws-amplify/backend-auth +## 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..ee4d34c724 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.3", "type": "module", "publishConfig": { "access": "public" @@ -18,7 +18,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.2", + "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.3", "@aws-amplify/backend-output-storage": "^0.4.0-beta.1", "@aws-amplify/plugin-types": "^0.9.0-beta.0" }, diff --git a/packages/backend-data/CHANGELOG.md b/packages/backend-data/CHANGELOG.md index 49184e16d7..bffd11e857 100644 --- a/packages/backend-data/CHANGELOG.md +++ b/packages/backend-data/CHANGELOG.md @@ -1,5 +1,12 @@ # @aws-amplify/backend-data +## 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 01423ae013..fa392ac063 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.3", "type": "module", "publishConfig": { "access": "public" diff --git a/packages/backend-function/CHANGELOG.md b/packages/backend-function/CHANGELOG.md index 58e13df3f1..4f62e22084 100644 --- a/packages/backend-function/CHANGELOG.md +++ b/packages/backend-function/CHANGELOG.md @@ -1,5 +1,16 @@ # @aws-amplify/backend-function +## 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..15e14967cb 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.2", "type": "module", "publishConfig": { "access": "public" diff --git a/packages/backend-storage/CHANGELOG.md b/packages/backend-storage/CHANGELOG.md index e28ea4eae3..32efdba0c1 100644 --- a/packages/backend-storage/CHANGELOG.md +++ b/packages/backend-storage/CHANGELOG.md @@ -1,5 +1,20 @@ # @aws-amplify/backend-storage +## 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..e7d5dea314 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.2", "type": "module", "publishConfig": { "access": "public" diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 4814f0392b..f594dec1a4 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,31 @@ # @aws-amplify/backend +## 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/package.json b/packages/backend/package.json index 505dd8d232..778ee21853 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.4", "type": "module", "publishConfig": { "access": "public" @@ -22,14 +22,14 @@ "license": "Apache-2.0", "dependencies": { "@aws-amplify/data-schema": "^0.13.8", - "@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/backend-auth": "^0.5.0-beta.3", + "@aws-amplify/backend-function": "^0.8.0-beta.2", + "@aws-amplify/backend-data": "^0.10.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/backend-storage": "^0.6.0-beta.2", + "@aws-amplify/client-config": "^0.9.0-beta.3", "@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" diff --git a/packages/cli-core/CHANGELOG.md b/packages/cli-core/CHANGELOG.md index 061c3742c5..ea62927c57 100644 --- a/packages/cli-core/CHANGELOG.md +++ b/packages/cli-core/CHANGELOG.md @@ -1,5 +1,17 @@ # @aws-amplify/cli-core +## 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 01060e151a..dc3361fa30 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.1", "type": "module", "publishConfig": { "access": "public" diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 16fa3e8f64..fe72250307 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,18 @@ # @aws-amplify/backend-cli +## 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..6447e95b46 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.4", "description": "Command line interface for various Amplify tools", "bin": { "amplify": "lib/amplify.js" @@ -32,13 +32,13 @@ "@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.3", "@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/client-config/CHANGELOG.md b/packages/client-config/CHANGELOG.md index 543de3cbbc..9be0100b4e 100644 --- a/packages/client-config/CHANGELOG.md +++ b/packages/client-config/CHANGELOG.md @@ -1,5 +1,11 @@ # @aws-amplify/client-config +## 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..766bef8882 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.3", "type": "module", "publishConfig": { "access": "public" diff --git a/packages/create-amplify/CHANGELOG.md b/packages/create-amplify/CHANGELOG.md index 900e077368..eab6588993 100644 --- a/packages/create-amplify/CHANGELOG.md +++ b/packages/create-amplify/CHANGELOG.md @@ -1,5 +1,19 @@ # create-amplify +## 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/package.json b/packages/create-amplify/package.json index c397c2f69f..558267f7eb 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.3", "type": "module", "publishConfig": { "access": "public" @@ -16,7 +16,7 @@ }, "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", diff --git a/packages/integration-tests/CHANGELOG.md b/packages/integration-tests/CHANGELOG.md index 62c5e9ad70..17e5e8c811 100644 --- a/packages/integration-tests/CHANGELOG.md +++ b/packages/integration-tests/CHANGELOG.md @@ -1,5 +1,17 @@ # @aws-amplify/integration-tests +## 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 69cf1523dd..baa2607c9b 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -1,13 +1,13 @@ { "name": "@aws-amplify/integration-tests", "private": true, - "version": "0.4.4-beta.0", + "version": "0.5.0-beta.1", "type": "module", "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.3", + "@aws-amplify/backend": "^0.13.0-beta.4", "@aws-amplify/backend-secret": "^0.4.5-beta.1", - "@aws-amplify/client-config": "^0.8.1-beta.2", + "@aws-amplify/client-config": "^0.9.0-beta.3", "@aws-amplify/data-schema": "^0.13.8", "@aws-amplify/platform-core": "^0.5.0-beta.1", "@aws-sdk/client-amplify": "^3.465.0", diff --git a/packages/sandbox/CHANGELOG.md b/packages/sandbox/CHANGELOG.md index 58be13e643..4ebc709b58 100644 --- a/packages/sandbox/CHANGELOG.md +++ b/packages/sandbox/CHANGELOG.md @@ -1,5 +1,17 @@ # @aws-amplify/sandbox +## 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..eb9473a1d6 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.3", "type": "module", "publishConfig": { "access": "public" @@ -20,8 +20,8 @@ "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/credential-providers": "^3.465.0", From 532abbcede97b69c1e30f33b10f5cdc683e72e37 Mon Sep 17 00:00:00 2001 From: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:26:31 -0500 Subject: [PATCH 14/41] restore data-storage-auth e2e thresholds (#1102) --- .changeset/famous-eels-kiss.md | 2 + package-lock.json | 54 +++++++++---------- .../data_storage_auth_with_triggers.ts | 4 +- .../update-1/data/resource.ts | 13 +++++ 4 files changed, 44 insertions(+), 29 deletions(-) create mode 100644 .changeset/famous-eels-kiss.md 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/package-lock.json b/package-lock.json index 50da30ab96..cfca546d09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23318,7 +23318,7 @@ }, "packages/auth-construct": { "name": "@aws-amplify/auth-construct-alpha", - "version": "0.6.0-beta.2", + "version": "0.6.0-beta.3", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", @@ -23333,17 +23333,17 @@ }, "packages/backend": { "name": "@aws-amplify/backend", - "version": "0.13.0-beta.3", + "version": "0.13.0-beta.4", "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.3", + "@aws-amplify/backend-data": "^0.10.0-beta.3", + "@aws-amplify/backend-function": "^0.8.0-beta.2", "@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/backend-storage": "^0.6.0-beta.2", + "@aws-amplify/client-config": "^0.9.0-beta.3", "@aws-amplify/data-schema": "^0.13.8", "@aws-amplify/platform-core": "^0.5.0-beta.1", "@aws-amplify/plugin-types": "^0.9.0-beta.0", @@ -23360,10 +23360,10 @@ }, "packages/backend-auth": { "name": "@aws-amplify/backend-auth", - "version": "0.5.0-beta.2", + "version": "0.5.0-beta.3", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.2", + "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.3", "@aws-amplify/backend-output-storage": "^0.4.0-beta.1", "@aws-amplify/plugin-types": "^0.9.0-beta.0" }, @@ -23378,7 +23378,7 @@ }, "packages/backend-data": { "name": "@aws-amplify/backend-data", - "version": "0.10.0-beta.2", + "version": "0.10.0-beta.3", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", @@ -23414,7 +23414,7 @@ }, "packages/backend-function": { "name": "@aws-amplify/backend-function", - "version": "0.8.0-beta.1", + "version": "0.8.0-beta.2", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", @@ -23481,7 +23481,7 @@ }, "packages/backend-storage": { "name": "@aws-amplify/backend-storage", - "version": "0.6.0-beta.1", + "version": "0.6.0-beta.2", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", @@ -23499,19 +23499,19 @@ }, "packages/cli": { "name": "@aws-amplify/backend-cli", - "version": "0.12.0-beta.3", + "version": "0.12.0-beta.4", "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.3", "@aws-sdk/credential-provider-ini": "^3.465.0", "@aws-sdk/credential-providers": "^3.465.0", "@aws-sdk/region-config-resolver": "^3.465.0", @@ -23540,7 +23540,7 @@ }, "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", @@ -23661,7 +23661,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", @@ -23679,10 +23679,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", @@ -23852,13 +23852,13 @@ }, "packages/integration-tests": { "name": "@aws-amplify/integration-tests", - "version": "0.4.4-beta.0", + "version": "0.5.0-beta.1", "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.3", + "@aws-amplify/backend": "^0.13.0-beta.4", "@aws-amplify/backend-secret": "^0.4.5-beta.1", - "@aws-amplify/client-config": "^0.8.1-beta.2", + "@aws-amplify/client-config": "^0.9.0-beta.3", "@aws-amplify/data-schema": "^0.13.8", "@aws-amplify/platform-core": "^0.5.0-beta.1", "@aws-sdk/client-amplify": "^3.465.0", @@ -24440,13 +24440,13 @@ }, "packages/sandbox": { "name": "@aws-amplify/sandbox", - "version": "0.5.2-beta.2", + "version": "0.5.2-beta.3", "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", 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 6500407750..00d6451fa0 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 @@ -179,8 +179,8 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { sourceFile: sourceDataResourceFile, projectFile: dataResourceFile, deployThresholdSec: { - onWindows: 135, - onOther: 125, + onWindows: 40, + onOther: 30, }, }, ]; 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/update-1/data/resource.ts index 4bd805c2b0..dcb1dcaa79 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/update-1/data/resource.ts @@ -20,6 +20,19 @@ 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() }) From fbf2d2648f309a8debb0ed77c015b1ae9129d2a2 Mon Sep 17 00:00:00 2001 From: Amplifiyer <51211245+Amplifiyer@users.noreply.github.com> Date: Wed, 6 Mar 2024 18:57:56 +0100 Subject: [PATCH 15/41] chore: Try moving to 4 core CPUs for github workflows (#1103) * chore: Try moving to 4 core CPUs for github workflows * lint * Try actions@3.3.3 * revert windows image * revert cache actions upgrade --- .github/workflows/health_checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/health_checks.yml b/.github/workflows/health_checks.yml index 8028b41fe2..aa40e5f2cd 100644 --- a/.github/workflows/health_checks.yml +++ b/.github/workflows/health_checks.yml @@ -133,7 +133,7 @@ jobs: matrix: os: [ - amplify-backend_ubuntu-latest_4-core, + ubuntu-latest, macos-latest-xl, amplify-backend_windows-latest_8-core, ] From 268acd88a0fb1e8b67ccba2c8f0f8bb8e0685464 Mon Sep 17 00:00:00 2001 From: Zeyu Li Date: Wed, 6 Mar 2024 10:37:46 -0800 Subject: [PATCH 16/41] feat(data): enable destructive schema updates for all deployments and enable table replacement upon GSI updates in sandbox (#991) * feat(data): enable destructive schema updates in sandbox * update changeset * feat: always allow desctructive update & add replace table for gsi update in env * fix lint * always allow gsi update replacing table in sandbox & unit tests * fix lint * rm sandbox in changeset --- .changeset/lucky-tigers-carry.md | 5 + packages/backend-data/src/factory.test.ts | 153 ++++++++++++++++------ packages/backend-data/src/factory.ts | 49 ++++++- 3 files changed, 161 insertions(+), 46 deletions(-) create mode 100644 .changeset/lucky-tigers-carry.md 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/packages/backend-data/src/factory.test.ts b/packages/backend-data/src/factory.test.ts index 606b9ce256..42edb7266d 100644 --- a/packages/backend-data/src/factory.test.ts +++ b/packages/backend-data/src/factory.test.ts @@ -33,6 +33,8 @@ import { import { AmplifyDataResources } from '@aws-amplify/data-construct'; import { AmplifyUserError } from '@aws-amplify/platform-core'; +const CUSTOM_DDB_CFN_TYPE = 'Custom::AmplifyDynamoDBTable'; + const testSchema = /* GraphQL */ ` type Todo @model { id: ID! @@ -41,15 +43,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 +129,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 ); @@ -260,6 +290,43 @@ void describe('DataFactory', () => { }); }); +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 = () => { DataFactory.factoryCount = 0; }; diff --git a/packages/backend-data/src/factory.ts b/packages/backend-data/src/factory.ts index b16d753d80..6170d68be0 100644 --- a/packages/backend-data/src/factory.ts +++ b/packages/backend-data/src/factory.ts @@ -1,3 +1,4 @@ +import { IConstruct } from 'constructs'; import { AuthResources, BackendOutputStorageStrategy, @@ -7,7 +8,11 @@ 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'; @@ -24,7 +29,8 @@ 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'; /** @@ -171,15 +177,52 @@ class DataGenerator implements ConstructContainerEntryGenerator { 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); 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 */ From bdbf6e88ffe0d9cff522c4fbfe25e811247990d2 Mon Sep 17 00:00:00 2001 From: Roshane Pascual Date: Wed, 6 Mar 2024 10:56:47 -0800 Subject: [PATCH 17/41] set default function memory to 512 (#1105) --- .changeset/brave-shirts-push.md | 5 +++++ packages/backend-function/src/factory.test.ts | 11 +++++++++++ packages/backend-function/src/factory.ts | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 .changeset/brave-shirts-push.md 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/packages/backend-function/src/factory.test.ts b/packages/backend-function/src/factory.test.ts index 3b08d3f63b..9bb0b499f9 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( () => diff --git a/packages/backend-function/src/factory.ts b/packages/backend-function/src/factory.ts index b2821cb874..85671d4e81 100644 --- a/packages/backend-function/src/factory.ts +++ b/packages/backend-function/src/factory.ts @@ -176,7 +176,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; } From f99989714a1a373e5f26ceb11298637a5e49741c Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Wed, 6 Mar 2024 13:44:58 -0800 Subject: [PATCH 18/41] Enable user pool group access to storage and modify owner access syntax (#1104) --- .changeset/spicy-bulldogs-itch.md | 6 ++ packages/backend-auth/API.md | 2 +- packages/backend-auth/src/factory.ts | 27 ++++++-- packages/backend-storage/API.md | 8 ++- .../src/access_builder.test.ts | 37 +++++++---- .../backend-storage/src/access_builder.ts | 28 +++++--- packages/backend-storage/src/constants.ts | 2 +- .../src/storage_access_orchestrator.test.ts | 64 +++++++++---------- .../src/storage_access_orchestrator.ts | 6 +- packages/backend-storage/src/types.ts | 14 +++- .../src/validate_storage_access_paths.test.ts | 35 +++++----- .../src/validate_storage_access_paths.ts | 24 +++---- packages/plugin-types/API.md | 4 +- .../src/resource_access_acceptor.ts | 8 +-- 14 files changed, 165 insertions(+), 100 deletions(-) create mode 100644 .changeset/spicy-bulldogs-itch.md 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/packages/backend-auth/API.md b/packages/backend-auth/API.md index 9bdc2e1b6d..dad83e023c 100644 --- a/packages/backend-auth/API.md +++ b/packages/backend-auth/API.md @@ -46,7 +46,7 @@ export type AuthLoginWithFactoryProps = Omit & ResourceAccessAcceptorFactory; +export type BackendAuth = ResourceProvider & ResourceAccessAcceptorFactory; // @public export const defineAuth: (props: AmplifyAuthProps) => ConstructFactory; diff --git a/packages/backend-auth/src/factory.ts b/packages/backend-auth/src/factory.ts index 529fb7eb17..ffd8860bbb 100644 --- a/packages/backend-auth/src/factory.ts +++ b/packages/backend-auth/src/factory.ts @@ -23,7 +23,7 @@ import { UserPool, UserPoolOperation } from 'aws-cdk-lib/aws-cognito'; import { AmplifyUserError } from '@aws-amplify/platform-core'; export type BackendAuth = ResourceProvider & - ResourceAccessAcceptorFactory; + ResourceAccessAcceptorFactory; export type AmplifyAuthProps = Expand< Omit & { @@ -126,12 +126,24 @@ 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); }, }), @@ -140,6 +152,13 @@ class AmplifyAuthGenerator implements ConstructContainerEntryGenerator { }; } +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-storage/API.md b/packages/backend-storage/API.md index 786dbb0a52..6f0a314139 100644 --- a/packages/backend-storage/API.md +++ b/packages/backend-storage/API.md @@ -33,11 +33,15 @@ export type AmplifyStorageTriggerEvent = 'onDelete' | 'onUpload'; // @public export const defineStorage: (props: AmplifyStorageFactoryProps) => ConstructFactory>; +// @public +export type EntityId = 'identity'; + // @public export type StorageAccessBuilder = { authenticated: StorageActionBuilder; guest: StorageActionBuilder; - owner: StorageActionBuilder; + group: (groupName: string) => StorageActionBuilder; + entity: (entityId: EntityId) => StorageActionBuilder; resource: (other: ConstructFactory) => StorageActionBuilder; }; @@ -45,7 +49,7 @@ export type StorageAccessBuilder = { export type StorageAccessDefinition = { getResourceAccessAcceptor: (getInstanceProps: ConstructFactoryGetInstanceProps) => ResourceAccessAcceptor; actions: StorageAction[]; - ownerPlaceholderSubstitution: string; + idSubstitution: string; }; // @public (undocumented) diff --git a/packages/backend-storage/src/access_builder.test.ts b/packages/backend-storage/src/access_builder.test.ts index 5b616bca01..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,19 +93,17 @@ 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, + accessDefinition.idSubstitution, '${cognito-identity.amazonaws.com:sub}' ); assert.equal( @@ -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 0035f8d3eb..ebadabef05 100644 --- a/packages/backend-storage/src/access_builder.ts +++ b/packages/backend-storage/src/access_builder.ts @@ -11,29 +11,37 @@ export const roleAccessBuilder: StorageAccessBuilder = { 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.amazonaws.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/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/storage_access_orchestrator.test.ts b/packages/backend-storage/src/storage_access_orchestrator.test.ts index d0c1ecf7c6..7e4456e65f 100644 --- a/packages/backend-storage/src/storage_access_orchestrator.test.ts +++ b/packages/backend-storage/src/storage_access_orchestrator.test.ts @@ -4,7 +4,7 @@ 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 { ownerPathPartToken } from './constants.js'; +import { entityIdPathToken } from './constants.js'; import { StorageAccessPolicyFactory } from './storage_access_policy_factory.js'; void describe('StorageAccessOrchestrator', () => { @@ -35,7 +35,7 @@ void describe('StorageAccessOrchestrator', () => { identifier: 'testResourceAccessAcceptor', acceptResourceAccess: acceptResourceAccessMock, }), - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, ], }), @@ -64,7 +64,7 @@ void describe('StorageAccessOrchestrator', () => { identifier: 'testResourceAccessAcceptor', acceptResourceAccess: acceptResourceAccessMock, }), - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, ], }), @@ -111,14 +111,14 @@ void describe('StorageAccessOrchestrator', () => { { actions: ['get', 'write', 'delete'], getResourceAccessAcceptor: getResourceAccessAcceptorStub, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, ], '/another/prefix/*': [ { actions: ['get'], getResourceAccessAcceptor: getResourceAccessAcceptorStub, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, ], }), @@ -178,19 +178,19 @@ void describe('StorageAccessOrchestrator', () => { { actions: ['get', 'write', 'delete'], getResourceAccessAcceptor: getResourceAccessAcceptorStub1, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, { actions: ['get'], getResourceAccessAcceptor: getResourceAccessAcceptorStub2, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, ], '/another/prefix/*': [ { actions: ['get', 'delete'], getResourceAccessAcceptor: getResourceAccessAcceptorStub2, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, ], }), @@ -260,14 +260,14 @@ void describe('StorageAccessOrchestrator', () => { const acceptResourceAccessMock = mock.fn(); const storageAccessOrchestrator = new StorageAccessOrchestrator( () => ({ - [`/test/${ownerPathPartToken}/*`]: [ + [`/test/${entityIdPathToken}/*`]: [ { actions: ['get', 'write'], getResourceAccessAcceptor: () => ({ identifier: 'testResourceAccessAcceptor', acceptResourceAccess: acceptResourceAccessMock, }), - ownerPlaceholderSubstitution: '{testOwnerSub}', + idSubstitution: '{testOwnerSub}', }, ], }), @@ -314,7 +314,7 @@ void describe('StorageAccessOrchestrator', () => { identifier: 'resourceAccessAcceptor1', acceptResourceAccess: acceptResourceAccessMock1, }), - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, ], '/foo/bar/*': [ @@ -324,7 +324,7 @@ void describe('StorageAccessOrchestrator', () => { identifier: 'resourceAccessAcceptor2', acceptResourceAccess: acceptResourceAccessMock2, }), - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, ], }), @@ -393,16 +393,16 @@ void describe('StorageAccessOrchestrator', () => { const storageAccessOrchestrator = new StorageAccessOrchestrator( () => ({ - '/foo/{owner}/*': [ + '/foo/{entity_id}/*': [ { actions: ['write', 'delete'], getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, - ownerPlaceholderSubstitution: '{ownerSub}', + idSubstitution: '{idSub}', }, { actions: ['get'], getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, ], }), @@ -420,12 +420,12 @@ void describe('StorageAccessOrchestrator', () => { { Action: 's3:PutObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/foo/{ownerSub}/*`, + Resource: `${bucket.bucketArn}/foo/{idSub}/*`, }, { Action: 's3:DeleteObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/foo/{ownerSub}/*`, + Resource: `${bucket.bucketArn}/foo/{idSub}/*`, }, { Action: 's3:GetObject', @@ -462,7 +462,7 @@ void describe('StorageAccessOrchestrator', () => { { actions: ['get', 'write'], getResourceAccessAcceptor: getResourceAccessAcceptorStub1, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, ], // acceptor1 should be denied read and write on this path @@ -471,7 +471,7 @@ void describe('StorageAccessOrchestrator', () => { { actions: ['get'], getResourceAccessAcceptor: getResourceAccessAcceptorStub2, - ownerPlaceholderSubstitution: '{ownerSub}', + idSubstitution: '{idSub}', }, ], // acceptor1 should be denied write on this path (read from parent path covers read on this path) @@ -479,22 +479,22 @@ void describe('StorageAccessOrchestrator', () => { '/foo/baz/*': [ { actions: ['get'], - ownerPlaceholderSubstitution: '*', + 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/{owner}/*': [ + '/other/{entity_id}/*': [ { actions: ['get', 'write', 'delete'], getResourceAccessAcceptor: getResourceAccessAcceptorStub2, - ownerPlaceholderSubstitution: '{ownerSub}', + idSubstitution: '{idSub}', }, { actions: ['get'], getResourceAccessAcceptor: getResourceAccessAcceptorStub1, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, ], }), @@ -554,18 +554,18 @@ void describe('StorageAccessOrchestrator', () => { Effect: 'Allow', Resource: [ `${bucket.bucketArn}/foo/bar/*`, - `${bucket.bucketArn}/other/{ownerSub}/*`, + `${bucket.bucketArn}/other/{idSub}/*`, ], }, { Action: 's3:PutObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/other/{ownerSub}/*`, + Resource: `${bucket.bucketArn}/other/{idSub}/*`, }, { Action: 's3:DeleteObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/other/{ownerSub}/*`, + Resource: `${bucket.bucketArn}/other/{idSub}/*`, }, ], Version: '2012-10-17', @@ -586,17 +586,17 @@ void describe('StorageAccessOrchestrator', () => { { actions: ['get'], getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, { actions: ['write'], getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, - ownerPlaceholderSubstitution: '{ownerSub}', + idSubstitution: '{idSub}', }, { actions: ['delete'], getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, ], }), @@ -649,19 +649,19 @@ void describe('StorageAccessOrchestrator', () => { { actions: ['read', 'get', 'list'], getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, { actions: ['list'], getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, ], '/other/baz/*': [ { actions: ['read'], getResourceAccessAcceptor: authenticatedResourceAccessAcceptor, - ownerPlaceholderSubstitution: '*', + idSubstitution: '*', }, ], }), diff --git a/packages/backend-storage/src/storage_access_orchestrator.ts b/packages/backend-storage/src/storage_access_orchestrator.ts index b3494bce04..c1dccaccb0 100644 --- a/packages/backend-storage/src/storage_access_orchestrator.ts +++ b/packages/backend-storage/src/storage_access_orchestrator.ts @@ -8,7 +8,7 @@ import { StorageAccessGenerator, StoragePath, } from './types.js'; -import { ownerPathPartToken } from './constants.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'; @@ -100,8 +100,8 @@ export class StorageAccessOrchestrator { // make the owner placeholder substitution in the s3 prefix const prefix = s3Prefix.replaceAll( - ownerPathPartToken, - permission.ownerPlaceholderSubstitution + entityIdPathToken, + permission.idSubstitution ) as StoragePath; // replace "read" with "get" and "list" in actions diff --git a/packages/backend-storage/src/types.ts b/packages/backend-storage/src/types.ts index 871f71146a..c7a5f8fc1b 100644 --- a/packages/backend-storage/src/types.ts +++ b/packages/backend-storage/src/types.ts @@ -20,6 +20,15 @@ export type AmplifyStorageFactoryProps = Omit< 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! * @@ -29,7 +38,8 @@ export type AmplifyStorageFactoryProps = Omit< export type StorageAccessBuilder = { authenticated: StorageActionBuilder; guest: StorageActionBuilder; - owner: StorageActionBuilder; + group: (groupName: string) => StorageActionBuilder; + entity: (entityId: EntityId) => StorageActionBuilder; resource: ( other: ConstructFactory ) => StorageActionBuilder; @@ -59,7 +69,7 @@ export type StorageAccessDefinition = { /** * The value that will be substituted into the resource string in place of the {owner} token */ - ownerPlaceholderSubstitution: string; + idSubstitution: 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 7bb9794cbc..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 }); @@ -55,42 +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 that is a prefix of a path with an owner token', () => { assert.throws( - () => validateStorageAccessPaths(['/foo/{owner}/*', '/foo/*']), + () => validateStorageAccessPaths(['/foo/{entity_id}/*', '/foo/*']), { - message: `A path cannot be a prefix of another path that contains the ${ownerPathPartToken} token.`, + message: `A path cannot be a prefix of another path that contains the ${entityIdPathToken} token.`, } ); assert.throws( - () => validateStorageAccessPaths(['/foo/bar/{owner}/*', '/foo/*']), + () => validateStorageAccessPaths(['/foo/bar/{entity_id}/*', '/foo/*']), { - message: `A path cannot be a prefix of another path that contains the ${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 032f772e67..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'; /** @@ -65,13 +65,13 @@ const validateStoragePath = ( */ const validateOwnerTokenRules = (path: string, otherPrefixes: string[]) => { // if there's no owner token in the path, this validation is a noop - if (!path.includes(ownerPathPartToken)) { + 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 ${ownerPathPartToken} token.`, + message: `A path cannot be a prefix of another path that contains the ${entityIdPathToken} token.`, details: `Found [${path}] which has prefixes [${otherPrefixes.join( ', ' )}].`, @@ -79,12 +79,12 @@ const validateOwnerTokenRules = (path: string, otherPrefixes: string[]) => { }); } - const ownerSplit = path.split(ownerPathPartToken); + 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`, }); } @@ -92,22 +92,22 @@ const validateOwnerTokenRules = (path: string, otherPrefixes: 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}/*"`, + 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}/*"`, }); } }; 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; }; From 7f5edeef7e95b733c0186ccc651775834d818635 Mon Sep 17 00:00:00 2001 From: Roshane Pascual Date: Thu, 7 Mar 2024 09:31:18 -0800 Subject: [PATCH 19/41] ensure typed shim files contain only the function name (#1108) * ensure typed shim files contain only the function name * change check to onDelete * refactor FunctionEnvironmentTypeGenerator * small nit --- .changeset/smart-crews-serve.md | 6 ++++++ packages/backend-function/src/factory.ts | 4 +++- .../src/function_env_translator.test.ts | 20 +++++++++++++------ .../src/function_env_translator.ts | 8 ++++---- .../src/function_env_type_generator.test.ts | 10 +++++----- .../src/function_env_type_generator.ts | 9 +++------ .../data_storage_auth_with_triggers.ts | 7 +++++++ 7 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 .changeset/smart-crews-serve.md 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/packages/backend-function/src/factory.ts b/packages/backend-function/src/factory.ts index 85671d4e81..2f38ff8eb5 100644 --- a/packages/backend-function/src/factory.ts +++ b/packages/backend-function/src/factory.ts @@ -26,6 +26,7 @@ import { FunctionOutput, functionOutputKey, } from '@aws-amplify/backend-output-schemas'; +import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js'; /** * Entry point for defining a function in the Amplify ecosystem @@ -292,7 +293,8 @@ class AmplifyFunction this.functionEnvironmentTranslator = new FunctionEnvironmentTranslator( functionLambda, props.environment, - backendSecretResolver + backendSecretResolver, + new FunctionEnvironmentTypeGenerator(id) ); this.resources = { 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 4ffd458f7a..5e204b1f39 100644 --- a/packages/backend-function/src/function_env_translator.ts +++ b/packages/backend-function/src/function_env_translator.ts @@ -26,7 +26,8 @@ export class FunctionEnvironmentTranslator { 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) { @@ -86,10 +87,9 @@ export class FunctionEnvironmentTranslator { // 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[] => { - new FunctionEnvironmentTypeGenerator( - this.lambda.node.id, + this.functionEnvironmentTypeGenerator.generateTypedProcessEnvShim( this.amplifyBackendEnvVarNames - ).generateTypedProcessEnvShim(); + ); return []; }, }); 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 18686aedc7..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,7 +19,7 @@ void describe('FunctionEnvironmentTypeGenerator', () => { new FunctionEnvironmentTypeGenerator('testFunction'); const sampleStaticEnv = '_HANDLER: string;'; - functionEnvironmentTypeGenerator.generateTypedProcessEnvShim(); + functionEnvironmentTypeGenerator.generateTypedProcessEnvShim([]); // assert type definition file path assert.equal( @@ -46,10 +46,10 @@ void describe('FunctionEnvironmentTypeGenerator', () => { }; }); const functionEnvironmentTypeGenerator = - new FunctionEnvironmentTypeGenerator('testFunction', ['TEST_ENV']); + new FunctionEnvironmentTypeGenerator('testFunction'); const sampleStaticEnv = 'TEST_ENV: string;'; - functionEnvironmentTypeGenerator.generateTypedProcessEnvShim(); + functionEnvironmentTypeGenerator.generateTypedProcessEnvShim(['TEST_ENV']); // assert type definition file path assert.equal( @@ -69,10 +69,10 @@ void describe('FunctionEnvironmentTypeGenerator', () => { void it('generated type definition file has valid syntax', async () => { const targetDirectory = await fsp.mkdtemp('func_env_type_gen_test'); const functionEnvironmentTypeGenerator = - new FunctionEnvironmentTypeGenerator('testFunction', ['TEST_ENV']); + new FunctionEnvironmentTypeGenerator('testFunction'); const filePath = `${process.cwd()}/.amplify/function-env/testFunction.ts`; - functionEnvironmentTypeGenerator.generateTypedProcessEnvShim(); + 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 3a9e243dcd..ba3bfb0d68 100644 --- a/packages/backend-function/src/function_env_type_generator.ts +++ b/packages/backend-function/src/function_env_type_generator.ts @@ -12,10 +12,7 @@ export class FunctionEnvironmentTypeGenerator { /** * Initialize typed process.env shim file name and location */ - constructor( - private readonly functionName: string, - private readonly amplifyBackendEnvVars: string[] = [] - ) { + constructor(private readonly functionName: string) { this.typeDefFilePath = `${process.cwd()}/.amplify/function-env/${ this.functionName }.ts`; @@ -24,7 +21,7 @@ export class FunctionEnvironmentTypeGenerator { /** * Generate a typed process.env shim */ - generateTypedProcessEnvShim() { + generateTypedProcessEnvShim(amplifyBackendEnvVars: string[]) { const lambdaEnvVarTypeName = 'LambdaProvidedEnvVars'; const amplifyBackendEnvVarTypeName = 'AmplifyBackendEnvVars'; @@ -57,7 +54,7 @@ export class FunctionEnvironmentTypeGenerator { `/** Amplify backend environment variables available at runtime, this includes environment variables defined in \`defineFunction\` and by cross resource mechanisms */` ); declarations.push(`type ${amplifyBackendEnvVarTypeName} = {`); - this.amplifyBackendEnvVars.forEach((envName) => { + amplifyBackendEnvVars.forEach((envName) => { const declaration = `${envName}: string;`; declarations.push(declaration); 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 00d6451fa0..4111fab905 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 @@ -236,6 +236,13 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { 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 setUpDeployEnvironment = async ( From 615a3e62694a6e3048fa0edf59c8012b424c6835 Mon Sep 17 00:00:00 2001 From: MJ Zhang <0618@users.noreply.github.com> Date: Thu, 7 Mar 2024 11:42:05 -0800 Subject: [PATCH 20/41] chore: upgrade @parcel/watcher to use the latest version (#1113) --- .changeset/proud-bags-dream.md | 5 + package-lock.json | 315 +++++++++++++++++++++++++++++---- packages/sandbox/package.json | 4 +- 3 files changed, 287 insertions(+), 37 deletions(-) create mode 100644 .changeset/proud-bags-dream.md 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/package-lock.json b/package-lock.json index cfca546d09..38dfd142ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10323,6 +10323,266 @@ "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/@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/@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": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -14661,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", @@ -19160,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", @@ -19195,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", @@ -24452,7 +24721,7 @@ "@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", @@ -24465,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/sandbox/package.json b/packages/sandbox/package.json index eb9473a1d6..d96d593bd8 100644 --- a/packages/sandbox/package.json +++ b/packages/sandbox/package.json @@ -24,10 +24,10 @@ "@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", - "@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", From ab05ae09be55685fe474eeead32ef229b6f43568 Mon Sep 17 00:00:00 2001 From: Charles Shin Date: Thu, 7 Mar 2024 11:56:15 -0800 Subject: [PATCH 21/41] feat: attach policy & ssm params to access userpool from auth resource (#1090) * feat: attach policy & ssm params to acces userpool from auth resource * remove action scope, introduce meta actions and granular iam action * update type naming to match other access pattern * Update packages/backend-auth/src/access_builder.ts Co-authored-by: Amplifiyer <51211245+Amplifiyer@users.noreply.github.com> * fix comments * fix name * update jsdoc * rename to manageUsers * addressing nit * update permission mapping * update API.md * removed type enforcement between meta & iam actions, updated list * use flatmap --------- Co-authored-by: Amplifiyer <51211245+Amplifiyer@users.noreply.github.com> --- .changeset/good-wasps-sin.md | 5 + packages/backend-auth/API.md | 31 ++++ .../backend-auth/src/access_builder.test.ts | 57 ++++++++ packages/backend-auth/src/access_builder.ts | 11 ++ .../src/auth_access_policy_arbiter.test.ts | 132 ++++++++++++++++++ .../src/auth_access_policy_arbiter.ts | 58 ++++++++ packages/backend-auth/src/factory.test.ts | 63 +++++++++ packages/backend-auth/src/factory.ts | 52 ++++++- packages/backend-auth/src/types.ts | 66 ++++++++- .../userpool_access_policy_factory.test.ts | 65 +++++++++ .../src/userpool_access_policy_factory.ts | 109 +++++++++++++++ 11 files changed, 642 insertions(+), 7 deletions(-) create mode 100644 .changeset/good-wasps-sin.md create mode 100644 packages/backend-auth/src/access_builder.test.ts create mode 100644 packages/backend-auth/src/access_builder.ts create mode 100644 packages/backend-auth/src/auth_access_policy_arbiter.test.ts create mode 100644 packages/backend-auth/src/auth_access_policy_arbiter.ts create mode 100644 packages/backend-auth/src/userpool_access_policy_factory.test.ts create mode 100644 packages/backend-auth/src/userpool_access_policy_factory.ts 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/packages/backend-auth/API.md b/packages/backend-auth/API.md index dad83e023c..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,6 +49,28 @@ 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; 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 ffd8860bbb..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,12 +19,15 @@ 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; @@ -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, @@ -148,6 +165,29 @@ class AmplifyAuthGenerator implements ConstructContainerEntryGenerator { }, }), }; + 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; }; } 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..6d5651a478 --- /dev/null +++ b/packages/backend-auth/src/userpool_access_policy_factory.ts @@ -0,0 +1,109 @@ +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', + }); + } + + 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'], +}; From 9d42ac12eac04435c63c4f3e8abaa9a90671f444 Mon Sep 17 00:00:00 2001 From: Amplifiyer <51211245+Amplifiyer@users.noreply.github.com> Date: Fri, 8 Mar 2024 13:55:56 +0100 Subject: [PATCH 22/41] chore: use 4 core windows runner for github actions (#1106) * chore: use 4 core windows runner for github actions * revert cache actions upgrade * Update health_checks.yml * lint --- .github/actions/install_with_cache/action.yml | 2 +- .github/workflows/health_checks.yml | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) 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 aa40e5f2cd..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: - [ - ubuntu-latest, - 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,7 +166,7 @@ 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] env: From a7774887b79cc4e35544589161f5dcfd4f40bad9 Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Fri, 8 Mar 2024 12:14:04 -0800 Subject: [PATCH 23/41] enable configuring functions to access GraphQL API in `defineData` (#1116) --- .changeset/breezy-eyes-appear.md | 5 + package-lock.json | 20 +- packages/backend-data/package.json | 4 +- .../src/app_sync_policy_generator.test.ts | 147 ++++++++ .../src/app_sync_policy_generator.ts | 47 +++ .../src/convert_authorization_modes.test.ts | 28 +- .../src/convert_authorization_modes.ts | 33 +- .../src/convert_functions.test.ts | 91 +---- .../backend-data/src/convert_functions.ts | 21 +- packages/backend-data/src/factory.test.ts | 318 +++++++++++++++++- packages/backend-data/src/factory.ts | 98 ++++-- packages/backend/package.json | 2 +- packages/integration-tests/package.json | 2 +- 13 files changed, 645 insertions(+), 171 deletions(-) create mode 100644 .changeset/breezy-eyes-appear.md create mode 100644 packages/backend-data/src/app_sync_policy_generator.test.ts create mode 100644 packages/backend-data/src/app_sync_policy_generator.ts 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/package-lock.json b/package-lock.json index 38dfd142ab..c67359a509 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1244,17 +1244,17 @@ } }, "node_modules/@aws-amplify/data-schema": { - "version": "0.13.8", - "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema/-/data-schema-0.13.8.tgz", - "integrity": "sha512-167euk2z2QGC25wCUMMREVY7FKDmgK9FOfQVt3Oz1hIy1awL2PFPPbad+8Egf989a+gtGWbNdx3zrx97XLu4vw==", + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema/-/data-schema-0.13.11.tgz", + "integrity": "sha512-9BD+Qun3ve4Z4F7c1Ri6GrarEdEuCw+OjxFgaSouSZsouJ8a1+oVvMoqeU8Sq4+WvQJfgl8WzloQYGSGHzsznw==", "dependencies": { "@aws-amplify/data-schema-types": "*" } }, "node_modules/@aws-amplify/data-schema-types": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema-types/-/data-schema-types-0.7.6.tgz", - "integrity": "sha512-g5LmvLEJ5BDtfxRT6QK/+HhTfGGADbF7gbxtWPbhP9hRYwDai0A9ARJ6qETLPzUE/1vXtURlJV+MLutsDrIkPA==", + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema-types/-/data-schema-types-0.7.7.tgz", + "integrity": "sha512-DjHeJ2dfTPTG+MjkguTcKTy20OlP4DOo1AORPSz23Yl4W+0P2DfG03VlT6o9xc9jWqreQ3oJGkDsO/2oR81KsA==", "dependencies": { "rxjs": "^7.8.1" } @@ -23613,7 +23613,7 @@ "@aws-amplify/backend-secret": "^0.4.5-beta.1", "@aws-amplify/backend-storage": "^0.6.0-beta.2", "@aws-amplify/client-config": "^0.9.0-beta.3", - "@aws-amplify/data-schema": "^0.13.8", + "@aws-amplify/data-schema": "^0.13.11", "@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" @@ -23653,12 +23653,12 @@ "@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.6", + "@aws-amplify/data-schema-types": "^0.7.7", "@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.8", + "@aws-amplify/data-schema": "^0.13.11", "@aws-amplify/platform-core": "^0.5.0-beta.1" }, "peerDependencies": { @@ -24128,7 +24128,7 @@ "@aws-amplify/backend": "^0.13.0-beta.4", "@aws-amplify/backend-secret": "^0.4.5-beta.1", "@aws-amplify/client-config": "^0.9.0-beta.3", - "@aws-amplify/data-schema": "^0.13.8", + "@aws-amplify/data-schema": "^0.13.11", "@aws-amplify/platform-core": "^0.5.0-beta.1", "@aws-sdk/client-amplify": "^3.465.0", "@aws-sdk/client-cloudformation": "^3.465.0", diff --git a/packages/backend-data/package.json b/packages/backend-data/package.json index fa392ac063..0d2fffd044 100644 --- a/packages/backend-data/package.json +++ b/packages/backend-data/package.json @@ -18,7 +18,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-amplify/data-schema": "^0.13.8", + "@aws-amplify/data-schema": "^0.13.11", "@aws-amplify/backend-platform-test-stubs": "^0.3.3-beta.0", "@aws-amplify/platform-core": "^0.5.0-beta.1" }, @@ -31,6 +31,6 @@ "@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.6" + "@aws-amplify/data-schema-types": "^0.7.7" } } 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/convert_authorization_modes.test.ts b/packages/backend-data/src/convert_authorization_modes.test.ts index a50668c9b7..161d93f0fd 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,7 +209,7 @@ void describe('convertAuthorizationModesToCDK', () => { assert.deepStrictEqual( convertAuthorizationModesToCDK( - functionInstanceProvider, + getInstancePropsStub, undefined, authModes ), @@ -240,7 +234,7 @@ void describe('convertAuthorizationModesToCDK', () => { assert.deepStrictEqual( convertAuthorizationModesToCDK( - functionInstanceProvider, + getInstancePropsStub, providedAuthConfig, authModes ), @@ -254,7 +248,7 @@ void describe('convertAuthorizationModesToCDK', () => { }; const convertedOutput = convertAuthorizationModesToCDK( - functionInstanceProvider, + getInstancePropsStub, providedAuthConfig, authModes ); diff --git a/packages/backend-data/src/convert_authorization_modes.ts b/packages/backend-data/src/convert_authorization_modes.ts index b0e7e64741..4c13126b9c 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,19 @@ const computeUserPoolAuthFromResource = ( */ const computeIAMAuthFromResource = ( providedAuthConfig: ProvidedAuthConfig | undefined, - authModes: AuthorizationModes | undefined + authModes: AuthorizationModes | undefined, + additionalRoles: IRole[] = [] ): CDKIAMAuthorizationConfig | undefined => { if (providedAuthConfig) { + const allowListedRoles = [ + ...(authModes?.allowListedRoleNames || []), + ...additionalRoles, + ]; return { authenticatedUserRole: providedAuthConfig.authenticatedUserRole, unauthenticatedUserRole: providedAuthConfig.unauthenticatedUserRole, identityPoolId: providedAuthConfig.identityPoolId, - allowListedRoles: authModes?.allowListedRoleNames ?? [], + allowListedRoles, }; } return; @@ -168,9 +176,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 +191,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/factory.test.ts b/packages/backend-data/src/factory.test.ts index 42edb7266d..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,7 @@ 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'; @@ -288,6 +292,318 @@ 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', () => { diff --git a/packages/backend-data/src/factory.ts b/packages/backend-data/src/factory.ts index 6170d68be0..e2406b47c0 100644 --- a/packages/backend-data/src/factory.ts +++ b/packages/backend-data/src/factory.ts @@ -17,11 +17,7 @@ import { GraphqlOutput } from '@aws-amplify/backend-output-schemas'; import * as path from 'path'; import { AmplifyDataError, DataProps } from './types.js'; import { convertSchemaToCDK, isModelSchema } from './convert_schema.js'; -import { - FunctionInstanceProvider, - buildConstructFactoryFunctionInstanceProvider, - convertFunctionNameMapToCDK, -} from './convert_functions.js'; +import { convertFunctionNameMapToCDK } from './convert_functions.js'; import { ProvidedAuthConfig, buildConstructFactoryProvidedAuthConfig, @@ -32,6 +28,11 @@ import { validateAuthorizationModes } from './validate_authorization_modes.js'; 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. @@ -82,7 +83,7 @@ export class DataFactory implements ConstructFactory { ) ?.getInstance(props) ), - buildConstructFactoryFunctionInstanceProvider(props), + props, outputStorageStrategy ); } @@ -97,18 +98,52 @@ 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[] = []; + try { + if (isModelSchema(this.props.schema)) { + ({ jsFunctions, functionSchemaAccess } = this.props.schema.transform()); + } + 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 + ); + } + 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( @@ -147,30 +182,9 @@ class DataGenerator implements ConstructContainerEntryGenerator { ); const functionNameMap = convertFunctionNameMapToCDK( - this.functionInstanceProvider, + this.getInstanceProps, this.props.functions ?? {} ); - - let amplifyGraphqlDefinition; - let jsFunctions; - try { - if (isModelSchema(this.props.schema)) { - ({ jsFunctions } = this.props.schema.transform()); - } - 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 - ); - } - const amplifyApi = new AmplifyData(scope, this.defaultName, { apiName: this.props.name, definition: amplifyGraphqlDefinition, @@ -199,6 +213,26 @@ class DataGenerator implements ConstructContainerEntryGenerator { 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; }; } diff --git a/packages/backend/package.json b/packages/backend/package.json index 778ee21853..1e41fd4333 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -21,7 +21,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/data-schema": "^0.13.8", + "@aws-amplify/data-schema": "^0.13.11", "@aws-amplify/backend-auth": "^0.5.0-beta.3", "@aws-amplify/backend-function": "^0.8.0-beta.2", "@aws-amplify/backend-data": "^0.10.0-beta.3", diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index baa2607c9b..da69afe101 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -8,7 +8,7 @@ "@aws-amplify/backend": "^0.13.0-beta.4", "@aws-amplify/backend-secret": "^0.4.5-beta.1", "@aws-amplify/client-config": "^0.9.0-beta.3", - "@aws-amplify/data-schema": "^0.13.8", + "@aws-amplify/data-schema": "^0.13.11", "@aws-amplify/platform-core": "^0.5.0-beta.1", "@aws-sdk/client-amplify": "^3.465.0", "@aws-sdk/client-cloudformation": "^3.465.0", From 1d444df68cce9db7e2b03d0ffc2d34d7d16e347d Mon Sep 17 00:00:00 2001 From: awsluja <110861985+awsluja@users.noreply.github.com> Date: Fri, 8 Mar 2024 13:44:21 -0800 Subject: [PATCH 24/41] fix: SAML deployments (#1075) * fix: IAM arns should not include region * chore: add changeset --- .changeset/odd-shirts-collect.md | 5 + packages/auth-construct/src/construct.test.ts | 144 +----------------- packages/auth-construct/src/construct.ts | 25 --- 3 files changed, 6 insertions(+), 168 deletions(-) create mode 100644 .changeset/odd-shirts-collect.md 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/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, From 1791f72abab42d7ab98e2150b72d4ff100bba7ed Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:27:27 -0800 Subject: [PATCH 25/41] Version Packages (beta) (#1107) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 9 +++++++++ packages/auth-construct/CHANGELOG.md | 6 ++++++ packages/auth-construct/package.json | 2 +- packages/backend-auth/CHANGELOG.md | 12 ++++++++++++ packages/backend-auth/package.json | 4 ++-- packages/backend-data/CHANGELOG.md | 7 +++++++ packages/backend-data/package.json | 2 +- packages/backend-function/CHANGELOG.md | 7 +++++++ packages/backend-function/package.json | 2 +- packages/backend-storage/CHANGELOG.md | 6 ++++++ packages/backend-storage/package.json | 2 +- packages/backend/CHANGELOG.md | 15 +++++++++++++++ packages/backend/package.json | 10 +++++----- packages/cli/CHANGELOG.md | 7 +++++++ packages/cli/package.json | 4 ++-- packages/integration-tests/CHANGELOG.md | 6 ++++++ packages/integration-tests/package.json | 6 +++--- packages/sandbox/CHANGELOG.md | 6 ++++++ packages/sandbox/package.json | 2 +- 19 files changed, 98 insertions(+), 17 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 20dda29fbc..1658164ffb 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -29,15 +29,19 @@ "changesets": [ "brave-carrots-glow", "brave-pets-clean", + "brave-shirts-push", + "breezy-eyes-appear", "brown-otters-smoke", "chatty-icons-mix", "cyan-steaks-repeat", "eighty-rings-pull", + "famous-eels-kiss", "five-fireants-shout", "fluffy-books-dance", "four-donuts-jump", "friendly-kids-lick", "giant-feet-sing", + "good-wasps-sin", "great-timers-invent", "khaki-panthers-grin", "khaki-pants-sniff", @@ -46,16 +50,19 @@ "light-cougars-give", "little-books-press", "loud-sheep-occur", + "lucky-tigers-carry", "lucky-trainers-matter", "mean-frogs-visit", "mighty-experts-compare", "modern-files-arrive", "modern-terms-stare", "new-kings-beg", + "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", @@ -64,9 +71,11 @@ "short-olives-bow", "shy-horses-act", "silver-needles-rush", + "smart-crews-serve", "smooth-penguins-joke", "smooth-tigers-double", "sour-rice-listen", + "spicy-bulldogs-itch", "three-doors-act", "tidy-readers-prove" ] diff --git a/packages/auth-construct/CHANGELOG.md b/packages/auth-construct/CHANGELOG.md index f388c901fa..de701d5c7d 100644 --- a/packages/auth-construct/CHANGELOG.md +++ b/packages/auth-construct/CHANGELOG.md @@ -1,5 +1,11 @@ # @aws-amplify/auth-construct-alpha +## 0.6.0-beta.4 + +### Patch Changes + +- 1d444df: Fix deployment bug with SAML providers. + ## 0.6.0-beta.3 ### Patch Changes diff --git a/packages/auth-construct/package.json b/packages/auth-construct/package.json index 97f0626447..5a9e4edf09 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.3", + "version": "0.6.0-beta.4", "type": "commonjs", "publishConfig": { "access": "public" diff --git a/packages/backend-auth/CHANGELOG.md b/packages/backend-auth/CHANGELOG.md index 4ae77b8d4e..2757c750b6 100644 --- a/packages/backend-auth/CHANGELOG.md +++ b/packages/backend-auth/CHANGELOG.md @@ -1,5 +1,17 @@ # @aws-amplify/backend-auth +## 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 diff --git a/packages/backend-auth/package.json b/packages/backend-auth/package.json index ee4d34c724..82ef225d13 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.3", + "version": "0.5.0-beta.4", "type": "module", "publishConfig": { "access": "public" @@ -18,7 +18,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.3", + "@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" }, diff --git a/packages/backend-data/CHANGELOG.md b/packages/backend-data/CHANGELOG.md index bffd11e857..e13b2f4de3 100644 --- a/packages/backend-data/CHANGELOG.md +++ b/packages/backend-data/CHANGELOG.md @@ -1,5 +1,12 @@ # @aws-amplify/backend-data +## 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 diff --git a/packages/backend-data/package.json b/packages/backend-data/package.json index 0d2fffd044..c3bfb144f2 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.3", + "version": "0.10.0-beta.4", "type": "module", "publishConfig": { "access": "public" diff --git a/packages/backend-function/CHANGELOG.md b/packages/backend-function/CHANGELOG.md index 4f62e22084..62af76a673 100644 --- a/packages/backend-function/CHANGELOG.md +++ b/packages/backend-function/CHANGELOG.md @@ -1,5 +1,12 @@ # @aws-amplify/backend-function +## 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 diff --git a/packages/backend-function/package.json b/packages/backend-function/package.json index 15e14967cb..10d86e874e 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.2", + "version": "0.8.0-beta.3", "type": "module", "publishConfig": { "access": "public" diff --git a/packages/backend-storage/CHANGELOG.md b/packages/backend-storage/CHANGELOG.md index 32efdba0c1..d900dfc312 100644 --- a/packages/backend-storage/CHANGELOG.md +++ b/packages/backend-storage/CHANGELOG.md @@ -1,5 +1,11 @@ # @aws-amplify/backend-storage +## 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 diff --git a/packages/backend-storage/package.json b/packages/backend-storage/package.json index e7d5dea314..7edf2e8031 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.2", + "version": "0.6.0-beta.3", "type": "module", "publishConfig": { "access": "public" diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index f594dec1a4..500a97fb7c 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @aws-amplify/backend +## 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 diff --git a/packages/backend/package.json b/packages/backend/package.json index 1e41fd4333..22a19fa98a 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend", - "version": "0.13.0-beta.4", + "version": "0.13.0-beta.5", "type": "module", "publishConfig": { "access": "public" @@ -22,13 +22,13 @@ "license": "Apache-2.0", "dependencies": { "@aws-amplify/data-schema": "^0.13.11", - "@aws-amplify/backend-auth": "^0.5.0-beta.3", - "@aws-amplify/backend-function": "^0.8.0-beta.2", - "@aws-amplify/backend-data": "^0.10.0-beta.3", + "@aws-amplify/backend-auth": "^0.5.0-beta.4", + "@aws-amplify/backend-function": "^0.8.0-beta.3", + "@aws-amplify/backend-data": "^0.10.0-beta.4", "@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.2", + "@aws-amplify/backend-storage": "^0.6.0-beta.3", "@aws-amplify/client-config": "^0.9.0-beta.3", "@aws-amplify/platform-core": "^0.5.0-beta.1", "@aws-amplify/plugin-types": "^0.9.0-beta.0", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index fe72250307..2f1da901f7 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,12 @@ # @aws-amplify/backend-cli +## 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 diff --git a/packages/cli/package.json b/packages/cli/package.json index 6447e95b46..6ea944bad5 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.4", + "version": "0.12.0-beta.5", "description": "Command line interface for various Amplify tools", "bin": { "amplify": "lib/amplify.js" @@ -38,7 +38,7 @@ "@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.3", + "@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", diff --git a/packages/integration-tests/CHANGELOG.md b/packages/integration-tests/CHANGELOG.md index 17e5e8c811..0884352b38 100644 --- a/packages/integration-tests/CHANGELOG.md +++ b/packages/integration-tests/CHANGELOG.md @@ -1,5 +1,11 @@ # @aws-amplify/integration-tests +## 0.5.0-beta.2 + +### Patch Changes + +- 7f5edee: Ensure typed shim files contain only the function name + ## 0.5.0-beta.1 ### Minor Changes diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index da69afe101..c7f31fb562 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -1,11 +1,11 @@ { "name": "@aws-amplify/integration-tests", "private": true, - "version": "0.5.0-beta.1", + "version": "0.5.0-beta.2", "type": "module", "devDependencies": { - "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.3", - "@aws-amplify/backend": "^0.13.0-beta.4", + "@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.9.0-beta.3", "@aws-amplify/data-schema": "^0.13.11", diff --git a/packages/sandbox/CHANGELOG.md b/packages/sandbox/CHANGELOG.md index 4ebc709b58..bbf468aa12 100644 --- a/packages/sandbox/CHANGELOG.md +++ b/packages/sandbox/CHANGELOG.md @@ -1,5 +1,11 @@ # @aws-amplify/sandbox +## 0.5.2-beta.4 + +### Patch Changes + +- 615a3e6: upgrade @parcel/watcher wo use the latest version + ## 0.5.2-beta.3 ### Patch Changes diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json index d96d593bd8..f941222920 100644 --- a/packages/sandbox/package.json +++ b/packages/sandbox/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/sandbox", - "version": "0.5.2-beta.3", + "version": "0.5.2-beta.4", "type": "module", "publishConfig": { "access": "public" From 91dae554eb31b59e2411f3f339182ddb6bf1c2cd Mon Sep 17 00:00:00 2001 From: Kamil Sobol Date: Fri, 8 Mar 2024 15:52:38 -0800 Subject: [PATCH 26/41] remove allowListedRoleNames from defineData (#1120) --- .changeset/famous-glasses-know.md | 5 ++ packages/backend-data/API.md | 1 - .../src/convert_authorization_modes.test.ts | 44 --------------- .../src/convert_authorization_modes.ts | 6 +-- packages/backend-data/src/types.ts | 5 -- .../src/validate_authorization_modes.test.ts | 54 ++++++------------- .../src/validate_authorization_modes.ts | 24 +-------- 7 files changed, 23 insertions(+), 116 deletions(-) create mode 100644 .changeset/famous-glasses-know.md 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/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/src/convert_authorization_modes.test.ts b/packages/backend-data/src/convert_authorization_modes.test.ts index 161d93f0fd..84fd09e87f 100644 --- a/packages/backend-data/src/convert_authorization_modes.test.ts +++ b/packages/backend-data/src/convert_authorization_modes.test.ts @@ -216,50 +216,6 @@ void describe('convertAuthorizationModesToCDK', () => { 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( - getInstancePropsStub, - providedAuthConfig, - authModes - ), - expectedOutput - ); - }); - - void it('allows for specifying allow listed roles roles with values specified', () => { - const authModes: AuthorizationModes = { - allowListedRoleNames: ['MyAdminRole', 'MyQARole'], - }; - - const convertedOutput = convertAuthorizationModesToCDK( - getInstancePropsStub, - 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 4c13126b9c..bbd94a88f4 100644 --- a/packages/backend-data/src/convert_authorization_modes.ts +++ b/packages/backend-data/src/convert_authorization_modes.ts @@ -128,15 +128,11 @@ const computeIAMAuthFromResource = ( additionalRoles: IRole[] = [] ): CDKIAMAuthorizationConfig | undefined => { if (providedAuthConfig) { - const allowListedRoles = [ - ...(authModes?.allowListedRoleNames || []), - ...additionalRoles, - ]; return { authenticatedUserRole: providedAuthConfig.authenticatedUserRole, unauthenticatedUserRole: providedAuthConfig.unauthenticatedUserRole, identityPoolId: providedAuthConfig.identityPoolId, - allowListedRoles, + allowListedRoles: additionalRoles, }; } return; 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) ); From beb159146f7ec97b048e6ca2539cf8d912a55a42 Mon Sep 17 00:00:00 2001 From: Roshane Pascual Date: Fri, 8 Mar 2024 16:15:09 -0800 Subject: [PATCH 27/41] update text to match sandbox default behavior (#1122) --- .changeset/modern-moons-fail.md | 5 +++++ packages/cli/src/commands/sandbox/sandbox_command.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/modern-moons-fail.md 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/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, From c9f03ee9a018d51bec8a96d12fa1cd4512d897b6 Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Tue, 12 Mar 2024 09:14:47 -0700 Subject: [PATCH 28/41] expose subset of plugin-types from backend submodule export (#1128) --- .changeset/afraid-flies-repair.md | 5 ++ packages/backend/API.md | 46 +++++++++++++++++++ packages/backend/api-extractor.json | 3 +- packages/backend/package.json | 3 ++ packages/backend/src/index.internal.ts | 9 ++++ packages/backend/src/types/platform.ts | 21 +++++++++ .../backend_submodule_type_exports.test.ts | 41 +++++++++++++++++ 7 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 .changeset/afraid-flies-repair.md create mode 100644 packages/backend/src/index.internal.ts create mode 100644 packages/backend/src/types/platform.ts create mode 100644 packages/integration-tests/src/test-in-memory/backend_submodule_type_exports.test.ts 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/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/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 22a19fa98a..4443c37ac0 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -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": { 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/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; +}; From 75f69ea1a3ec0d26ddc6c119938d70f8fc6649a6 Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Tue, 12 Mar 2024 09:15:44 -0700 Subject: [PATCH 29/41] store attribution string in function stack (#1129) --- .changeset/little-baboons-tan.md | 5 +++ package-lock.json | 34 +++++++++---------- packages/backend-function/src/factory.test.ts | 20 +++++++++++ packages/backend-function/src/factory.ts | 12 ++++++- 4 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 .changeset/little-baboons-tan.md 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/package-lock.json b/package-lock.json index c67359a509..cadea4ae3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23587,7 +23587,7 @@ }, "packages/auth-construct": { "name": "@aws-amplify/auth-construct-alpha", - "version": "0.6.0-beta.3", + "version": "0.6.0-beta.4", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", @@ -23602,16 +23602,16 @@ }, "packages/backend": { "name": "@aws-amplify/backend", - "version": "0.13.0-beta.4", + "version": "0.13.0-beta.5", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-auth": "^0.5.0-beta.3", - "@aws-amplify/backend-data": "^0.10.0-beta.3", - "@aws-amplify/backend-function": "^0.8.0-beta.2", + "@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.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.11", "@aws-amplify/platform-core": "^0.5.0-beta.1", @@ -23629,10 +23629,10 @@ }, "packages/backend-auth": { "name": "@aws-amplify/backend-auth", - "version": "0.5.0-beta.3", + "version": "0.5.0-beta.4", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.3", + "@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" }, @@ -23647,7 +23647,7 @@ }, "packages/backend-data": { "name": "@aws-amplify/backend-data", - "version": "0.10.0-beta.3", + "version": "0.10.0-beta.4", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", @@ -23683,7 +23683,7 @@ }, "packages/backend-function": { "name": "@aws-amplify/backend-function", - "version": "0.8.0-beta.2", + "version": "0.8.0-beta.3", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", @@ -23750,7 +23750,7 @@ }, "packages/backend-storage": { "name": "@aws-amplify/backend-storage", - "version": "0.6.0-beta.2", + "version": "0.6.0-beta.3", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^0.7.0-beta.0", @@ -23768,7 +23768,7 @@ }, "packages/cli": { "name": "@aws-amplify/backend-cli", - "version": "0.12.0-beta.4", + "version": "0.12.0-beta.5", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-deployer": "^0.5.1-beta.1", @@ -23780,7 +23780,7 @@ "@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.3", + "@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", @@ -24121,11 +24121,11 @@ }, "packages/integration-tests": { "name": "@aws-amplify/integration-tests", - "version": "0.5.0-beta.1", + "version": "0.5.0-beta.2", "license": "Apache-2.0", "devDependencies": { - "@aws-amplify/auth-construct-alpha": "^0.6.0-beta.3", - "@aws-amplify/backend": "^0.13.0-beta.4", + "@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.9.0-beta.3", "@aws-amplify/data-schema": "^0.13.11", @@ -24709,7 +24709,7 @@ }, "packages/sandbox": { "name": "@aws-amplify/sandbox", - "version": "0.5.2-beta.3", + "version": "0.5.2-beta.4", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-deployer": "^0.5.1-beta.1", diff --git a/packages/backend-function/src/factory.test.ts b/packages/backend-function/src/factory.test.ts index 9bb0b499f9..b57e5f6937 100644 --- a/packages/backend-function/src/factory.test.ts +++ b/packages/backend-function/src/factory.test.ts @@ -347,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 2f38ff8eb5..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 @@ -302,6 +306,12 @@ class AmplifyFunction }; this.storeOutput(outputStorageStrategy); + + new AttributionMetadataStorage().storeAttributionMetadata( + Stack.of(this), + functionStackType, + fileURLToPath(new URL('../package.json', import.meta.url)) + ); } getResourceAccessAcceptor = () => ({ From 808d054058014992d0a83af290d78a7081853bc5 Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Tue, 12 Mar 2024 09:16:02 -0700 Subject: [PATCH 30/41] Add function hotswapping to e2e suite (#1130) --- .changeset/thin-steaks-shave.md | 2 ++ .../data_storage_auth_with_triggers.ts | 20 +++++++++++++++++++ .../update-1/data/resource.ts | 3 +++ .../update-1/func-src/handler.ts | 9 +++++++++ .../update-1/function.ts | 5 ----- 5 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 .changeset/thin-steaks-shave.md create mode 100644 packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/update-1/func-src/handler.ts delete mode 100644 packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/update-1/function.ts 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/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 4111fab905..0ce1536366 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 @@ -103,6 +103,8 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { private readonly dataResourceFileSuffix = 'data/resource.ts'; + private readonly functionHandlerFileSuffix = 'func-src/handler.ts'; + private readonly testSecretNames = [ 'googleId', 'googleSecret', @@ -174,6 +176,16 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { const dataResourceFile = pathToFileURL( path.join(this.projectAmplifyDirPath, this.dataResourceFileSuffix) ); + + const sourceFunctionUpdateFile = pathToFileURL( + path.join( + fileURLToPath(this.sourceProjectUpdateDirPath), + this.functionHandlerFileSuffix + ) + ); + const functionHandlerFile = pathToFileURL( + path.join(this.projectAmplifyDirPath, this.functionHandlerFileSuffix) + ); return [ { sourceFile: sourceDataResourceFile, @@ -183,6 +195,14 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { onOther: 30, }, }, + { + sourceFile: sourceFunctionUpdateFile, + projectFile: functionHandlerFile, + deployThresholdSec: { + onWindows: 30, + onOther: 30, + }, + }, ]; } 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/update-1/data/resource.ts index dcb1dcaa79..d447c77699 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/update-1/data/resource.ts @@ -1,3 +1,6 @@ +// 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 data definition. The destination location contains the ../function.js dependency import { defaultNodeFunc } from '../function.js'; import { type ClientSchema, diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/update-1/func-src/handler.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/update-1/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/update-1/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/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', -}); From 05c3c9bb429ef20435ca1e61a17ee81fee0f76fd Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Tue, 12 Mar 2024 09:17:33 -0700 Subject: [PATCH 31/41] Update `TargetLanguage` type name in model gen package (#1132) --- .changeset/clean-pets-join.md | 6 ++++++ packages/cli/src/form-generation/form_generation_handler.ts | 2 +- packages/model-generator/API.md | 5 +---- packages/model-generator/src/generate_api_code.test.ts | 6 +++--- packages/model-generator/src/generate_api_code.ts | 2 +- .../model-generator/src/graphql_document_generator.test.ts | 2 +- packages/model-generator/src/graphql_document_generator.ts | 4 ++-- packages/model-generator/src/model_generator.ts | 3 +-- 8 files changed, 16 insertions(+), 14 deletions(-) create mode 100644 .changeset/clean-pets-join.md 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/packages/cli/src/form-generation/form_generation_handler.ts b/packages/cli/src/form-generation/form_generation_handler.ts index 29a344630d..a742fe8317 100644 --- a/packages/cli/src/form-generation/form_generation_handler.ts +++ b/packages/cli/src/form-generation/form_generation_handler.ts @@ -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/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/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; From 281af347722a7b5cbb58c83ce21bf8a02b548bd1 Mon Sep 17 00:00:00 2001 From: MURAKAMI Masahiko Date: Wed, 13 Mar 2024 01:46:05 +0900 Subject: [PATCH 32/41] docs: replace package name cli to backend-cli (#1119) --- PROJECT_ARCHITECTURE.md | 2 +- markdown-assets/simple-dependency-graph.png | Bin 205249 -> 205724 bytes 2 files changed, 1 insertion(+), 1 deletion(-) 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 2b8fd74814b1ef47f22791eda6e609037baf6676..4b2fc419a253eb45c3c3308a5280d19e7decb163 100644 GIT binary patch delta 88624 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 O#dXyiDy7PI0{<5osDCd2 delta 88388 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�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 ri`0|%*{K*2_t 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 Date: Tue, 12 Mar 2024 12:13:13 -0700 Subject: [PATCH 33/41] increase win hotswap timeout (#1139) --- .changeset/empty-emus-thank.md | 2 ++ .../src/test-project-setup/data_storage_auth_with_triggers.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 .changeset/empty-emus-thank.md 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/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 0ce1536366..dee714b9a3 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 @@ -199,7 +199,7 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { sourceFile: sourceFunctionUpdateFile, projectFile: functionHandlerFile, deployThresholdSec: { - onWindows: 30, + onWindows: 40, onOther: 30, }, }, From 3a73ba025569226b18baa27ad59861c4583559b6 Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Tue, 12 Mar 2024 13:08:33 -0700 Subject: [PATCH 34/41] Func hotswap e2e update (#1137) --- .changeset/small-hotels-do.md | 2 + .../predicated_action_macros.ts | 9 ++-- .../predicated_action_queue_builder.ts | 11 +++-- .../src/process-controller/types.ts | 7 +++ .../src/test-e2e/deployment.test.ts | 4 +- .../data_storage_auth_with_triggers.ts | 47 ++++++++----------- .../test-project-setup/test_project_base.ts | 7 ++- .../hotswap-update-files/README.md | 2 + .../data/resource.ts | 3 -- .../func-src/handler.ts | 0 .../hotswap-update-files/function.ts | 38 +++++++++++++++ 11 files changed, 87 insertions(+), 43 deletions(-) create mode 100644 .changeset/small-hotels-do.md create mode 100644 packages/integration-tests/src/process-controller/types.ts create mode 100644 packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/README.md rename packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/{update-1 => hotswap-update-files}/data/resource.ts (81%) rename packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/{update-1 => hotswap-update-files}/func-src/handler.ts (100%) create mode 100644 packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/function.ts 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/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-project-setup/data_storage_auth_with_triggers.ts b/packages/integration-tests/src/test-project-setup/data_storage_auth_with_triggers.ts index dee714b9a3..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 @@ -97,14 +97,10 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { ); private readonly sourceProjectUpdateDirPath: URL = new URL( - `${this.sourceProjectDirPath}/update-1`, + `${this.sourceProjectDirPath}/hotswap-update-files`, import.meta.url ); - private readonly dataResourceFileSuffix = 'data/resource.ts'; - - private readonly functionHandlerFileSuffix = 'func-src/handler.ts'; - private readonly testSecretNames = [ 'googleId', 'googleSecret', @@ -167,37 +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) - ); - - const sourceFunctionUpdateFile = pathToFileURL( - path.join( - fileURLToPath(this.sourceProjectUpdateDirPath), - this.functionHandlerFileSuffix - ) - ); - const functionHandlerFile = pathToFileURL( - path.join(this.projectAmplifyDirPath, this.functionHandlerFileSuffix) - ); return [ { - sourceFile: sourceDataResourceFile, - projectFile: dataResourceFile, + replacements: [this.getUpdateReplacementDefinition('data/resource.ts')], deployThresholdSec: { onWindows: 40, onOther: 30, }, }, { - sourceFile: sourceFunctionUpdateFile, - projectFile: functionHandlerFile, + replacements: [ + this.getUpdateReplacementDefinition('func-src/handler.ts'), + this.getUpdateReplacementDefinition('function.ts'), + ], deployThresholdSec: { onWindows: 40, onOther: 30, @@ -265,6 +243,19 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { 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 => { 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-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 81% 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 d447c77699..dcb1dcaa79 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 @@ -1,6 +1,3 @@ -// 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 data definition. The destination location contains the ../function.js dependency import { defaultNodeFunc } from '../function.js'; import { type ClientSchema, diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/update-1/func-src/handler.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/func-src/handler.ts similarity index 100% rename from packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/update-1/func-src/handler.ts rename to packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/func-src/handler.ts 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', +}); From ee247fdd5cd97e7440608a7b16fd883a46effa1b Mon Sep 17 00:00:00 2001 From: MJ Zhang <0618@users.noreply.github.com> Date: Tue, 12 Mar 2024 13:47:22 -0700 Subject: [PATCH 35/41] feat: Import `printer` from cli-core (#1135) * export printer from cli-core * update package-lock * use printer from cli-core * add changeset * update API.md --- .changeset/rich-onions-learn.md | 7 +++++++ packages/cli-core/API.md | 3 +++ packages/cli-core/src/index.ts | 1 + .../src/client-config/client_config_generator_adapter.ts | 2 +- .../commands/configure/configure_profile_command.test.ts | 3 +-- .../src/commands/configure/configure_profile_command.ts | 3 +-- .../telemetry/configure_telemetry_command.test.ts | 2 +- .../configure/telemetry/configure_telemetry_command.ts | 2 +- packages/cli/src/commands/info/info_command.test.ts | 2 +- packages/cli/src/commands/info/info_command.ts | 2 +- packages/cli/src/commands/open/open.ts | 2 +- .../pipeline-deploy/pipeline_deploy_command_factory.ts | 6 ++++-- .../sandbox/sandbox-delete/sandbox_delete_command.test.ts | 3 +-- .../sandbox-secret/sandbox_secret_get_command.test.ts | 2 +- .../sandbox/sandbox-secret/sandbox_secret_get_command.ts | 2 +- .../sandbox-secret/sandbox_secret_list_command.test.ts | 2 +- .../sandbox/sandbox-secret/sandbox_secret_list_command.ts | 2 +- packages/cli/src/commands/sandbox/sandbox_command.test.ts | 3 +-- .../cli/src/commands/sandbox/sandbox_command_factory.ts | 2 +- .../commands/sandbox/sandbox_event_handler_factory.test.ts | 2 +- .../src/commands/sandbox/sandbox_event_handler_factory.ts | 3 +-- packages/cli/src/error_handler.test.ts | 3 +-- packages/cli/src/error_handler.ts | 3 +-- .../cli/src/form-generation/form_generation_handler.ts | 2 +- packages/cli/src/printer.ts | 7 ------- .../create-amplify/src/amplify_project_creator.test.ts | 2 +- packages/create-amplify/src/amplify_project_creator.ts | 3 +-- packages/create-amplify/src/create_amplify.ts | 2 +- packages/create-amplify/src/get_project_root.ts | 3 +-- packages/create-amplify/src/gitignore_initializer.test.ts | 2 +- packages/create-amplify/src/gitignore_initializer.ts | 3 +-- packages/create-amplify/src/printer.ts | 7 ------- 32 files changed, 41 insertions(+), 52 deletions(-) create mode 100644 .changeset/rich-onions-learn.md delete mode 100644 packages/cli/src/printer.ts delete mode 100644 packages/create-amplify/src/printer.ts 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/packages/cli-core/API.md b/packages/cli-core/API.md index 96cd39ce63..c408c1beb1 100644 --- a/packages/cli-core/API.md +++ b/packages/cli-core/API.md @@ -67,6 +67,9 @@ export class Printer { 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/src/index.ts b/packages/cli-core/src/index.ts index f24d2d872b..e7fce20f88 100644 --- a/packages/cli-core/src/index.ts +++ b/packages/cli-core/src/index.ts @@ -1,5 +1,6 @@ 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/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_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..6235399ce1 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 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..28551d827b 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 { COLOR, printer } from '@aws-amplify/cli-core'; /** * Coordinates creation of sandbox event handlers diff --git a/packages/cli/src/error_handler.test.ts b/packages/cli/src/error_handler.test.ts index 682460f0d8..059a3aaa56 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 { COLOR, 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'); diff --git a/packages/cli/src/error_handler.ts b/packages/cli/src/error_handler.ts index ded56ab88f..fa97efecdd 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 { COLOR, 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'; diff --git a/packages/cli/src/form-generation/form_generation_handler.ts b/packages/cli/src/form-generation/form_generation_handler.ts index a742fe8317..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; 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/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 2f21b57e0e..bc4bc1441f 100644 --- a/packages/create-amplify/src/get_project_root.ts +++ b/packages/create-amplify/src/get_project_root.ts @@ -1,8 +1,7 @@ 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 { 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); From 26cdffd3a9960a610bf5315f83941afea0596f89 Mon Sep 17 00:00:00 2001 From: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Tue, 12 Mar 2024 17:58:01 -0400 Subject: [PATCH 36/41] backend-data: add support for first-class defineFunction (#1138) --- .changeset/four-peaches-pull.md | 6 ++++++ package-lock.json | 20 +++++++++---------- packages/backend-data/package.json | 4 ++-- packages/backend-data/src/factory.ts | 15 +++++++++----- packages/backend/package.json | 2 +- packages/integration-tests/package.json | 2 +- .../data_storage_auth_with_triggers.test.ts | 1 + .../amplify/data/echo/handler2.ts | 6 ++++++ .../amplify/data/resource.ts | 15 +++++++++++++- .../hotswap-update-files/data/resource.ts | 15 +++++++++++++- 10 files changed, 65 insertions(+), 21 deletions(-) create mode 100644 .changeset/four-peaches-pull.md create mode 100644 packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/data/echo/handler2.ts 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/package-lock.json b/package-lock.json index cadea4ae3b..bd2229e7b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1244,17 +1244,17 @@ } }, "node_modules/@aws-amplify/data-schema": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema/-/data-schema-0.13.11.tgz", - "integrity": "sha512-9BD+Qun3ve4Z4F7c1Ri6GrarEdEuCw+OjxFgaSouSZsouJ8a1+oVvMoqeU8Sq4+WvQJfgl8WzloQYGSGHzsznw==", + "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.7", - "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema-types/-/data-schema-types-0.7.7.tgz", - "integrity": "sha512-DjHeJ2dfTPTG+MjkguTcKTy20OlP4DOo1AORPSz23Yl4W+0P2DfG03VlT6o9xc9jWqreQ3oJGkDsO/2oR81KsA==", + "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" } @@ -23613,7 +23613,7 @@ "@aws-amplify/backend-secret": "^0.4.5-beta.1", "@aws-amplify/backend-storage": "^0.6.0-beta.3", "@aws-amplify/client-config": "^0.9.0-beta.3", - "@aws-amplify/data-schema": "^0.13.11", + "@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" @@ -23653,12 +23653,12 @@ "@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.7", + "@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.11", + "@aws-amplify/data-schema": "^0.13.15", "@aws-amplify/platform-core": "^0.5.0-beta.1" }, "peerDependencies": { @@ -24128,7 +24128,7 @@ "@aws-amplify/backend": "^0.13.0-beta.5", "@aws-amplify/backend-secret": "^0.4.5-beta.1", "@aws-amplify/client-config": "^0.9.0-beta.3", - "@aws-amplify/data-schema": "^0.13.11", + "@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", diff --git a/packages/backend-data/package.json b/packages/backend-data/package.json index c3bfb144f2..5d7e83ddb5 100644 --- a/packages/backend-data/package.json +++ b/packages/backend-data/package.json @@ -18,7 +18,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@aws-amplify/data-schema": "^0.13.11", + "@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" }, @@ -31,6 +31,6 @@ "@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.7" + "@aws-amplify/data-schema-types": "^0.7.8" } } diff --git a/packages/backend-data/src/factory.ts b/packages/backend-data/src/factory.ts index e2406b47c0..606986fa00 100644 --- a/packages/backend-data/src/factory.ts +++ b/packages/backend-data/src/factory.ts @@ -1,5 +1,6 @@ import { IConstruct } from 'constructs'; import { + AmplifyFunction, AuthResources, BackendOutputStorageStrategy, ConstructContainerEntryGenerator, @@ -109,9 +110,11 @@ class DataGenerator implements ConstructContainerEntryGenerator { let amplifyGraphqlDefinition; let jsFunctions: JsResolver[] = []; let functionSchemaAccess: FunctionSchemaAccess[] = []; + let lambdaFunctions: Record> = {}; try { if (isModelSchema(this.props.schema)) { - ({ jsFunctions, functionSchemaAccess } = this.props.schema.transform()); + ({ jsFunctions, functionSchemaAccess, lambdaFunctions } = + this.props.schema.transform()); } amplifyGraphqlDefinition = convertSchemaToCDK(this.props.schema); } catch (error) { @@ -181,10 +184,12 @@ class DataGenerator implements ConstructContainerEntryGenerator { this.props.authorizationModes ); - const functionNameMap = convertFunctionNameMapToCDK( - this.getInstanceProps, - this.props.functions ?? {} - ); + const propsFunctions = this.props.functions ?? {}; + + const functionNameMap = convertFunctionNameMapToCDK(this.getInstanceProps, { + ...propsFunctions, + ...lambdaFunctions, + }); const amplifyApi = new AmplifyData(scope, this.defaultName, { apiName: this.props.name, definition: amplifyGraphqlDefinition, diff --git a/packages/backend/package.json b/packages/backend/package.json index 4443c37ac0..5719e71f56 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -24,7 +24,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/data-schema": "^0.13.11", + "@aws-amplify/data-schema": "^0.13.15", "@aws-amplify/backend-auth": "^0.5.0-beta.4", "@aws-amplify/backend-function": "^0.8.0-beta.3", "@aws-amplify/backend-data": "^0.10.0-beta.4", diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index c7f31fb562..b1bbb2593c 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -8,7 +8,7 @@ "@aws-amplify/backend": "^0.13.0-beta.5", "@aws-amplify/backend-secret": "^0.4.5-beta.1", "@aws-amplify/client-config": "^0.9.0-beta.3", - "@aws-amplify/data-schema": "^0.13.11", + "@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", 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-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/resource.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/data/resource.ts index 0adc7025b6..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 @@ -38,7 +38,20 @@ const schema = a.schema({ .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 diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/data/resource.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/data/resource.ts index dcb1dcaa79..4ff22e3255 100644 --- a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/data/resource.ts +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/data/resource.ts @@ -38,7 +38,20 @@ const schema = a.schema({ .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; From 937086bb6dcd2c96276e469d64f99b42f8a3597c Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Wed, 13 Mar 2024 09:39:15 -0700 Subject: [PATCH 37/41] enforce "resolution" in `AmplifyUserError` ctor (#1131) --- .changeset/serious-maps-wait.md | 11 +++++ .../src/userpool_access_policy_factory.ts | 4 +- packages/backend-data/src/factory.ts | 8 +++- packages/backend-deployer/src/cdk_deployer.ts | 5 ++- .../src/cdk_error_mapper.test.ts | 14 +++---- .../backend-deployer/src/cdk_error_mapper.ts | 42 ++++++++++++++----- .../package_manager_controller_factory.ts | 1 + .../sandbox_event_handler_factory.test.ts | 1 + packages/platform-core/API.md | 7 +++- .../src/errors/amplify_error.test.ts | 7 +++- .../platform-core/src/errors/amplify_error.ts | 8 ++++ .../src/errors/amplify_user_error.ts | 4 +- .../src/usage-data/usage_data_emitter.test.ts | 1 + .../sandbox/src/file_watching_sandbox.test.ts | 2 + 14 files changed, 88 insertions(+), 27 deletions(-) create mode 100644 .changeset/serious-maps-wait.md 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/packages/backend-auth/src/userpool_access_policy_factory.ts b/packages/backend-auth/src/userpool_access_policy_factory.ts index 6d5651a478..535a17d950 100644 --- a/packages/backend-auth/src/userpool_access_policy_factory.ts +++ b/packages/backend-auth/src/userpool_access_policy_factory.ts @@ -23,7 +23,9 @@ export class UserPoolAccessPolicyFactory { createPolicy = (actions: AuthAction[]) => { if (actions.length === 0) { throw new AmplifyUserError('EmptyPolicyError', { - message: 'At least one action must be specified', + message: 'At least one action must be specified.', + resolution: + 'Ensure all resource access rules specify at least one action.', }); } diff --git a/packages/backend-data/src/factory.ts b/packages/backend-data/src/factory.ts index 606986fa00..9948415d67 100644 --- a/packages/backend-data/src/factory.ts +++ b/packages/backend-data/src/factory.ts @@ -124,7 +124,9 @@ class DataGenerator implements ConstructContainerEntryGenerator { message: error instanceof Error ? error.message - : 'Cannot covert user schema', + : 'Failed to parse schema definition.', + resolution: + 'Check your data schema definition for syntax and type errors.', }, error instanceof Error ? error : undefined ); @@ -155,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 ); @@ -174,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 ); 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/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 6418e90f48..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 @@ -36,6 +36,7 @@ export class PackageManagerControllerFactory { throw new AmplifyUserError('UnsupportedPackageManagerError', { message, details, + resolution: 'Use a supported package manager for your OS', }); } return new PnpmPackageManagerController(this.cwd); 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 6235399ce1..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 @@ -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/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/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/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 From b931980e46e7520349b453b74a16358a574c967c Mon Sep 17 00:00:00 2001 From: bzsurbhi <115104450+bzsurbhi@users.noreply.github.com> Date: Wed, 13 Mar 2024 09:40:43 -0700 Subject: [PATCH 38/41] refactor listSandboxes into more generic listBackends (#1096) --- .changeset/two-shirts-type.md | 5 + .eslint_dictionary.json | 1 + packages/deployed-backend-client/API.md | 34 ++- .../src/deployed_backend_client.ts | 76 +++-- .../src/deployed_backend_client_factory.ts | 22 +- ...d_client_list_delete_failed_stacks.test.ts | 259 ++++++++++++++++++ ...oyed_backend_client_list_sandboxes.test.ts | 106 +++---- 7 files changed, 387 insertions(+), 116 deletions(-) create mode 100644 .changeset/two-shirts-type.md create mode 100644 packages/deployed-backend-client/src/deployed_backend_client_list_delete_failed_stacks.test.ts diff --git a/.changeset/two-shirts-type.md b/.changeset/two-shirts-type.md new file mode 100644 index 0000000000..7dd6a92b46 --- /dev/null +++ b/.changeset/two-shirts-type.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/deployed-backend-client': major +--- + +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 ffc7be9c91..54e92fd56c 100644 --- a/.eslint_dictionary.json +++ b/.eslint_dictionary.json @@ -11,6 +11,7 @@ "argv", "arn", "arns", + "backends", "birthdate", "bundler", "cdk", 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/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()); }); }); From a2a15e3aa751af135be32d0908da9d31d0d4cfb3 Mon Sep 17 00:00:00 2001 From: bzsurbhi <115104450+bzsurbhi@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:21:12 -0700 Subject: [PATCH 39/41] fix: update changeset to minor version (#1146) --- .changeset/two-shirts-type.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/two-shirts-type.md b/.changeset/two-shirts-type.md index 7dd6a92b46..b648cf6396 100644 --- a/.changeset/two-shirts-type.md +++ b/.changeset/two-shirts-type.md @@ -1,5 +1,5 @@ --- -'@aws-amplify/deployed-backend-client': major +'@aws-amplify/deployed-backend-client': minor --- Add listBackends method to return a list of stacks for sandbox and branch deployments From 3e34244f7ad6de1f72141f8aa897385f69b9d2b3 Mon Sep 17 00:00:00 2001 From: MJ Zhang <0618@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:30:08 -0700 Subject: [PATCH 40/41] feat: refactor `color` and remove `COLOR` (#1142) * refactor color * remove COLOR * add changeset * update API.md * use format to replace color and remove color * update changeset * update API.md * fix test * simplify printer * fix test * fix test by removing the error message assertion * Update .changeset/new-meals-destroy.md Co-authored-by: Kamil Sobol * add an assert --------- Co-authored-by: Kamil Sobol --- .changeset/new-meals-destroy.md | 7 ++++++ packages/cli-core/API.md | 9 ++------ packages/cli-core/src/colors.ts | 19 ---------------- packages/cli-core/src/format/format.ts | 3 ++- packages/cli-core/src/index.ts | 1 - packages/cli-core/src/printer/printer.ts | 9 ++------ .../sandbox/sandbox_event_handler_factory.ts | 11 +++++----- packages/cli/src/error_handler.test.ts | 22 +++++++++---------- packages/cli/src/error_handler.ts | 9 ++++---- packages/sandbox/src/file_watching_sandbox.ts | 9 ++++---- 10 files changed, 38 insertions(+), 61 deletions(-) create mode 100644 .changeset/new-meals-destroy.md delete mode 100644 packages/cli-core/src/colors.ts 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/packages/cli-core/API.md b/packages/cli-core/API.md index c408c1beb1..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; @@ -62,7 +57,7 @@ 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; } 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 e7fce20f88..4ca21c8e71 100644 --- a/packages/cli-core/src/index.ts +++ b/packages/cli-core/src/index.ts @@ -1,5 +1,4 @@ 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'; 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/src/commands/sandbox/sandbox_event_handler_factory.ts b/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.ts index 28551d827b..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,7 +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, printer } from '@aws-amplify/cli-core'; +import { format, printer } from '@aws-amplify/cli-core'; /** * Coordinates creation of sandbox event handlers @@ -45,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 059a3aaa56..771716002a 100644 --- a/packages/cli/src/error_handler.test.ts +++ b/packages/cli/src/error_handler.test.ts @@ -4,7 +4,7 @@ import { generateCommandFailureHandler, } from './error_handler.js'; import { Argv } from 'yargs'; -import { COLOR, printer } from '@aws-amplify/cli-core'; +import { printer } from '@aws-amplify/cli-core'; import assert from 'node:assert'; import { InvalidCredentialError } from './error/credential_error.js'; @@ -35,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', () => { @@ -47,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', () => { @@ -68,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', () => { @@ -81,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 fa97efecdd..7815dd411a 100644 --- a/packages/cli/src/error_handler.ts +++ b/packages/cli/src/error_handler.ts @@ -1,4 +1,4 @@ -import { COLOR, printer } from '@aws-amplify/cli-core'; +import { format, printer } from '@aws-amplify/cli-core'; import { InvalidCredentialError } from './error/credential_error.js'; import { EOL } from 'os'; import { Argv } from 'yargs'; @@ -79,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/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({ From fb9bf5fa587fae282b3c39bff3291b77043ff107 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 17:28:12 -0700 Subject: [PATCH 41/41] Version Packages (beta) (#1123) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 15 +++++++++++- packages/auth-construct/CHANGELOG.md | 6 +++++ packages/auth-construct/package.json | 4 ++-- packages/backend-auth/CHANGELOG.md | 8 +++++++ packages/backend-auth/package.json | 8 +++---- packages/backend-data/CHANGELOG.md | 12 ++++++++++ packages/backend-data/package.json | 6 ++--- packages/backend-deployer/CHANGELOG.md | 8 +++++++ packages/backend-deployer/package.json | 4 ++-- packages/backend-function/CHANGELOG.md | 7 ++++++ packages/backend-function/package.json | 6 ++--- packages/backend-output-storage/CHANGELOG.md | 7 ++++++ packages/backend-output-storage/package.json | 4 ++-- packages/backend-secret/CHANGELOG.md | 7 ++++++ packages/backend-secret/package.json | 4 ++-- packages/backend-storage/CHANGELOG.md | 6 +++++ packages/backend-storage/package.json | 6 ++--- packages/backend/CHANGELOG.md | 21 +++++++++++++++++ packages/backend/package.json | 18 +++++++-------- packages/cli-core/CHANGELOG.md | 13 +++++++++++ packages/cli-core/package.json | 4 ++-- packages/cli/CHANGELOG.md | 23 +++++++++++++++++++ packages/cli/package.json | 18 +++++++-------- packages/client-config/CHANGELOG.md | 11 +++++++++ packages/client-config/package.json | 8 +++---- packages/create-amplify/CHANGELOG.md | 11 +++++++++ packages/create-amplify/package.json | 6 ++--- packages/deployed-backend-client/CHANGELOG.md | 11 +++++++++ packages/deployed-backend-client/package.json | 4 ++-- packages/integration-tests/CHANGELOG.md | 6 +++++ packages/integration-tests/package.json | 12 +++++----- packages/model-generator/CHANGELOG.md | 11 +++++++++ packages/model-generator/package.json | 4 ++-- packages/platform-core/CHANGELOG.md | 6 +++++ packages/platform-core/package.json | 2 +- packages/sandbox/CHANGELOG.md | 17 ++++++++++++++ packages/sandbox/package.json | 14 +++++------ 37 files changed, 271 insertions(+), 67 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 1658164ffb..0776a5440d 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -27,18 +27,23 @@ "@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", @@ -48,6 +53,7 @@ "late-worms-rule", "lemon-peas-sin", "light-cougars-give", + "little-baboons-tan", "little-books-press", "loud-sheep-occur", "lucky-tigers-carry", @@ -55,8 +61,10 @@ "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", @@ -66,17 +74,22 @@ "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/packages/auth-construct/CHANGELOG.md b/packages/auth-construct/CHANGELOG.md index de701d5c7d..559131e387 100644 --- a/packages/auth-construct/CHANGELOG.md +++ b/packages/auth-construct/CHANGELOG.md @@ -1,5 +1,11 @@ # @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 diff --git a/packages/auth-construct/package.json b/packages/auth-construct/package.json index 5a9e4edf09..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.4", + "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/backend-auth/CHANGELOG.md b/packages/backend-auth/CHANGELOG.md index 2757c750b6..eca86d9c22 100644 --- a/packages/backend-auth/CHANGELOG.md +++ b/packages/backend-auth/CHANGELOG.md @@ -1,5 +1,13 @@ # @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 diff --git a/packages/backend-auth/package.json b/packages/backend-auth/package.json index 82ef225d13..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.4", + "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.4", - "@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-data/CHANGELOG.md b/packages/backend-data/CHANGELOG.md index e13b2f4de3..2d131b8d2f 100644 --- a/packages/backend-data/CHANGELOG.md +++ b/packages/backend-data/CHANGELOG.md @@ -1,5 +1,17 @@ # @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 diff --git a/packages/backend-data/package.json b/packages/backend-data/package.json index 5d7e83ddb5..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.4", + "version": "0.10.0-beta.5", "type": "module", "publishConfig": { "access": "public" @@ -20,14 +20,14 @@ "devDependencies": { "@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", 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-function/CHANGELOG.md b/packages/backend-function/CHANGELOG.md index 62af76a673..de95cc357b 100644 --- a/packages/backend-function/CHANGELOG.md +++ b/packages/backend-function/CHANGELOG.md @@ -1,5 +1,12 @@ # @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 diff --git a/packages/backend-function/package.json b/packages/backend-function/package.json index 10d86e874e..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.3", + "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-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/CHANGELOG.md b/packages/backend-storage/CHANGELOG.md index d900dfc312..ca711b542c 100644 --- a/packages/backend-storage/CHANGELOG.md +++ b/packages/backend-storage/CHANGELOG.md @@ -1,5 +1,11 @@ # @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 diff --git a/packages/backend-storage/package.json b/packages/backend-storage/package.json index 7edf2e8031..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.3", + "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/CHANGELOG.md b/packages/backend/CHANGELOG.md index 500a97fb7c..0de0e4db15 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @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 diff --git a/packages/backend/package.json b/packages/backend/package.json index 5719e71f56..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.5", + "version": "0.13.0-beta.6", "type": "module", "publishConfig": { "access": "public" @@ -25,15 +25,15 @@ "license": "Apache-2.0", "dependencies": { "@aws-amplify/data-schema": "^0.13.15", - "@aws-amplify/backend-auth": "^0.5.0-beta.4", - "@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.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.3", - "@aws-amplify/client-config": "^0.9.0-beta.3", - "@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/cli-core/CHANGELOG.md b/packages/cli-core/CHANGELOG.md index ea62927c57..19fc5c1dd6 100644 --- a/packages/cli-core/CHANGELOG.md +++ b/packages/cli-core/CHANGELOG.md @@ -1,5 +1,18 @@ # @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 diff --git a/packages/cli-core/package.json b/packages/cli-core/package.json index dc3361fa30..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.5.0-beta.1", + "version": "0.5.0-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", "@inquirer/prompts": "^3.0.0", "execa": "^8.0.1", "kleur": "^4.1.5" diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 2f1da901f7..c5a4ace3ea 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,28 @@ # @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 diff --git a/packages/cli/package.json b/packages/cli/package.json index 6ea944bad5..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.5", + "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.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/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.4", + "@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/client-config/CHANGELOG.md b/packages/client-config/CHANGELOG.md index 9be0100b4e..2bbe113bdc 100644 --- a/packages/client-config/CHANGELOG.md +++ b/packages/client-config/CHANGELOG.md @@ -1,5 +1,16 @@ # @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 diff --git a/packages/client-config/package.json b/packages/client-config/package.json index 766bef8882..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.9.0-beta.3", + "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/create-amplify/CHANGELOG.md b/packages/create-amplify/CHANGELOG.md index eab6588993..8f3fc3c2bf 100644 --- a/packages/create-amplify/CHANGELOG.md +++ b/packages/create-amplify/CHANGELOG.md @@ -1,5 +1,16 @@ # 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 diff --git a/packages/create-amplify/package.json b/packages/create-amplify/package.json index 558267f7eb..be88f0e55b 100644 --- a/packages/create-amplify/package.json +++ b/packages/create-amplify/package.json @@ -1,6 +1,6 @@ { "name": "create-amplify", - "version": "0.7.0-beta.3", + "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.5.0-beta.1", - "@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/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/integration-tests/CHANGELOG.md b/packages/integration-tests/CHANGELOG.md index 0884352b38..94f66fbaf8 100644 --- a/packages/integration-tests/CHANGELOG.md +++ b/packages/integration-tests/CHANGELOG.md @@ -1,5 +1,11 @@ # @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 diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index b1bbb2593c..8d436f3935 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -1,15 +1,15 @@ { "name": "@aws-amplify/integration-tests", "private": true, - "version": "0.5.0-beta.2", + "version": "0.5.0-beta.3", "type": "module", "devDependencies": { - "@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.9.0-beta.3", + "@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.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-iam": "^3.465.0", 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/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/sandbox/CHANGELOG.md b/packages/sandbox/CHANGELOG.md index bbf468aa12..1dda89802c 100644 --- a/packages/sandbox/CHANGELOG.md +++ b/packages/sandbox/CHANGELOG.md @@ -1,5 +1,22 @@ # @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 diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json index f941222920..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.4", + "version": "0.5.2-beta.5", "type": "module", "publishConfig": { "access": "public" @@ -18,12 +18,12 @@ }, "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.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-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",