diff --git a/.changeset/two-sloths-clap.md b/.changeset/two-sloths-clap.md new file mode 100644 index 0000000000..35f1fcf7c6 --- /dev/null +++ b/.changeset/two-sloths-clap.md @@ -0,0 +1,10 @@ +--- +'@aws-amplify/client-config': minor +'@aws-amplify/backend-output-schemas': patch +'@aws-amplify/integration-tests': patch +'@aws-amplify/backend-storage': patch +'@aws-amplify/backend': patch +'@aws-amplify/backend-cli': patch +--- + +add storage access rules to outputs diff --git a/packages/backend-output-schemas/src/storage/v1.ts b/packages/backend-output-schemas/src/storage/v1.ts index 5095714a81..0827b34d69 100644 --- a/packages/backend-output-schemas/src/storage/v1.ts +++ b/packages/backend-output-schemas/src/storage/v1.ts @@ -1,9 +1,29 @@ import { z } from 'zod'; +const storageAccessActionEnum = z.enum([ + 'read', + 'get', + 'list', + 'write', + 'delete', +]); + +const pathSchema = z.record( + z.string(), + z.object({ + guest: z.array(storageAccessActionEnum).optional(), + authenticated: z.array(storageAccessActionEnum).optional(), + groups: z.array(storageAccessActionEnum).optional(), + entity: z.array(storageAccessActionEnum).optional(), + resource: z.array(storageAccessActionEnum).optional(), + }) +); + const bucketSchema = z.object({ name: z.string(), bucketName: z.string(), storageRegion: z.string(), + paths: pathSchema.optional(), }); export const storageOutputSchema = z.object({ diff --git a/packages/backend-storage/src/access_builder.ts b/packages/backend-storage/src/access_builder.ts index 129c8b64ee..7309e1efa7 100644 --- a/packages/backend-storage/src/access_builder.ts +++ b/packages/backend-storage/src/access_builder.ts @@ -5,6 +5,7 @@ import { ResourceProvider, } from '@aws-amplify/plugin-types'; import { StorageAccessBuilder } from './types.js'; +import { entityIdSubstitution } from './constants.js'; export const roleAccessBuilder: StorageAccessBuilder = { authenticated: { @@ -69,7 +70,7 @@ export const roleAccessBuilder: StorageAccessBuilder = { }, ], actions, - idSubstitution: '${cognito-identity.amazonaws.com:sub}', + idSubstitution: entityIdSubstitution, }), }), resource: (other) => ({ diff --git a/packages/backend-storage/src/constants.ts b/packages/backend-storage/src/constants.ts index 8ee0e17bd5..7588e60997 100644 --- a/packages/backend-storage/src/constants.ts +++ b/packages/backend-storage/src/constants.ts @@ -1 +1,2 @@ export const entityIdPathToken = '{entity_id}'; +export const entityIdSubstitution = '${cognito-identity.amazonaws.com:sub}'; diff --git a/packages/backend-storage/src/construct.ts b/packages/backend-storage/src/construct.ts index d31f0b97eb..4308b3ceca 100644 --- a/packages/backend-storage/src/construct.ts +++ b/packages/backend-storage/src/construct.ts @@ -20,6 +20,7 @@ import { AttributionMetadataStorage } from '@aws-amplify/backend-output-storage' import { fileURLToPath } from 'node:url'; import { IFunction } from 'aws-cdk-lib/aws-lambda'; import { S3EventSourceV2 } from 'aws-cdk-lib/aws-lambda-event-sources'; +import { StorageAccessDefinitionOutput } from './private_types.js'; // Be very careful editing this value. It is the string that is used to attribute stacks to Amplify Storage in BI metrics const storageStackType = 'storage-S3'; @@ -84,6 +85,7 @@ export class AmplifyStorage readonly resources: StorageResources; readonly isDefault: boolean; readonly name: string; + accessDefinition: StorageAccessDefinitionOutput; /** * Create a new AmplifyStorage instance */ @@ -146,4 +148,11 @@ export class AmplifyStorage new S3EventSourceV2(this.resources.bucket, { events }) ); }; + + /** + * Add access definitions to storage + */ + addAccessDefinition = (accessOutput: StorageAccessDefinitionOutput) => { + this.accessDefinition = accessOutput; + }; } diff --git a/packages/backend-storage/src/private_types.ts b/packages/backend-storage/src/private_types.ts index 8c7e53f27a..8daaf2af48 100644 --- a/packages/backend-storage/src/private_types.ts +++ b/packages/backend-storage/src/private_types.ts @@ -15,3 +15,9 @@ export type StorageError = * StorageAction type intended to be used after mapping "read" to "get" and "list" */ export type InternalStorageAction = Exclude; + +/** + * Storage access types intended to be used to map storage access to storage outputs + */ +export type StorageAccessConfig = Record; +export type StorageAccessDefinitionOutput = Record; diff --git a/packages/backend-storage/src/storage_access_orchestrator.test.ts b/packages/backend-storage/src/storage_access_orchestrator.test.ts index ffcc031de3..75cecc30ba 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 { entityIdPathToken } from './constants.js'; +import { entityIdPathToken, entityIdSubstitution } from './constants.js'; import { StorageAccessPolicyFactory } from './storage_access_policy_factory.js'; import { StorageAccessDefinition } from './types.js'; @@ -78,7 +78,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), @@ -102,6 +103,11 @@ void describe('StorageAccessOrchestrator', () => { acceptResourceAccessMock.mock.calls[0].arguments[1], ssmEnvironmentEntriesStub ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + 'test/prefix/*': { + acceptor: ['get', 'write'], + }, + }); }); void it('handles multiple permissions for the same resource access acceptor', () => { @@ -132,7 +138,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), @@ -164,6 +171,14 @@ void describe('StorageAccessOrchestrator', () => { acceptResourceAccessMock.mock.calls[0].arguments[1], ssmEnvironmentEntriesStub ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + 'test/prefix/*': { + acceptor: ['get', 'write', 'delete'], + }, + 'another/prefix/*': { + acceptor: ['get'], + }, + }); }); void it('handles multiple resource access acceptors', () => { @@ -204,7 +219,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock1.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock1.mock.calls[0].arguments[0].document.toJSON(), @@ -259,6 +275,15 @@ void describe('StorageAccessOrchestrator', () => { acceptResourceAccessMock2.mock.calls[0].arguments[1], ssmEnvironmentEntriesStub ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + 'test/prefix/*': { + acceptor1: ['get', 'write', 'delete'], + acceptor2: ['get'], + }, + 'another/prefix/*': { + acceptor2: ['get', 'delete'], + }, + }); }); void it('replaces owner placeholder in s3 prefix', () => { @@ -274,7 +299,7 @@ void describe('StorageAccessOrchestrator', () => { acceptResourceAccess: acceptResourceAccessMock, }), ], - idSubstitution: '{testOwnerSub}', + idSubstitution: entityIdSubstitution, uniqueDefinitionIdValidations: accessDefinitionTestDefaults('acceptor') .uniqueDefinitionIdValidations, @@ -286,7 +311,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), @@ -295,12 +321,12 @@ void describe('StorageAccessOrchestrator', () => { { Action: 's3:GetObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/test/{testOwnerSub}/*`, + Resource: `${bucket.bucketArn}/test/${entityIdSubstitution}/*`, }, { Action: 's3:PutObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/test/{testOwnerSub}/*`, + Resource: `${bucket.bucketArn}/test/${entityIdSubstitution}/*`, }, ], Version: '2012-10-17', @@ -310,6 +336,11 @@ void describe('StorageAccessOrchestrator', () => { acceptResourceAccessMock.mock.calls[0].arguments[1], ssmEnvironmentEntriesStub ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + [`test/${entityIdSubstitution}/*`]: { + acceptor: ['get', 'write'], + }, + }); }); void it('denies parent actions on a subpath by default', () => { @@ -347,7 +378,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock1.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock1.mock.calls[0].arguments[0].document.toJSON(), @@ -396,6 +428,14 @@ void describe('StorageAccessOrchestrator', () => { Version: '2012-10-17', } ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + 'foo/*': { + acceptor1: ['get', 'write'], + }, + 'foo/bar/*': { + acceptor2: ['get'], + }, + }); }); void it('combines owner rules for same resource access acceptor', () => { @@ -411,7 +451,7 @@ void describe('StorageAccessOrchestrator', () => { { actions: ['write', 'delete'], getResourceAccessAcceptors: [authenticatedResourceAccessAcceptor], - idSubstitution: '{idSub}', + idSubstitution: entityIdSubstitution, uniqueDefinitionIdValidations: accessDefinitionTestDefaults('auth-with-id') .uniqueDefinitionIdValidations, @@ -428,7 +468,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), @@ -437,17 +478,17 @@ void describe('StorageAccessOrchestrator', () => { { Action: 's3:PutObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/foo/{idSub}/*`, + Resource: `${bucket.bucketArn}/foo/${entityIdSubstitution}/*`, }, { Action: 's3:DeleteObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/foo/{idSub}/*`, + Resource: `${bucket.bucketArn}/foo/${entityIdSubstitution}/*`, }, { Action: 's3:GetObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/foo/*/*`, + Resource: `${bucket.bucketArn}/foo/*`, }, ], Version: '2012-10-17', @@ -457,6 +498,14 @@ void describe('StorageAccessOrchestrator', () => { acceptResourceAccessMock.mock.calls[0].arguments[1], ssmEnvironmentEntriesStub ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + 'foo/*': { + auth: ['get'], + }, + [`foo/${entityIdSubstitution}/*`]: { + 'auth-with-id': ['write', 'delete'], + }, + }); }); void it('handles multiple resource access acceptors on multiple prefixes', () => { @@ -488,7 +537,7 @@ void describe('StorageAccessOrchestrator', () => { { actions: ['get'], getResourceAccessAcceptors: [getResourceAccessAcceptorStub2], - idSubstitution: '{idSub}', + idSubstitution: entityIdSubstitution, uniqueDefinitionIdValidations: accessDefinitionTestDefaults('stub2') .uniqueDefinitionIdValidations, @@ -509,7 +558,7 @@ void describe('StorageAccessOrchestrator', () => { { actions: ['get', 'write', 'delete'], getResourceAccessAcceptors: [getResourceAccessAcceptorStub2], - idSubstitution: '{idSub}', + idSubstitution: entityIdSubstitution, uniqueDefinitionIdValidations: accessDefinitionTestDefaults('stub2') .uniqueDefinitionIdValidations, @@ -526,7 +575,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock1.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock1.mock.calls[0].arguments[0].document.toJSON(), @@ -537,7 +587,7 @@ void describe('StorageAccessOrchestrator', () => { Effect: 'Allow', Resource: [ `${bucket.bucketArn}/foo/*`, - `${bucket.bucketArn}/other/*/*`, + `${bucket.bucketArn}/other/*`, ], }, { @@ -577,23 +627,40 @@ void describe('StorageAccessOrchestrator', () => { Effect: 'Allow', Resource: [ `${bucket.bucketArn}/foo/bar/*`, - `${bucket.bucketArn}/other/{idSub}/*`, + `${bucket.bucketArn}/other/${entityIdSubstitution}/*`, ], }, { Action: 's3:PutObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/other/{idSub}/*`, + Resource: `${bucket.bucketArn}/other/${entityIdSubstitution}/*`, }, { Action: 's3:DeleteObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/other/{idSub}/*`, + Resource: `${bucket.bucketArn}/other/${entityIdSubstitution}/*`, }, ], Version: '2012-10-17', } ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + 'foo/*': { + stub1: ['get', 'write'], + }, + 'foo/bar/*': { + stub2: ['get'], + }, + 'foo/baz/*': { + stub1: ['get'], + }, + 'other/*': { + stub1: ['get'], + }, + [`other/${entityIdSubstitution}/*`]: { + stub2: ['get', 'write', 'delete'], + }, + }); }); void it('throws validation error for multiple rules on the same resource access acceptor', () => { @@ -658,7 +725,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), @@ -695,6 +763,14 @@ void describe('StorageAccessOrchestrator', () => { acceptResourceAccessMock.mock.calls[0].arguments[1], ssmEnvironmentEntriesStub ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + 'foo/bar/*': { + auth: ['get', 'list'], + }, + 'other/baz/*': { + auth: ['get', 'list'], + }, + }); }); }); }); diff --git a/packages/backend-storage/src/storage_access_orchestrator.ts b/packages/backend-storage/src/storage_access_orchestrator.ts index e4b4c0e22a..35d2553ab4 100644 --- a/packages/backend-storage/src/storage_access_orchestrator.ts +++ b/packages/backend-storage/src/storage_access_orchestrator.ts @@ -8,11 +8,15 @@ import { StorageAccessGenerator, StoragePath, } from './types.js'; -import { entityIdPathToken } from './constants.js'; +import { entityIdPathToken, entityIdSubstitution } 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, StorageError } from './private_types.js'; +import { + InternalStorageAction, + StorageAccessConfig, + StorageError, +} from './private_types.js'; import { AmplifyUserError } from '@aws-amplify/platform-core'; /* some types internal to this file to improve readability */ @@ -88,12 +92,26 @@ export class StorageAccessOrchestrator { // verify that the paths in the access definition are valid this.validateStorageAccessPaths(Object.keys(storageAccessDefinition)); + const storageOutputAccessDefinition: Record = + {}; + // iterate over the access definition and group permissions by ResourceAccessAcceptor Object.entries(storageAccessDefinition).forEach( ([s3Prefix, accessPermissions]) => { const uniqueDefinitionIdSet = new Set(); // iterate over all of the access definitions for a given prefix accessPermissions.forEach((permission) => { + const accessConfig: StorageAccessConfig = {}; + // 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) + ); + // iterate over all uniqueDefinitionIdValidations and ensure uniqueness within this path prefix permission.uniqueDefinitionIdValidations.forEach( ({ uniqueDefinitionId, validationErrorOptions }) => { @@ -105,24 +123,21 @@ export class StorageAccessOrchestrator { } else { uniqueDefinitionIdSet.add(uniqueDefinitionId); } + + accessConfig[uniqueDefinitionId] = noDuplicateActions; } ); // make the owner placeholder substitution in the s3 prefix - const prefix = s3Prefix.replaceAll( - entityIdPathToken, + const prefix = placeholderSubstitution( + s3Prefix, permission.idSubstitution - ) as StoragePath; - - // replace "read" with "get" and "list" in actions - const replaceReadWithGetAndList = permission.actions.flatMap( - (action) => (action === 'read' ? ['get', 'list'] : [action]) - ) as InternalStorageAction[]; - - // ensure the actions list has no duplicates - const noDuplicateActions = Array.from( - new Set(replaceReadWithGetAndList) ); + storageOutputAccessDefinition[prefix] = { + ...storageOutputAccessDefinition[prefix], + ...accessConfig, + }; + // set an entry that maps this permission to each resource acceptor permission.getResourceAccessAcceptors.forEach( (getResourceAccessAcceptor) => { @@ -139,6 +154,8 @@ export class StorageAccessOrchestrator { // iterate over the access map entries and invoke each ResourceAccessAcceptor to accept the permissions this.attachPolicies(this.ssmEnvironmentEntries); + + return storageOutputAccessDefinition; }; /** @@ -195,7 +212,11 @@ export class StorageAccessOrchestrator { const allPaths = Array.from(this.prefixDenyMap.keys()); allPaths.forEach((storagePath) => { const parent = findParent(storagePath, allPaths); - if (!parent) { + // do not add to prefix deny map if there is no parent or the path is a subpath with entity id + if ( + !parent || + parent === storagePath.replaceAll(`${entityIdSubstitution}/`, '') + ) { return; } // if a parent path is defined, invoke the denyByDefault callback on this subpath for all policies that exist on the parent path @@ -258,6 +279,26 @@ export class StorageAccessOrchestratorFactory { ); } +/** + * Performs the owner placeholder substitution in the s3 prefix + */ +const placeholderSubstitution = ( + s3Prefix: string, + idSubstitution: string +): StoragePath => { + const prefix = s3Prefix.replaceAll( + entityIdPathToken, + idSubstitution + ) as StoragePath; + + // for owner paths where prefix ends with '/*/*' remove the last wildcard + if (prefix.endsWith('/*/*')) { + return prefix.slice(0, -2) as StoragePath; + } + + return prefix as StoragePath; +}; + /** * 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 diff --git a/packages/backend-storage/src/storage_container_entry_generator.ts b/packages/backend-storage/src/storage_container_entry_generator.ts index 90fbd59bf3..b7da0eaaed 100644 --- a/packages/backend-storage/src/storage_container_entry_generator.ts +++ b/packages/backend-storage/src/storage_container_entry_generator.ts @@ -78,7 +78,9 @@ export class StorageContainerEntryGenerator ); // the orchestrator generates policies according to the accessDefinition and attaches the policies to appropriate roles - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); + amplifyStorage.addAccessDefinition(storageAccessOutput); return amplifyStorage; }; diff --git a/packages/backend-storage/src/storage_outputs_aspect.test.ts b/packages/backend-storage/src/storage_outputs_aspect.test.ts index 7487f0864d..cccf5ba94c 100644 --- a/packages/backend-storage/src/storage_outputs_aspect.test.ts +++ b/packages/backend-storage/src/storage_outputs_aspect.test.ts @@ -7,6 +7,7 @@ import { StorageOutput } from '@aws-amplify/backend-output-schemas'; import { App, Stack } from 'aws-cdk-lib'; import { IConstruct } from 'constructs'; import { AmplifyUserError } from '@aws-amplify/platform-core'; +import { StorageAccessDefinitionOutput } from './private_types.js'; void describe('StorageOutputsAspect', () => { let app: App; @@ -129,6 +130,49 @@ void describe('StorageOutputsAspect', () => { }) ); }); + + void it('should add access paths if the storage has access rules configured', () => { + const accessDefinition = { + 'path/*': { + authenticated: ['get', 'list', 'write', 'delete'], + guest: ['get', 'list'], + }, + }; + const node = new AmplifyStorage(stack, 'test', { name: 'testName' }); + node.addAccessDefinition( + accessDefinition as StorageAccessDefinitionOutput + ); + aspect = new StorageOutputsAspect(outputStorageStrategy); + aspect.visit(node); + + assert.equal( + addBackendOutputEntryMock.mock.calls[0].arguments[0], + 'AWS::Amplify::Storage' + ); + assert.equal( + addBackendOutputEntryMock.mock.calls[0].arguments[1].payload + .storageRegion, + Stack.of(node).region + ); + assert.equal( + addBackendOutputEntryMock.mock.calls[0].arguments[1].payload.bucketName, + node.resources.bucket.bucketName + ); + assert.equal( + appendToBackendOutputListMock.mock.calls[0].arguments[0], + 'AWS::Amplify::Storage' + ); + assert.equal( + appendToBackendOutputListMock.mock.calls[0].arguments[1].payload + .buckets, + JSON.stringify({ + name: node.name, + bucketName: node.resources.bucket.bucketName, + storageRegion: Stack.of(node).region, + paths: accessDefinition, + }) + ); + }); }); void describe('Validate', () => { diff --git a/packages/backend-storage/src/storage_outputs_aspect.ts b/packages/backend-storage/src/storage_outputs_aspect.ts index 9e9a9fcfa0..dd448937a5 100644 --- a/packages/backend-storage/src/storage_outputs_aspect.ts +++ b/packages/backend-storage/src/storage_outputs_aspect.ts @@ -7,6 +7,7 @@ import { BackendOutputStorageStrategy } from '@aws-amplify/plugin-types'; import { IAspect, Stack } from 'aws-cdk-lib'; import { IConstruct } from 'constructs'; import { AmplifyStorage } from './construct.js'; +import { StorageAccessDefinitionOutput } from './private_types.js'; /** * Aspect to store the storage outputs in the backend @@ -95,16 +96,22 @@ export class StorageOutputsAspect implements IAspect { }, }); } - + const bucketsPayload: Record< + string, + string | StorageAccessDefinitionOutput + > = { + name: node.name, + bucketName: node.resources.bucket.bucketName, + storageRegion: Stack.of(node).region, + }; + if (node.accessDefinition) { + bucketsPayload.paths = node.accessDefinition; + } // both default and non-default buckets should have the name, bucket name, and storage region stored in `buckets` field outputStorageStrategy.appendToBackendOutputList(storageOutputKey, { version: '1', payload: { - buckets: JSON.stringify({ - name: node.name, - bucketName: node.resources.bucket.bucketName, - storageRegion: Stack.of(node).region, - }), + buckets: JSON.stringify(bucketsPayload), }, }); }; diff --git a/packages/backend-storage/src/types.ts b/packages/backend-storage/src/types.ts index 39f0a4dead..caf7abc1c0 100644 --- a/packages/backend-storage/src/types.ts +++ b/packages/backend-storage/src/types.ts @@ -41,17 +41,29 @@ export type StorageAccessBuilder = { /** * Configure storage access for authenticated users. Requires `defineAuth` in the backend definition. * @see https://docs.amplify.aws/gen2/build-a-backend/storage/#authenticated-user-access + * + * When configuring access for paths with the `{entity_id}` token, the token is replaced with a wildcard (`*`). + * For a path like `media/profile-pictures/{entity_id}/*`, this means access is configured for authenticated users for any file within + * `media/profile-pictures/*`. */ authenticated: StorageActionBuilder; /** * Configure storage access for guest (unauthenticated) users. Requires `defineAuth` in the backend definition. * @see https://docs.amplify.aws/gen2/build-a-backend/storage/#guest-user-access + * + * When configuring access for paths with the `{entity_id}` token, the token is replaced with a wildcard (`*`). + * For a path like `media/profile-pictures/{entity_id}/*`, this means access is configured for guest users for any file within + * `media/profile-pictures/*`. */ guest: StorageActionBuilder; /** * Configure storage access for User Pool groups. Requires `defineAuth` with groups config in the backend definition. * @see https://docs.amplify.aws/gen2/build-a-backend/storage/#user-group-access * @param groupName The User Pool group name to configure access for + * + * When configuring access for paths with the `{entity_id}` token, the token is replaced with a wildcard (`*`). + * For a path like `media/profile-pictures/{entity_id}/*`, this means access is configured for that specific group for any file within + * `media/profile-pictures/*`. */ groups: (groupNames: string[]) => StorageActionBuilder; /** @@ -64,6 +76,10 @@ export type StorageAccessBuilder = { * Grant other resources in the Amplify backend access to storage. * @see https://docs.amplify.aws/gen2/build-a-backend/storage/#grant-function-access * @param other The target resource to grant access to. Currently only the return value of `defineFunction` is supported. + * + * When configuring access for paths with the `{entity_id}` token, the token is replaced with a wildcard (`*`). + * For a path like `media/profile-pictures/{entity_id}/*`, this means access is configured for resources for any file within + * `media/profile-pictures/*`. */ resource: ( other: ConstructFactory diff --git a/packages/backend/src/backend_factory.test.ts b/packages/backend/src/backend_factory.test.ts index 268bede3ff..cfc01a9e8b 100644 --- a/packages/backend/src/backend_factory.test.ts +++ b/packages/backend/src/backend_factory.test.ts @@ -196,7 +196,7 @@ void describe('Backend', () => { const backend = new BackendFactory({}, rootStack); const clientConfigPartial: DeepPartialAmplifyGeneratedConfigs = { - version: '1.1', + version: '1.2', custom: { someCustomOutput: 'someCustomOutputValue', }, diff --git a/packages/backend/src/backend_factory.ts b/packages/backend/src/backend_factory.ts index 023f52244f..d2c7662df3 100644 --- a/packages/backend/src/backend_factory.ts +++ b/packages/backend/src/backend_factory.ts @@ -33,7 +33,7 @@ const rootStackTypeIdentifier = 'root'; // Client config version that is used by `backend.addOutput()` const DEFAULT_CLIENT_CONFIG_VERSION_FOR_BACKEND_ADD_OUTPUT = - ClientConfigVersionOption.V1_1; + ClientConfigVersionOption.V1_2; /** * Factory that collects and instantiates all the Amplify backend constructs diff --git a/packages/backend/src/engine/custom_outputs_accumulator.test.ts b/packages/backend/src/engine/custom_outputs_accumulator.test.ts index 6962175e4c..dc89639ab8 100644 --- a/packages/backend/src/engine/custom_outputs_accumulator.test.ts +++ b/packages/backend/src/engine/custom_outputs_accumulator.test.ts @@ -59,11 +59,11 @@ void describe('Custom outputs accumulator', () => { ); const configPart1: DeepPartialAmplifyGeneratedConfigs = { - version: '1.1', + version: '1.2', custom: { output1: 'val1' }, }; const configPart2: DeepPartialAmplifyGeneratedConfigs = { - version: '1.1', + version: '1.2', custom: { output2: 'val2' }, }; accumulator.addOutput(configPart1); @@ -115,7 +115,7 @@ void describe('Custom outputs accumulator', () => { assert.throws( () => - accumulator.addOutput({ version: '1.1', custom: { output1: 'val1' } }), + accumulator.addOutput({ version: '1.2', custom: { output1: 'val1' } }), (error: AmplifyUserError) => { assert.strictEqual( error.message, diff --git a/packages/cli/src/commands/generate/outputs/generate_outputs_command.test.ts b/packages/cli/src/commands/generate/outputs/generate_outputs_command.test.ts index 80333f072c..b6aa2065d7 100644 --- a/packages/cli/src/commands/generate/outputs/generate_outputs_command.test.ts +++ b/packages/cli/src/commands/generate/outputs/generate_outputs_command.test.ts @@ -74,7 +74,7 @@ void describe('generate outputs command', () => { assert.equal(generateClientConfigMock.mock.callCount(), 1); assert.deepEqual( generateClientConfigMock.mock.calls[0].arguments[1], - '1.1' // default version + '1.2' // default version ); assert.deepEqual( generateClientConfigMock.mock.calls[0].arguments[2], @@ -97,7 +97,7 @@ void describe('generate outputs command', () => { assert.equal(generateClientConfigMock.mock.callCount(), 1); assert.deepEqual( generateClientConfigMock.mock.calls[0].arguments[1], - '1.1' // default version + '1.2' // default version ); assert.deepStrictEqual( generateClientConfigMock.mock.calls[0].arguments[2], @@ -118,7 +118,7 @@ void describe('generate outputs command', () => { namespace: 'app_id', type: 'branch', }, - '1.1', + '1.2', '/foo/bar', undefined, ] @@ -136,7 +136,7 @@ void describe('generate outputs command', () => { { stackName: 'stack_name', }, - '1.1', + '1.2', '/foo/bar', undefined, ] @@ -154,7 +154,7 @@ void describe('generate outputs command', () => { { stackName: 'stack_name', }, - '1.1', + '1.2', 'foo/bar', undefined, ] @@ -172,7 +172,7 @@ void describe('generate outputs command', () => { { stackName: 'stack_name', }, - '1.1', + '1.2', 'foo/bar', ClientConfigFormat.DART, ] diff --git a/packages/cli/src/commands/sandbox/sandbox_command.test.ts b/packages/cli/src/commands/sandbox/sandbox_command.test.ts index 7b653d877c..c485456549 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command.test.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command.test.ts @@ -427,15 +427,15 @@ void describe('sandbox command', () => { ); }); - void it('sandbox creates an empty client config file if one does not already exist for version 1.1', async (contextual) => { + void it('sandbox creates an empty client config file if one does not already exist for version 1.2', async (contextual) => { contextual.mock.method(fs, 'existsSync', () => false); const writeFileMock = contextual.mock.method(fsp, 'writeFile', () => true); - await commandRunner.runCommand('sandbox --outputs-version 1.1'); + await commandRunner.runCommand('sandbox --outputs-version 1.2'); assert.equal(sandboxStartMock.mock.callCount(), 1); assert.equal(writeFileMock.mock.callCount(), 1); assert.deepStrictEqual( writeFileMock.mock.calls[0].arguments[1], - `{\n "version": "1.1"\n}` + `{\n "version": "1.2"\n}` ); assert.deepStrictEqual( writeFileMock.mock.calls[0].arguments[0], 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 2a3366612c..2ba2996a0e 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 @@ -23,7 +23,7 @@ void describe('sandbox_event_handler_factory', () => { } as unknown as ClientConfigGeneratorAdapter; const clientConfigLifecycleHandler = new ClientConfigLifecycleHandler( clientConfigGeneratorAdapterMock, - '1.1', + '1.2', 'test-out', ClientConfigFormat.JSON ); @@ -73,7 +73,7 @@ void describe('sandbox_event_handler_factory', () => { namespace: 'test', name: 'name', }, - '1.1', + '1.2', 'test-out', 'json', ]); @@ -185,7 +185,7 @@ void describe('sandbox_event_handler_factory', () => { namespace: 'test', name: 'name', }, - '1.1', + '1.2', 'test-out', 'json', ]); diff --git a/packages/client-config/API.md b/packages/client-config/API.md index 40b7882c3e..397419f74f 100644 --- a/packages/client-config/API.md +++ b/packages/client-config/API.md @@ -16,6 +16,9 @@ type AmazonCognitoStandardAttributes = 'address' | 'birthdate' | 'email' | 'fami // @public type AmazonCognitoStandardAttributes_2 = 'address' | 'birthdate' | 'email' | 'family_name' | 'gender' | 'given_name' | 'locale' | 'middle_name' | 'name' | 'nickname' | 'phone_number' | 'picture' | 'preferred_username' | 'profile' | 'sub' | 'updated_at' | 'website' | 'zoneinfo'; +// @public +type AmazonCognitoStandardAttributes_3 = 'address' | 'birthdate' | 'email' | 'family_name' | 'gender' | 'given_name' | 'locale' | 'middle_name' | 'name' | 'nickname' | 'phone_number' | 'picture' | 'preferred_username' | 'profile' | 'sub' | 'updated_at' | 'website' | 'zoneinfo'; + // @public interface AmazonLocationServiceConfig { name?: string; @@ -28,12 +31,38 @@ interface AmazonLocationServiceConfig_2 { style?: string; } +// @public +interface AmazonLocationServiceConfig_3 { + name?: string; + style?: string; +} + // @public type AmazonPinpointChannels = 'IN_APP_MESSAGING' | 'FCM' | 'APNS' | 'EMAIL' | 'SMS'; // @public type AmazonPinpointChannels_2 = 'IN_APP_MESSAGING' | 'FCM' | 'APNS' | 'EMAIL' | 'SMS'; +// @public +type AmazonPinpointChannels_3 = 'IN_APP_MESSAGING' | 'FCM' | 'APNS' | 'EMAIL' | 'SMS'; + +// @public (undocumented) +type AmplifyStorageAccessActions = 'read' | 'get' | 'list' | 'write' | 'delete'; + +// @public +interface AmplifyStorageAccessRule { + // (undocumented) + authenticated?: AmplifyStorageAccessActions[]; + // (undocumented) + entity?: AmplifyStorageAccessActions[]; + // (undocumented) + groups?: AmplifyStorageAccessActions[]; + // (undocumented) + guest?: AmplifyStorageAccessActions[]; + // (undocumented) + resource?: AmplifyStorageAccessActions[]; +} + // @public (undocumented) interface AmplifyStorageBucket { // (undocumented) @@ -42,6 +71,20 @@ interface AmplifyStorageBucket { bucket_name: string; // (undocumented) name: string; + // (undocumented) + paths?: { + [k: string]: AmplifyStorageAccessRule; + }; +} + +// @public (undocumented) +interface AmplifyStorageBucket_2 { + // (undocumented) + aws_region: string; + // (undocumented) + bucket_name: string; + // (undocumented) + name: string; } // @public (undocumented) @@ -88,12 +131,12 @@ export type AuthClientConfig = { interface AWSAmplifyBackendOutputs { analytics?: { amazon_pinpoint?: { - aws_region: AwsRegion; + aws_region: string; app_id: string; }; }; auth?: { - aws_region: AwsRegion; + aws_region: string; user_pool_id: string; user_pool_client_id: string; identity_pool_id?: string; @@ -133,7 +176,7 @@ interface AWSAmplifyBackendOutputs { authorization_types: AwsAppsyncAuthorizationType[]; }; geo?: { - aws_region: AwsRegion; + aws_region: string; maps?: { items: { [k: string]: AmazonLocationServiceConfig; @@ -159,7 +202,7 @@ interface AWSAmplifyBackendOutputs { bucket_name: string; buckets?: AmplifyStorageBucket[]; }; - version: '1.1'; + version: '1.2'; } // @public @@ -235,6 +278,84 @@ interface AWSAmplifyBackendOutputs_2 { storage?: { aws_region: AwsRegion_2; bucket_name: string; + buckets?: AmplifyStorageBucket_2[]; + }; + version: '1.1'; +} + +// @public +interface AWSAmplifyBackendOutputs_3 { + analytics?: { + amazon_pinpoint?: { + aws_region: AwsRegion_3; + app_id: string; + }; + }; + auth?: { + aws_region: AwsRegion_3; + user_pool_id: string; + user_pool_client_id: string; + identity_pool_id?: string; + password_policy?: { + min_length: number; + require_numbers: boolean; + require_lowercase: boolean; + require_uppercase: boolean; + require_symbols: boolean; + }; + oauth?: { + identity_providers: ('GOOGLE' | 'FACEBOOK' | 'LOGIN_WITH_AMAZON' | 'SIGN_IN_WITH_APPLE')[]; + domain: string; + scopes: string[]; + redirect_sign_in_uri: string[]; + redirect_sign_out_uri: string[]; + response_type: 'code' | 'token'; + }; + standard_required_attributes?: AmazonCognitoStandardAttributes_3[]; + username_attributes?: ('email' | 'phone_number' | 'username')[]; + user_verification_types?: ('email' | 'phone_number')[]; + unauthenticated_identities_enabled?: boolean; + mfa_configuration?: 'NONE' | 'OPTIONAL' | 'REQUIRED'; + mfa_methods?: ('SMS' | 'TOTP')[]; + }; + custom?: { + [k: string]: unknown; + }; + data?: { + aws_region: AwsRegion_3; + url: string; + model_introspection?: { + [k: string]: unknown; + }; + api_key?: string; + default_authorization_type: AwsAppsyncAuthorizationType_3; + authorization_types: AwsAppsyncAuthorizationType_3[]; + }; + geo?: { + aws_region: AwsRegion_3; + maps?: { + items: { + [k: string]: AmazonLocationServiceConfig_3; + }; + default: string; + }; + search_indices?: { + items: string[]; + default: string; + }; + geofence_collections?: { + items: string[]; + default: string; + }; + }; + notifications?: { + aws_region: AwsRegion_3; + amazon_pinpoint_app_id: string; + channels: AmazonPinpointChannels_3[]; + }; + storage?: { + aws_region: AwsRegion_3; + bucket_name: string; }; version: '1'; } @@ -245,14 +366,20 @@ type AwsAppsyncAuthorizationType = 'AMAZON_COGNITO_USER_POOLS' | 'API_KEY' | 'AW // @public type AwsAppsyncAuthorizationType_2 = 'AMAZON_COGNITO_USER_POOLS' | 'API_KEY' | 'AWS_IAM' | 'AWS_LAMBDA' | 'OPENID_CONNECT'; +// @public +type AwsAppsyncAuthorizationType_3 = 'AMAZON_COGNITO_USER_POOLS' | 'API_KEY' | 'AWS_IAM' | 'AWS_LAMBDA' | 'OPENID_CONNECT'; + // @public (undocumented) type AwsRegion = string; // @public (undocumented) type AwsRegion_2 = string; +// @public (undocumented) +type AwsRegion_3 = string; + // @public -export type ClientConfig = clientConfigTypesV1_1.AWSAmplifyBackendOutputs | clientConfigTypesV1.AWSAmplifyBackendOutputs; +export type ClientConfig = clientConfigTypesV1_2.AWSAmplifyBackendOutputs | clientConfigTypesV1_1.AWSAmplifyBackendOutputs | clientConfigTypesV1.AWSAmplifyBackendOutputs; // @public (undocumented) export enum ClientConfigFileBaseName { @@ -280,29 +407,44 @@ export enum ClientConfigFormat { export type ClientConfigLegacy = Partial; declare namespace clientConfigTypesV1 { + export { + AmazonCognitoStandardAttributes_3 as AmazonCognitoStandardAttributes, + AwsRegion_3 as AwsRegion, + AwsAppsyncAuthorizationType_3 as AwsAppsyncAuthorizationType, + AmazonPinpointChannels_3 as AmazonPinpointChannels, + AWSAmplifyBackendOutputs_3 as AWSAmplifyBackendOutputs, + AmazonLocationServiceConfig_3 as AmazonLocationServiceConfig + } +} +export { clientConfigTypesV1 } + +declare namespace clientConfigTypesV1_1 { export { AmazonCognitoStandardAttributes_2 as AmazonCognitoStandardAttributes, AwsRegion_2 as AwsRegion, AwsAppsyncAuthorizationType_2 as AwsAppsyncAuthorizationType, AmazonPinpointChannels_2 as AmazonPinpointChannels, AWSAmplifyBackendOutputs_2 as AWSAmplifyBackendOutputs, - AmazonLocationServiceConfig_2 as AmazonLocationServiceConfig + AmazonLocationServiceConfig_2 as AmazonLocationServiceConfig, + AmplifyStorageBucket_2 as AmplifyStorageBucket } } -export { clientConfigTypesV1 } +export { clientConfigTypesV1_1 } -declare namespace clientConfigTypesV1_1 { +declare namespace clientConfigTypesV1_2 { export { AmazonCognitoStandardAttributes, AwsRegion, AwsAppsyncAuthorizationType, AmazonPinpointChannels, + AmplifyStorageAccessActions, AWSAmplifyBackendOutputs, AmazonLocationServiceConfig, - AmplifyStorageBucket + AmplifyStorageBucket, + AmplifyStorageAccessRule } } -export { clientConfigTypesV1_1 } +export { clientConfigTypesV1_2 } // @public (undocumented) export type ClientConfigVersion = `${ClientConfigVersionOption}`; @@ -314,11 +456,13 @@ export enum ClientConfigVersionOption { // (undocumented) V1 = "1", // (undocumented) - V1_1 = "1.1" + V1_1 = "1.1", + // (undocumented) + V1_2 = "1.2" } // @public -export type ClientConfigVersionTemplateType = T extends '1.1' ? clientConfigTypesV1_1.AWSAmplifyBackendOutputs : T extends '1' ? clientConfigTypesV1.AWSAmplifyBackendOutputs : never; +export type ClientConfigVersionTemplateType = T extends '1.2' ? clientConfigTypesV1_2.AWSAmplifyBackendOutputs : T extends '1.1' ? clientConfigTypesV1_1.AWSAmplifyBackendOutputs : T extends '1' ? clientConfigTypesV1.AWSAmplifyBackendOutputs : never; // @public (undocumented) export type CustomClientConfig = { @@ -329,7 +473,7 @@ export type CustomClientConfig = { export const DEFAULT_CLIENT_CONFIG_VERSION: ClientConfigVersion; // @public -export const generateClientConfig: (backendIdentifier: DeployedBackendIdentifier, version: T, awsClientProvider?: AWSClientProvider<{ +export const generateClientConfig: (backendIdentifier: DeployedBackendIdentifier, version: T, awsClientProvider?: AWSClientProvider<{ getS3Client: S3Client; getAmplifyClient: AmplifyClient; getCloudFormationClient: CloudFormationClient; diff --git a/packages/client-config/src/client-config-contributor/client_config_contributor_factory.ts b/packages/client-config/src/client-config-contributor/client_config_contributor_factory.ts index 0c269c8285..6c8fb49d45 100644 --- a/packages/client-config/src/client-config-contributor/client_config_contributor_factory.ts +++ b/packages/client-config/src/client-config-contributor/client_config_contributor_factory.ts @@ -4,9 +4,11 @@ import { CustomClientConfigContributor as Custom1_1, DataClientConfigContributor as Data1_1, StorageClientConfigContributorV1 as Storage1, - StorageClientConfigContributor as Storage1_1, - VersionContributor as VersionContributor1_1, + StorageClientConfigContributorV1_1 as Storage1_1, + StorageClientConfigContributor as Storage1_2, + VersionContributor as VersionContributor1_2, VersionContributorV1, + VersionContributorV1_1, } from './client_config_contributor_v1.js'; import { ClientConfigContributor } from '../client-config-types/client_config_contributor.js'; @@ -31,11 +33,19 @@ export class ClientConfigContributorFactory { private readonly modelIntrospectionSchemaAdapter: ModelIntrospectionSchemaAdapter ) { this.versionedClientConfigContributors = { + [ClientConfigVersionOption.V1_2]: [ + new Auth1_1(), + new Data1_1(this.modelIntrospectionSchemaAdapter), + new Storage1_2(), + new VersionContributor1_2(), + new Custom1_1(), + ], + [ClientConfigVersionOption.V1_1]: [ new Auth1_1(), new Data1_1(this.modelIntrospectionSchemaAdapter), new Storage1_1(), - new VersionContributor1_1(), + new VersionContributorV1_1(), new Custom1_1(), ], @@ -48,12 +58,12 @@ export class ClientConfigContributorFactory { new Custom1_1(), ], - // Legacy config is derived from V1.1 (latest) of unified default config + // Legacy config is derived from V1.2 (latest) of unified default config [ClientConfigVersionOption.V0]: [ new Auth1_1(), new Data1_1(this.modelIntrospectionSchemaAdapter), - new Storage1_1(), - new VersionContributor1_1(), + new Storage1_2(), + new VersionContributor1_2(), new Custom1_1(), ], }; diff --git a/packages/client-config/src/client-config-contributor/client_config_contributor_v1.test.ts b/packages/client-config/src/client-config-contributor/client_config_contributor_v1.test.ts index dfab479b0e..022bea9dd4 100644 --- a/packages/client-config/src/client-config-contributor/client_config_contributor_v1.test.ts +++ b/packages/client-config/src/client-config-contributor/client_config_contributor_v1.test.ts @@ -8,7 +8,7 @@ import { } from './client_config_contributor_v1.js'; import { ClientConfig, - clientConfigTypesV1_1, + clientConfigTypesV1_2, } from '../client-config-types/client_config.js'; import assert from 'node:assert'; import { @@ -74,7 +74,7 @@ void describe('auth client config contributor v1', () => { identity_pool_id: 'testIdentityPoolId', unauthenticated_identities_enabled: true, }, - } as Partial + } as Partial ); }); @@ -99,7 +99,7 @@ void describe('auth client config contributor v1', () => { aws_region: 'testRegion', identity_pool_id: 'testIdentityPoolId', }, - } as Partial + } as Partial ); }); @@ -133,7 +133,7 @@ void describe('auth client config contributor v1', () => { require_uppercase: true, }, }, - } as Partial + } as Partial ); }); @@ -166,7 +166,7 @@ void describe('auth client config contributor v1', () => { require_uppercase: false, }, }, - } as Partial + } as Partial ); }); @@ -236,7 +236,7 @@ void describe('auth client config contributor v1', () => { response_type: 'code', }, }, - } as Partial + } as Partial ); }); @@ -300,7 +300,7 @@ void describe('auth client config contributor v1', () => { response_type: 'code', }, }, - } as Partial + } as Partial ); }); @@ -358,7 +358,7 @@ void describe('auth client config contributor v1', () => { response_type: 'code', }, }, - } as Pick; + } as Pick; void it('returns translated config when mfa is disabled', () => { const contributor = new AuthClientConfigContributor(); @@ -459,7 +459,7 @@ void describe('data client config contributor v1', () => { url: 'testApiEndpoint', aws_region: 'us-east-1', }, - } as Partial); + } as Partial); }); void it('returns translated config with model introspection when resolvable', async () => { @@ -507,7 +507,7 @@ void describe('data client config contributor v1', () => { enums: {}, }, }, - } as Partial); + } as Partial); }); }); @@ -540,6 +540,12 @@ void describe('storage client config contributor v1', () => { name: 'testName', bucketName: 'testBucketName', storageRegion: 'testRegion', + paths: { + 'path/*': { + guest: ['get', 'list'], + authenticated: ['read', 'write', 'delete'], + }, + }, }), ]); assert.deepStrictEqual( @@ -562,6 +568,12 @@ void describe('storage client config contributor v1', () => { name: 'testName', bucket_name: 'testBucketName', aws_region: 'testRegion', + paths: { + 'path/*': { + guest: ['get', 'list'], + authenticated: ['read', 'write', 'delete'], + }, + }, }, ], }, @@ -613,6 +625,6 @@ void describe('Custom client config contributor v1', () => { void describe('Custom client config contributor v1', () => { void it('contributes the version correctly', () => { - assert.deepEqual(new VersionContributor().contribute(), { version: '1.1' }); + assert.deepEqual(new VersionContributor().contribute(), { version: '1.2' }); }); }); diff --git a/packages/client-config/src/client-config-contributor/client_config_contributor_v1.ts b/packages/client-config/src/client-config-contributor/client_config_contributor_v1.ts index 30ce2a0649..425775a978 100644 --- a/packages/client-config/src/client-config-contributor/client_config_contributor_v1.ts +++ b/packages/client-config/src/client-config-contributor/client_config_contributor_v1.ts @@ -9,18 +9,34 @@ import { import { ClientConfig, ClientConfigVersionOption, + clientConfigTypesV1, clientConfigTypesV1_1, + clientConfigTypesV1_2, } from '../client-config-types/client_config.js'; import { ModelIntrospectionSchemaAdapter } from '../model_introspection_schema_adapter.js'; import { AwsAppsyncAuthorizationType } from '../client-config-schema/client_config_v1.1.js'; +import { AmplifyStorageAccessRule } from '../client-config-schema/client_config_v1.2.js'; // All categories client config contributors are included here to mildly enforce them using // the same schema (version and other types) /** - * Translator for the version number of ClientConfig of V1.1 + * Translator for the version number of ClientConfig of V1.2 */ export class VersionContributor implements ClientConfigContributor { + /** + * Return the version of the schema types that this contributor uses + */ + contribute = (): ClientConfig => { + return { version: ClientConfigVersionOption.V1_2 }; + }; +} + +/** + * Translator for the version number of ClientConfig of V1.1 + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export class VersionContributorV1_1 implements ClientConfigContributor { /** * Return the version of the schema types that this contributor uses */ @@ -66,7 +82,7 @@ export class AuthClientConfigContributor implements ClientConfigContributor { obj[key] = JSON.parse(value); }; - const authClientConfig: Partial = + const authClientConfig: Partial = {}; authClientConfig.auth = { @@ -257,9 +273,59 @@ export class DataClientConfigContributor implements ClientConfigContributor { } /** - * Translator for the Storage portion of ClientConfig in V1.1 + * Translator for the Storage portion of ClientConfig in V1.2 */ +// eslint-disable-next-line @typescript-eslint/naming-convention export class StorageClientConfigContributor implements ClientConfigContributor { + /** + * Given some BackendOutput, contribute the Storage portion of the client config + */ + contribute = ({ + [storageOutputKey]: storageOutput, + }: UnifiedBackendOutput): Partial | Record => { + if (storageOutput === undefined) { + return {}; + } + const config: Partial = {}; + const bucketsStringArray = JSON.parse( + storageOutput.payload.buckets ?? '[]' + ); + config.storage = { + aws_region: storageOutput.payload.storageRegion, + bucket_name: storageOutput.payload.bucketName, + buckets: bucketsStringArray + .map((b: string) => JSON.parse(b)) + .map( + ({ + name, + bucketName, + storageRegion, + paths, + }: { + name: string; + bucketName: string; + storageRegion: string; + paths: Record; + }) => ({ + name, + bucket_name: bucketName, + aws_region: storageRegion, + paths, + }) + ), + }; + + return config; + }; +} + +/** + * Translator for the Storage portion of ClientConfig in V1.1 + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export class StorageClientConfigContributorV1_1 + implements ClientConfigContributor +{ /** * Given some BackendOutput, contribute the Storage portion of the client config */ @@ -314,7 +380,7 @@ export class StorageClientConfigContributorV1 if (storageOutput === undefined) { return {}; } - const config: Partial = {}; + const config: Partial = {}; config.storage = { aws_region: storageOutput.payload.storageRegion, diff --git a/packages/client-config/src/client-config-schema/client_config_v1.2.ts b/packages/client-config/src/client-config-schema/client_config_v1.2.ts new file mode 100644 index 0000000000..f9aa397d97 --- /dev/null +++ b/packages/client-config/src/client-config-schema/client_config_v1.2.ts @@ -0,0 +1,272 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * Amazon Cognito standard attributes for users -- https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html + */ +export type AmazonCognitoStandardAttributes = + | 'address' + | 'birthdate' + | 'email' + | 'family_name' + | 'gender' + | 'given_name' + | 'locale' + | 'middle_name' + | 'name' + | 'nickname' + | 'phone_number' + | 'picture' + | 'preferred_username' + | 'profile' + | 'sub' + | 'updated_at' + | 'website' + | 'zoneinfo'; +export type AwsRegion = string; +/** + * List of supported auth types for AWS AppSync + */ +export type AwsAppsyncAuthorizationType = + | 'AMAZON_COGNITO_USER_POOLS' + | 'API_KEY' + | 'AWS_IAM' + | 'AWS_LAMBDA' + | 'OPENID_CONNECT'; +/** + * supported channels for Amazon Pinpoint + */ +export type AmazonPinpointChannels = + | 'IN_APP_MESSAGING' + | 'FCM' + | 'APNS' + | 'EMAIL' + | 'SMS'; +export type AmplifyStorageAccessActions = + | 'read' + | 'get' + | 'list' + | 'write' + | 'delete'; + +/** + * Config format for Amplify Gen 2 client libraries to communicate with backend services. + */ +export interface AWSAmplifyBackendOutputs { + /** + * Version of this schema + */ + version: '1.2'; + /** + * Outputs manually specified by developers for use with frontend library + */ + analytics?: { + amazon_pinpoint?: { + /** + * AWS Region of Amazon Pinpoint resources + */ + aws_region: string; + app_id: string; + }; + }; + /** + * Outputs generated from defineAuth + */ + auth?: { + /** + * AWS Region of Amazon Cognito resources + */ + aws_region: string; + /** + * Cognito User Pool ID + */ + user_pool_id: string; + /** + * Cognito User Pool Client ID + */ + user_pool_client_id: string; + /** + * Cognito Identity Pool ID + */ + identity_pool_id?: string; + /** + * Cognito User Pool password policy + */ + password_policy?: { + min_length: number; + require_numbers: boolean; + require_lowercase: boolean; + require_uppercase: boolean; + require_symbols: boolean; + }; + oauth?: { + /** + * Identity providers set on Cognito User Pool + * + * @minItems 0 + */ + identity_providers: ( + | 'GOOGLE' + | 'FACEBOOK' + | 'LOGIN_WITH_AMAZON' + | 'SIGN_IN_WITH_APPLE' + )[]; + /** + * Domain used for identity providers + */ + domain: string; + /** + * @minItems 0 + */ + scopes: string[]; + /** + * URIs used to redirect after signing in using an identity provider + * + * @minItems 1 + */ + redirect_sign_in_uri: string[]; + /** + * URIs used to redirect after signing out + * + * @minItems 1 + */ + redirect_sign_out_uri: string[]; + response_type: 'code' | 'token'; + }; + /** + * Cognito User Pool standard attributes required for signup + * + * @minItems 0 + */ + standard_required_attributes?: AmazonCognitoStandardAttributes[]; + /** + * Cognito User Pool username attributes + * + * @minItems 1 + */ + username_attributes?: ('email' | 'phone_number' | 'username')[]; + user_verification_types?: ('email' | 'phone_number')[]; + unauthenticated_identities_enabled?: boolean; + mfa_configuration?: 'NONE' | 'OPTIONAL' | 'REQUIRED'; + mfa_methods?: ('SMS' | 'TOTP')[]; + }; + /** + * Outputs generated from defineData + */ + data?: { + aws_region: AwsRegion; + /** + * AppSync endpoint URL + */ + url: string; + /** + * generated model introspection schema for use with generateClient + */ + model_introspection?: { + [k: string]: unknown; + }; + api_key?: string; + default_authorization_type: AwsAppsyncAuthorizationType; + authorization_types: AwsAppsyncAuthorizationType[]; + }; + /** + * Outputs manually specified by developers for use with frontend library + */ + geo?: { + /** + * AWS Region of Amazon Location Service resources + */ + aws_region: string; + /** + * Maps from Amazon Location Service + */ + maps?: { + items: { + [k: string]: AmazonLocationServiceConfig; + }; + default: string; + }; + /** + * Location search (search by places, addresses, coordinates) + */ + search_indices?: { + /** + * @minItems 1 + */ + items: string[]; + default: string; + }; + /** + * Geofencing (visualize virtual perimeters) + */ + geofence_collections?: { + /** + * @minItems 1 + */ + items: string[]; + default: string; + }; + }; + /** + * Outputs manually specified by developers for use with frontend library + */ + notifications?: { + aws_region: AwsRegion; + amazon_pinpoint_app_id: string; + /** + * @minItems 1 + */ + channels: AmazonPinpointChannels[]; + }; + /** + * Outputs generated from defineStorage + */ + storage?: { + aws_region: AwsRegion; + bucket_name: string; + buckets?: AmplifyStorageBucket[]; + }; + /** + * Outputs generated from backend.addOutput({ custom: }) + */ + custom?: { + [k: string]: unknown; + }; +} +/** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` ".*". + */ +export interface AmazonLocationServiceConfig { + /** + * Map resource name + */ + name?: string; + /** + * Map style + */ + style?: string; +} +export interface AmplifyStorageBucket { + name: string; + bucket_name: string; + aws_region: string; + paths?: { + [k: string]: AmplifyStorageAccessRule; + }; +} +/** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` ".*". + */ +export interface AmplifyStorageAccessRule { + guest?: AmplifyStorageAccessActions[]; + authenticated?: AmplifyStorageAccessActions[]; + groups?: AmplifyStorageAccessActions[]; + entity?: AmplifyStorageAccessActions[]; + resource?: AmplifyStorageAccessActions[]; +} diff --git a/packages/client-config/src/client-config-schema/schema_v1.2.json b/packages/client-config/src/client-config-schema/schema_v1.2.json new file mode 100644 index 0000000000..b85a10ff30 --- /dev/null +++ b/packages/client-config/src/client-config-schema/schema_v1.2.json @@ -0,0 +1,476 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://amplify.aws/2024-02/outputs-schema.json", + "title": "AWS Amplify Backend Outputs", + "description": "Config format for Amplify Gen 2 client libraries to communicate with backend services.", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "description": "JSON schema", + "type": "string" + }, + "version": { + "description": "Version of this schema", + "const": "1.2" + }, + "analytics": { + "description": "Outputs manually specified by developers for use with frontend library", + "type": "object", + "additionalProperties": false, + "properties": { + "amazon_pinpoint": { + "type": "object", + "additionalProperties": false, + "properties": { + "aws_region": { + "description": "AWS Region of Amazon Pinpoint resources", + "$ref": "#/$defs/aws_region" + }, + "app_id": { + "type": "string" + } + }, + "required": ["aws_region", "app_id"] + } + } + }, + "auth": { + "description": "Outputs generated from defineAuth", + "type": "object", + "additionalProperties": false, + "properties": { + "aws_region": { + "description": "AWS Region of Amazon Cognito resources", + "$ref": "#/$defs/aws_region" + }, + "user_pool_id": { + "description": "Cognito User Pool ID", + "type": "string" + }, + "user_pool_client_id": { + "description": "Cognito User Pool Client ID", + "type": "string" + }, + "identity_pool_id": { + "description": "Cognito Identity Pool ID", + "type": "string" + }, + "password_policy": { + "description": "Cognito User Pool password policy", + "type": "object", + "additionalProperties": false, + "properties": { + "min_length": { + "type": "integer", + "minimum": 6, + "maximum": 99 + }, + "require_numbers": { + "type": "boolean" + }, + "require_lowercase": { + "type": "boolean" + }, + "require_uppercase": { + "type": "boolean" + }, + "require_symbols": { + "type": "boolean" + } + }, + "required": [ + "min_length", + "require_numbers", + "require_lowercase", + "require_uppercase", + "require_symbols" + ] + }, + "oauth": { + "type": "object", + "additionalProperties": false, + "properties": { + "identity_providers": { + "description": "Identity providers set on Cognito User Pool", + "type": "array", + "items": { + "type": "string", + "enum": [ + "GOOGLE", + "FACEBOOK", + "LOGIN_WITH_AMAZON", + "SIGN_IN_WITH_APPLE" + ] + }, + "minItems": 0, + "uniqueItems": true + }, + "domain": { + "description": "Domain used for identity providers", + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 0, + "uniqueItems": true + }, + "redirect_sign_in_uri": { + "description": "URIs used to redirect after signing in using an identity provider", + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "redirect_sign_out_uri": { + "description": "URIs used to redirect after signing out", + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "response_type": { + "type": "string", + "enum": ["code", "token"] + } + }, + "required": [ + "identity_providers", + "domain", + "scopes", + "redirect_sign_in_uri", + "redirect_sign_out_uri", + "response_type" + ] + }, + "standard_required_attributes": { + "description": "Cognito User Pool standard attributes required for signup", + "type": "array", + "items": { + "$ref": "#/$defs/amazon_cognito_standard_attributes" + }, + "minItems": 0, + "uniqueItems": true + }, + "username_attributes": { + "description": "Cognito User Pool username attributes", + "type": "array", + "items": { + "type": "string", + "enum": ["email", "phone_number", "username"] + }, + "minItems": 1, + "uniqueItems": true + }, + "user_verification_types": { + "type": "array", + "items": { + "type": "string", + "enum": ["email", "phone_number"] + } + }, + "unauthenticated_identities_enabled": { + "type": "boolean", + "default": true + }, + "mfa_configuration": { + "type": "string", + "enum": ["NONE", "OPTIONAL", "REQUIRED"] + }, + "mfa_methods": { + "type": "array", + "items": { + "enum": ["SMS", "TOTP"] + } + } + }, + "required": ["aws_region", "user_pool_id", "user_pool_client_id"] + }, + "data": { + "description": "Outputs generated from defineData", + "type": "object", + "additionalProperties": false, + "properties": { + "aws_region": { + "$ref": "#/$defs/aws_region" + }, + "url": { + "description": "AppSync endpoint URL", + "type": "string" + }, + "model_introspection": { + "description": "generated model introspection schema for use with generateClient", + "type": "object" + }, + "api_key": { + "type": "string" + }, + "default_authorization_type": { + "$ref": "#/$defs/aws_appsync_authorization_type" + }, + "authorization_types": { + "type": "array", + "items": { + "$ref": "#/$defs/aws_appsync_authorization_type" + } + } + }, + "required": [ + "aws_region", + "url", + "default_authorization_type", + "authorization_types" + ] + }, + "geo": { + "description": "Outputs manually specified by developers for use with frontend library", + "type": "object", + "additionalProperties": false, + "properties": { + "aws_region": { + "description": "AWS Region of Amazon Location Service resources", + "$ref": "#/$defs/aws_region" + }, + "maps": { + "description": "Maps from Amazon Location Service", + "type": "object", + "additionalProperties": false, + "properties": { + "items": { + "type": "object", + "additionalProperties": false, + "propertyNames": { + "description": "Amazon Location Service Map name", + "type": "string" + }, + "patternProperties": { + ".*": { + "$ref": "#/$defs/amazon_location_service_config" + } + } + }, + "default": { + "type": "string" + } + }, + "required": ["items", "default"] + }, + "search_indices": { + "description": "Location search (search by places, addresses, coordinates)", + "type": "object", + "additionalProperties": false, + "properties": { + "items": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "description": "Actual search name", + "type": "string" + } + }, + "default": { + "type": "string" + } + }, + "required": ["items", "default"] + }, + "geofence_collections": { + "description": "Geofencing (visualize virtual perimeters)", + "type": "object", + "additionalProperties": false, + "properties": { + "items": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "description": "Geofence name", + "type": "string" + } + }, + "default": { + "type": "string" + } + }, + "required": ["items", "default"] + } + }, + "required": ["aws_region"] + }, + "notifications": { + "type": "object", + "description": "Outputs manually specified by developers for use with frontend library", + "additionalProperties": false, + "properties": { + "aws_region": { + "$ref": "#/$defs/aws_region" + }, + "amazon_pinpoint_app_id": { + "type": "string" + }, + "channels": { + "type": "array", + "items": { + "$ref": "#/$defs/amazon_pinpoint_channels" + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": ["aws_region", "amazon_pinpoint_app_id", "channels"] + }, + "storage": { + "type": "object", + "description": "Outputs generated from defineStorage", + "additionalProperties": false, + "properties": { + "aws_region": { + "$ref": "#/$defs/aws_region" + }, + "bucket_name": { + "type": "string" + }, + "buckets": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_bucket" + } + } + }, + "required": ["aws_region", "bucket_name"] + }, + "custom": { + "description": "Outputs generated from backend.addOutput({ custom: })", + "type": "object" + } + }, + "required": ["version"], + "$defs": { + "amplify_storage_access_actions": { + "type": "string", + "enum": ["read", "get", "list", "write", "delete"] + }, + "amplify_storage_access_rule": { + "type": "object", + "additionalProperties": false, + "properties": { + "guest": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_access_actions" + } + }, + "authenticated": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_access_actions" + } + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_access_actions" + } + }, + "entity": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_access_actions" + } + }, + "resource": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_access_actions" + } + } + } + }, + "amplify_storage_bucket": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "bucket_name": { + "type": "string" + }, + "aws_region": { + "type": "string" + }, + "paths": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".*": { + "$ref": "#/$defs/amplify_storage_access_rule" + } + } + } + }, + "required": ["bucket_name", "aws_region", "name"] + }, + "aws_region": { + "type": "string" + }, + "amazon_cognito_standard_attributes": { + "description": "Amazon Cognito standard attributes for users -- https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html", + "type": "string", + "enum": [ + "address", + "birthdate", + "email", + "family_name", + "gender", + "given_name", + "locale", + "middle_name", + "name", + "nickname", + "phone_number", + "picture", + "preferred_username", + "profile", + "sub", + "updated_at", + "website", + "zoneinfo" + ] + }, + "aws_appsync_authorization_type": { + "description": "List of supported auth types for AWS AppSync", + "type": "string", + "enum": [ + "AMAZON_COGNITO_USER_POOLS", + "API_KEY", + "AWS_IAM", + "AWS_LAMBDA", + "OPENID_CONNECT" + ] + }, + "amazon_location_service_config": { + "type": "object", + "additionalProperties": false, + "properties": { + "style": { + "description": "Map style", + "type": "string" + } + } + }, + "amazon_pinpoint_channels": { + "description": "supported channels for Amazon Pinpoint", + "type": "string", + "enum": ["IN_APP_MESSAGING", "FCM", "APNS", "EMAIL", "SMS"] + } + } +} diff --git a/packages/client-config/src/client-config-types/client_config.ts b/packages/client-config/src/client-config-types/client_config.ts index d9cc71e1aa..5467484d67 100644 --- a/packages/client-config/src/client-config-types/client_config.ts +++ b/packages/client-config/src/client-config-types/client_config.ts @@ -9,8 +9,10 @@ import { NotificationsClientConfig } from './notifications_client_config.js'; // Versions of new unified config schemas import * as clientConfigTypesV1 from '../client-config-schema/client_config_v1.js'; -// eslint-disable-next-line @typescript-eslint/naming-convention +/* eslint-disable @typescript-eslint/naming-convention */ import * as clientConfigTypesV1_1 from '../client-config-schema/client_config_v1.1.js'; +import * as clientConfigTypesV1_2 from '../client-config-schema/client_config_v1.2.js'; +/* eslint-enable @typescript-eslint/naming-convention */ /** * Merged type of all category client config legacy types @@ -32,22 +34,24 @@ export type ClientConfigLegacy = Partial< * ClientConfig = clientConfigTypesV1.AWSAmplifyBackendOutputs | clientConfigTypesV2.AWSAmplifyBackendOutputs; */ export type ClientConfig = + | clientConfigTypesV1_2.AWSAmplifyBackendOutputs | clientConfigTypesV1_1.AWSAmplifyBackendOutputs | clientConfigTypesV1.AWSAmplifyBackendOutputs; -export { clientConfigTypesV1, clientConfigTypesV1_1 }; +export { clientConfigTypesV1, clientConfigTypesV1_1, clientConfigTypesV1_2 }; export enum ClientConfigVersionOption { V0 = '0', // Legacy client config V1 = '1', V1_1 = '1.1', + V1_2 = '1.2', } export type ClientConfigVersion = `${ClientConfigVersionOption}`; // Client config version that is generated by default if customers didn't specify one export const DEFAULT_CLIENT_CONFIG_VERSION: ClientConfigVersion = - ClientConfigVersionOption.V1_1; + ClientConfigVersionOption.V1_2; /** * Return type of `getClientConfig`. This types narrow the returned client config version @@ -60,7 +64,9 @@ export const DEFAULT_CLIENT_CONFIG_VERSION: ClientConfigVersion = * ? clientConfigTypesV2.AWSAmplifyBackendOutputs * : never; */ -export type ClientConfigVersionTemplateType = T extends '1.1' +export type ClientConfigVersionTemplateType = T extends '1.2' + ? clientConfigTypesV1_2.AWSAmplifyBackendOutputs + : T extends '1.1' ? clientConfigTypesV1_1.AWSAmplifyBackendOutputs : T extends '1' ? clientConfigTypesV1.AWSAmplifyBackendOutputs diff --git a/packages/client-config/src/client-config-writer/client_config_formatter_default.test.ts b/packages/client-config/src/client-config-writer/client_config_formatter_default.test.ts index 72023851a4..e605ef8967 100644 --- a/packages/client-config/src/client-config-writer/client_config_formatter_default.test.ts +++ b/packages/client-config/src/client-config-writer/client_config_formatter_default.test.ts @@ -13,7 +13,7 @@ void describe('client config formatter', () => { const sampleIdentityPoolId = 'test_identity_pool_id'; const sampleUserPoolClientId = 'test_user_pool_client_id'; const clientConfig: ClientConfig = { - version: '1.1', + version: '1.2', auth: { aws_region: sampleRegion, identity_pool_id: sampleIdentityPoolId, @@ -23,7 +23,7 @@ void describe('client config formatter', () => { }; const expectedConfigReturned: ClientConfig = { - version: '1.1', + version: '1.2', auth: { aws_region: sampleRegion, identity_pool_id: sampleIdentityPoolId, diff --git a/packages/client-config/src/client-config-writer/client_config_formatter_legacy.test.ts b/packages/client-config/src/client-config-writer/client_config_formatter_legacy.test.ts index 479c542efa..737257273d 100644 --- a/packages/client-config/src/client-config-writer/client_config_formatter_legacy.test.ts +++ b/packages/client-config/src/client-config-writer/client_config_formatter_legacy.test.ts @@ -20,7 +20,7 @@ void describe('client config formatter', () => { const sampleUserPoolId = randomUUID(); const clientConfig: ClientConfig = { - version: '1.1', + version: '1.2', auth: { aws_region: sampleRegion, identity_pool_id: sampleIdentityPoolId, diff --git a/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.test.ts b/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.test.ts index 1d564740a0..dda5c288a9 100644 --- a/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.test.ts +++ b/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.test.ts @@ -26,7 +26,7 @@ void describe('ClientConfigLegacyConverter', () => { version: '3' as any, }), new AmplifyFault('UnsupportedClientConfigVersionFault', { - message: 'Only version 1.1 of ClientConfig is supported.', + message: 'Only version 1.2 of ClientConfig is supported.', }) ); }); @@ -35,7 +35,7 @@ void describe('ClientConfigLegacyConverter', () => { const converter = new ClientConfigLegacyConverter(); const v1Config: ClientConfig = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_2, auth: { identity_pool_id: 'testIdentityPoolId', user_pool_id: 'testUserPoolId', @@ -133,7 +133,7 @@ void describe('ClientConfigLegacyConverter', () => { const converter = new ClientConfigLegacyConverter(); const v1Config: ClientConfig = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_2, data: { aws_region: 'testRegion', url: 'testUrl', @@ -274,7 +274,7 @@ void describe('ClientConfigLegacyConverter', () => { const converter = new ClientConfigLegacyConverter(); const v1Config: ClientConfig = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_2, storage: { aws_region: 'testRegion', bucket_name: 'testBucket', @@ -296,7 +296,7 @@ void describe('ClientConfigLegacyConverter', () => { const converter = new ClientConfigLegacyConverter(); const v1Config: ClientConfig = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_2, custom: { customKey: { customNestedKey: { @@ -327,7 +327,7 @@ void describe('ClientConfigLegacyConverter', () => { const converter = new ClientConfigLegacyConverter(); const v1Config: ClientConfig = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_2, analytics: { amazon_pinpoint: { aws_region: 'testRegion', @@ -356,7 +356,7 @@ void describe('ClientConfigLegacyConverter', () => { const converter = new ClientConfigLegacyConverter(); const v1Config: ClientConfig = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_2, geo: { aws_region: 'testRegion', maps: { @@ -409,7 +409,7 @@ void describe('ClientConfigLegacyConverter', () => { const converter = new ClientConfigLegacyConverter(); let v1Config: ClientConfig = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_2, notifications: { amazon_pinpoint_app_id: 'testAppId', aws_region: 'testRegion', @@ -452,7 +452,7 @@ void describe('ClientConfigLegacyConverter', () => { // both APNS and FCM cannot be specified together as they both map to Push. v1Config = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_2, notifications: { amazon_pinpoint_app_id: 'testAppId', aws_region: 'testRegion', diff --git a/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.ts b/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.ts index db61d1b1de..3131b041ed 100644 --- a/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.ts +++ b/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.ts @@ -2,7 +2,7 @@ import { AmplifyFault } from '@aws-amplify/platform-core'; import { ClientConfig, ClientConfigLegacy, - clientConfigTypesV1_1, + clientConfigTypesV1_2, } from '../client-config-types/client_config.js'; import { @@ -22,10 +22,10 @@ export class ClientConfigLegacyConverter { * Converts client config to a shape consumable by legacy libraries. */ convertToLegacyConfig = (clientConfig: ClientConfig): ClientConfigLegacy => { - // We can only convert from V1.1 of ClientConfig. For everything else, throw - if (!this.isClientConfigV1_1(clientConfig)) { + // We can only convert from V1.2 of ClientConfig. For everything else, throw + if (!this.isClientConfigV1_2(clientConfig)) { throw new AmplifyFault('UnsupportedClientConfigVersionFault', { - message: 'Only version 1.1 of ClientConfig is supported.', + message: 'Only version 1.2 of ClientConfig is supported.', }); } @@ -274,9 +274,9 @@ export class ClientConfigLegacyConverter { }; // eslint-disable-next-line @typescript-eslint/naming-convention - isClientConfigV1_1 = ( + isClientConfigV1_2 = ( clientConfig: ClientConfig - ): clientConfig is clientConfigTypesV1_1.AWSAmplifyBackendOutputs => { - return clientConfig.version === '1.1'; + ): clientConfig is clientConfigTypesV1_2.AWSAmplifyBackendOutputs => { + return clientConfig.version === '1.2'; }; } diff --git a/packages/client-config/src/client-config-writer/client_config_writer.test.ts b/packages/client-config/src/client-config-writer/client_config_writer.test.ts index 69697ebe3e..7f3771224d 100644 --- a/packages/client-config/src/client-config-writer/client_config_writer.test.ts +++ b/packages/client-config/src/client-config-writer/client_config_writer.test.ts @@ -42,7 +42,7 @@ void describe('client config writer', () => { }); const clientConfig: ClientConfig = { - version: '1.1', + version: '1.2', auth: { aws_region: sampleRegion, identity_pool_id: sampleIdentityPoolId, diff --git a/packages/client-config/src/generate_empty_client_config_to_file.test.ts b/packages/client-config/src/generate_empty_client_config_to_file.test.ts index 2552a1309a..34dbbc9f82 100644 --- a/packages/client-config/src/generate_empty_client_config_to_file.test.ts +++ b/packages/client-config/src/generate_empty_client_config_to_file.test.ts @@ -30,15 +30,15 @@ void describe('generate empty client config to file', () => { path.join(process.cwd(), 'userOutDir', 'amplifyconfiguration.ts') ); }); - void it('correctly generates an empty file for client config version 1.1', async () => { + void it('correctly generates an empty file for client config version 1.2', async () => { await generateEmptyClientConfigToFile( - ClientConfigVersionOption.V1_1, + ClientConfigVersionOption.V1_2, 'userOutDir' ); assert.equal(writeFileMock.mock.callCount(), 1); assert.deepStrictEqual( writeFileMock.mock.calls[0].arguments[1], - `{\n "version": "1.1"\n}` + `{\n "version": "1.2"\n}` ); assert.deepStrictEqual( writeFileMock.mock.calls[0].arguments[0], diff --git a/packages/client-config/src/generate_empty_client_config_to_file.ts b/packages/client-config/src/generate_empty_client_config_to_file.ts index 039cf781c4..260dad6173 100644 --- a/packages/client-config/src/generate_empty_client_config_to_file.ts +++ b/packages/client-config/src/generate_empty_client_config_to_file.ts @@ -15,7 +15,7 @@ export const generateEmptyClientConfigToFile = async ( format?: ClientConfigFormat ): Promise => { const clientConfig: ClientConfig = { - version: '1.1', + version: '1.2', }; return writeClientConfigToFile(clientConfig, version, outDir, format); }; diff --git a/packages/client-config/src/unified_client_config_generator.test.ts b/packages/client-config/src/unified_client_config_generator.test.ts index 4e6f0b0aed..c466311a68 100644 --- a/packages/client-config/src/unified_client_config_generator.test.ts +++ b/packages/client-config/src/unified_client_config_generator.test.ts @@ -26,6 +26,115 @@ const stubClientProvider = { }; void describe('UnifiedClientConfigGenerator', () => { void describe('generateClientConfig', () => { + void it('transforms backend output into client config for V1.2', async () => { + const stubOutput: UnifiedBackendOutput = { + [platformOutputKey]: { + version: '1', + payload: { + deploymentType: 'branch', + region: 'us-east-1', + }, + }, + [authOutputKey]: { + version: '1', + payload: { + identityPoolId: 'testIdentityPoolId', + userPoolId: 'testUserPoolId', + webClientId: 'testWebClientId', + authRegion: 'us-east-1', + passwordPolicyMinLength: '8', + passwordPolicyRequirements: + '["REQUIRES_NUMBERS","REQUIRES_LOWERCASE","REQUIRES_UPPERCASE"]', + mfaTypes: '["SMS","TOTP"]', + mfaConfiguration: 'OPTIONAL', + verificationMechanisms: '["email","phone_number"]', + usernameAttributes: '["email"]', + signupAttributes: '["email"]', + allowUnauthenticatedIdentities: 'true', + }, + }, + [graphqlOutputKey]: { + version: '1', + payload: { + awsAppsyncApiEndpoint: 'testApiEndpoint', + awsAppsyncRegion: 'us-east-1', + awsAppsyncAuthenticationType: 'API_KEY', + awsAppsyncAdditionalAuthenticationTypes: 'API_KEY', + awsAppsyncConflictResolutionMode: 'AUTO_MERGE', + awsAppsyncApiKey: 'testApiKey', + awsAppsyncApiId: 'testApiId', + amplifyApiModelSchemaS3Uri: 'testApiSchemaUri', + }, + }, + [customOutputKey]: { + version: '1', + payload: { + customOutputs: JSON.stringify({ + custom: { + output1: 'val1', + output2: 'val2', + }, + }), + }, + }, + }; + const outputRetrieval = mock.fn(async () => stubOutput); + const modelSchemaAdapter = new ModelIntrospectionSchemaAdapter( + stubClientProvider + ); + + mock.method( + modelSchemaAdapter, + 'getModelIntrospectionSchemaFromS3Uri', + () => undefined + ); + const configContributors = new ClientConfigContributorFactory( + modelSchemaAdapter + ).getContributors('1.2'); + const clientConfigGenerator = new UnifiedClientConfigGenerator( + outputRetrieval, + configContributors + ); + const result = await clientConfigGenerator.generateClientConfig(); + const expectedClientConfig: ClientConfig = { + auth: { + user_pool_id: 'testUserPoolId', + aws_region: 'us-east-1', + user_pool_client_id: 'testWebClientId', + identity_pool_id: 'testIdentityPoolId', + mfa_methods: ['SMS', 'TOTP'], + standard_required_attributes: ['email'], + username_attributes: ['email'], + user_verification_types: ['email', 'phone_number'], + mfa_configuration: 'OPTIONAL', + + password_policy: { + min_length: 8, + require_lowercase: true, + require_numbers: true, + require_symbols: false, + require_uppercase: true, + }, + + unauthenticated_identities_enabled: true, + }, + data: { + url: 'testApiEndpoint', + aws_region: 'us-east-1', + api_key: 'testApiKey', + default_authorization_type: 'API_KEY', + authorization_types: ['API_KEY'], + }, + custom: { + output1: 'val1', + output2: 'val2', + }, + version: '1.2', + }; + + assert.deepStrictEqual(result, expectedClientConfig); + }); + void it('transforms backend output into client config for V1.1', async () => { const stubOutput: UnifiedBackendOutput = { [platformOutputKey]: { @@ -297,7 +406,7 @@ void describe('UnifiedClientConfigGenerator', () => { ); const configContributors = new ClientConfigContributorFactory( modelSchemaAdapter - ).getContributors('1.1'); //Generate with new configuration format + ).getContributors('1.2'); //Generate with new configuration format const clientConfigGenerator = new UnifiedClientConfigGenerator( outputRetrieval, configContributors @@ -329,7 +438,7 @@ void describe('UnifiedClientConfigGenerator', () => { output1: 'val1', output2: 'val2', }, - version: '1.1', // The max version prevails + version: '1.2', // The max version prevails }; assert.deepStrictEqual(result, expectedClientConfig); @@ -368,7 +477,7 @@ void describe('UnifiedClientConfigGenerator', () => { ); const configContributors = new ClientConfigContributorFactory( modelSchemaAdapter - ).getContributors('1.1'); + ).getContributors('1.2'); const clientConfigGenerator = new UnifiedClientConfigGenerator( outputRetrieval, @@ -400,7 +509,7 @@ void describe('UnifiedClientConfigGenerator', () => { const configContributors = new ClientConfigContributorFactory( modelSchemaAdapter - ).getContributors('1.1'); + ).getContributors('1.2'); const clientConfigGenerator = new UnifiedClientConfigGenerator( outputRetrieval, @@ -432,7 +541,7 @@ void describe('UnifiedClientConfigGenerator', () => { const configContributors = new ClientConfigContributorFactory( modelSchemaAdapter - ).getContributors('1.1'); + ).getContributors('1.2'); const clientConfigGenerator = new UnifiedClientConfigGenerator( outputRetrieval, @@ -495,7 +604,7 @@ void describe('UnifiedClientConfigGenerator', () => { const configContributors = new ClientConfigContributorFactory( modelSchemaAdapter - ).getContributors('1.1'); + ).getContributors('1.2'); const clientConfigGenerator = new UnifiedClientConfigGenerator( outputRetrieval, @@ -528,7 +637,7 @@ void describe('UnifiedClientConfigGenerator', () => { const configContributors = new ClientConfigContributorFactory( modelSchemaAdapter - ).getContributors('1.1'); + ).getContributors('1.2'); const clientConfigGenerator = new UnifiedClientConfigGenerator( outputRetrieval, diff --git a/packages/integration-tests/src/amplify_auth_credentials_factory.ts b/packages/integration-tests/src/amplify_auth_credentials_factory.ts index a08b1455bd..d5797586e3 100644 --- a/packages/integration-tests/src/amplify_auth_credentials_factory.ts +++ b/packages/integration-tests/src/amplify_auth_credentials_factory.ts @@ -32,7 +32,7 @@ export class AmplifyAuthCredentialsFactory { */ constructor( private readonly cognitoIdentityProviderClient: CognitoIdentityProviderClient, - authConfig: NonNullable['auth']> + authConfig: NonNullable['auth']> ) { if (!authConfig.identity_pool_id) { throw new Error('Client config must have identity pool id.'); diff --git a/packages/integration-tests/src/test-project-setup/access_testing_project.ts b/packages/integration-tests/src/test-project-setup/access_testing_project.ts index b41d4bd1b1..e44010f82d 100644 --- a/packages/integration-tests/src/test-project-setup/access_testing_project.ts +++ b/packages/integration-tests/src/test-project-setup/access_testing_project.ts @@ -147,7 +147,7 @@ class AccessTestingProjectTestProject extends TestProjectBase { backendId: BackendIdentifier ): Promise { await super.assertPostDeployment(backendId); - const clientConfig = await generateClientConfig(backendId, '1.1'); + const clientConfig = await generateClientConfig(backendId, '1.2'); await this.assertDifferentCognitoInstanceCannotAssumeAmplifyRoles( clientConfig ); @@ -160,7 +160,7 @@ class AccessTestingProjectTestProject extends TestProjectBase { * I.e. roles not created by auth construct. */ private assertGenericIamRolesAccessToData = async ( - clientConfig: ClientConfigVersionTemplateType<'1.1'> + clientConfig: ClientConfigVersionTemplateType<'1.2'> ) => { if (!clientConfig.custom) { throw new Error('Client config is missing custom section'); @@ -262,7 +262,7 @@ class AccessTestingProjectTestProject extends TestProjectBase { * This asserts that authenticated and unauthenticated roles have relevant access to data API. */ private assertAmplifyAuthAccessToData = async ( - clientConfig: ClientConfigVersionTemplateType<'1.1'> + clientConfig: ClientConfigVersionTemplateType<'1.2'> ): Promise => { if (!clientConfig.auth) { throw new Error('Client config is missing auth section'); @@ -367,7 +367,7 @@ class AccessTestingProjectTestProject extends TestProjectBase { * unauthorized roles. I.e. it tests trust policy. */ private assertDifferentCognitoInstanceCannotAssumeAmplifyRoles = async ( - clientConfig: ClientConfigVersionTemplateType<'1.1'> + clientConfig: ClientConfigVersionTemplateType<'1.2'> ): Promise => { const simpleAuthUser = await this.createAuthenticatedSimpleAuthCognitoUser( clientConfig @@ -416,7 +416,7 @@ class AccessTestingProjectTestProject extends TestProjectBase { }; private createAuthenticatedSimpleAuthCognitoUser = async ( - clientConfig: ClientConfigVersionTemplateType<'1.1'> + clientConfig: ClientConfigVersionTemplateType<'1.2'> ): Promise => { if (!clientConfig.custom) { throw new Error('Client config is missing custom section'); @@ -496,7 +496,7 @@ class AccessTestingProjectTestProject extends TestProjectBase { }; private createAppSyncClient = ( - clientConfig: ClientConfigVersionTemplateType<'1.1'>, + clientConfig: ClientConfigVersionTemplateType<'1.2'>, credentials: IamCredentials ): ApolloClient => { if (!clientConfig.data?.url) { diff --git a/packages/integration-tests/src/test-project-setup/cdk/auth_cdk_project.ts b/packages/integration-tests/src/test-project-setup/cdk/auth_cdk_project.ts index 2beb33dff0..7010f5d295 100644 --- a/packages/integration-tests/src/test-project-setup/cdk/auth_cdk_project.ts +++ b/packages/integration-tests/src/test-project-setup/cdk/auth_cdk_project.ts @@ -78,7 +78,7 @@ class AuthTestCdkProject extends TestCdkProjectBase { { stackName: this.stackName, }, - '1.1', //version of the config + '1.2', //version of the config awsClientProvider ); diff --git a/packages/integration-tests/src/test-projects/custom-outputs/amplify/backend.ts b/packages/integration-tests/src/test-projects/custom-outputs/amplify/backend.ts index 2ae7efb16f..82b87ac655 100644 --- a/packages/integration-tests/src/test-projects/custom-outputs/amplify/backend.ts +++ b/packages/integration-tests/src/test-projects/custom-outputs/amplify/backend.ts @@ -16,7 +16,7 @@ const sampleIdentityPoolId = 'test_identity_pool_id'; const sampleUserPoolClientId = 'test_user_pool_client_id'; backend.addOutput({ - version: '1.1', + version: '1.2', custom: { // test deploy time values restApiUrl: restApi.url, @@ -26,7 +26,7 @@ backend.addOutput({ }); backend.addOutput({ - version: '1.1', + version: '1.2', custom: { // test synth time values // and composition of config @@ -36,7 +36,7 @@ backend.addOutput({ const fakeCognitoUserPoolId = 'fakeCognitoUserPoolId'; backend.addOutput({ - version: '1.1', + version: '1.2', // test reserved key auth: { aws_region: sampleRegion,