diff --git a/codebuild_specs/e2e_workflow.yml b/codebuild_specs/e2e_workflow.yml index dd7accc50e..c3603af25a 100644 --- a/codebuild_specs/e2e_workflow.yml +++ b/codebuild_specs/e2e_workflow.yml @@ -755,61 +755,62 @@ batch: CLI_REGION: ca-central-1 depend-on: - publish_to_local_registry - - identifier: utils_ddb_iam_access_data_construct_custom_logic_amplify_table_5 + - identifier: >- + ddb_iam_access_data_construct_custom_type_iam_access_custom_logic_amplify_table_5 buildspec: codebuild_specs/run_cdk_tests.yml env: compute-type: BUILD_GENERAL1_LARGE variables: NODE_OPTIONS: '--max-old-space-size=14848' TEST_SUITE: >- - src/__tests__/utils.test.ts|src/__tests__/ddb-iam-access.test.ts|src/__tests__/data-construct.test.ts|src/__tests__/custom-logic.test.ts|src/__tests__/amplify-table-5.test.ts + src/__tests__/ddb-iam-access.test.ts|src/__tests__/data-construct.test.ts|src/__tests__/custom-type-iam-access.test.ts|src/__tests__/custom-logic.test.ts|src/__tests__/amplify-table-5.test.ts CLI_REGION: eu-west-3 depend-on: - publish_to_local_registry - identifier: >- - add_resources_single_gsi_single_record_single_gsi_empty_table_single_gsi_1k_records_single_gsi_10k_records + add_resources_utils_single_gsi_single_record_single_gsi_empty_table_single_gsi_1k_records buildspec: codebuild_specs/run_cdk_tests.yml env: compute-type: BUILD_GENERAL1_LARGE variables: NODE_OPTIONS: '--max-old-space-size=14848' TEST_SUITE: >- - src/__tests__/add-resources.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-single-record.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-empty-table.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-1k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-10k-records.test.ts + src/__tests__/add-resources.test.ts|src/__tests__/unit-tests/utils.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-single-record.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-empty-table.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-1k-records.test.ts CLI_REGION: ap-southeast-2 depend-on: - publish_to_local_registry - identifier: >- - replace_2_gsis_update_attr_single_record_replace_2_gsis_update_attr_empty_table_replace_2_gsis_update_attr_1k_records_replace_2 + single_gsi_10k_records_replace_2_gsis_update_attr_single_record_replace_2_gsis_update_attr_empty_table_replace_2_gsis_update_at buildspec: codebuild_specs/run_cdk_tests.yml env: compute-type: BUILD_GENERAL1_LARGE variables: NODE_OPTIONS: '--max-old-space-size=14848' TEST_SUITE: >- - src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-single-record.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-empty-table.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-1k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-10k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-single-record.test.ts + src/__tests__/deploy-velocity-temporarily-disabled/single-gsi-10k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-single-record.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-empty-table.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-1k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-update-attr-10k-records.test.ts CLI_REGION: ca-central-1 depend-on: - publish_to_local_registry - identifier: >- - replace_2_gsis_empty_table_replace_2_gsis_1k_records_replace_2_gsis_10k_records_3_gsis_single_record_3_gsis_empty_table + replace_2_gsis_single_record_replace_2_gsis_empty_table_replace_2_gsis_1k_records_replace_2_gsis_10k_records_3_gsis_single_reco buildspec: codebuild_specs/run_cdk_tests.yml env: compute-type: BUILD_GENERAL1_LARGE variables: NODE_OPTIONS: '--max-old-space-size=14848' TEST_SUITE: >- - src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-empty-table.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-1k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-10k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-single-record.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-empty-table.test.ts + src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-single-record.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-empty-table.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-1k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/replace-2-gsis-10k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-single-record.test.ts CLI_REGION: eu-central-1 depend-on: - publish_to_local_registry - - identifier: 3_gsis_1k_records_3_gsis_10k_records + - identifier: 3_gsis_empty_table_3_gsis_1k_records_3_gsis_10k_records buildspec: codebuild_specs/run_cdk_tests.yml env: compute-type: BUILD_GENERAL1_LARGE variables: NODE_OPTIONS: '--max-old-space-size=14848' TEST_SUITE: >- - src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-1k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-10k-records.test.ts + src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-empty-table.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-1k-records.test.ts|src/__tests__/deploy-velocity-temporarily-disabled/3-gsis-10k-records.test.ts CLI_REGION: eu-north-1 depend-on: - publish_to_local_registry diff --git a/dependency_licenses.txt b/dependency_licenses.txt index 42cc8a97e0..220cb56211 100644 --- a/dependency_licenses.txt +++ b/dependency_licenses.txt @@ -6081,7 +6081,7 @@ MIT License ----- -The following software may be included in this product: @types/aws-lambda, @types/babel__core, @types/babel__generator, @types/babel__template, @types/babel__traverse, @types/fs-extra, @types/glob, @types/graceful-fs, @types/hjson, @types/http-cache-semantics, @types/ini, @types/istanbul-lib-coverage, @types/istanbul-lib-report, @types/istanbul-reports, @types/jest, @types/js-yaml, @types/json-schema, @types/lodash, @types/md5, @types/minimatch, @types/minimist, @types/mock-fs, @types/node, @types/normalize-package-data, @types/object-hash, @types/parse-json, @types/semver, @types/semver-utils, @types/stack-utils, @types/triple-beam, @types/uuid, @types/ws, @types/yargs, @types/yargs-parser, @types/zen-observable. A copy of the source code may be downloaded from https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/aws-lambda), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__core), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__generator), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__template), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__traverse), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/fs-extra), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/glob), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/graceful-fs), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/hjson), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/http-cache-semantics), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/ini), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/istanbul-lib-coverage), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/istanbul-lib-report), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/istanbul-reports), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/jest), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/js-yaml), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/json-schema), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/lodash), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/md5), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/minimatch), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/minimist), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mock-fs), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/node), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/normalize-package-data), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/object-hash), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/parse-json), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/semver), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/semver-utils), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/stack-utils), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/triple-beam), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/uuid), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/ws), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/yargs), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/yargs-parser), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/zen-observable). This software contains the following license and notice below: +The following software may be included in this product: @types/aws-lambda, @types/babel__core, @types/babel__generator, @types/babel__template, @types/babel__traverse, @types/fs-extra, @types/glob, @types/graceful-fs, @types/hjson, @types/http-cache-semantics, @types/ini, @types/istanbul-lib-coverage, @types/istanbul-lib-report, @types/istanbul-reports, @types/jest, @types/js-yaml, @types/json-schema, @types/lodash, @types/md5, @types/minimatch, @types/minimist, @types/mock-fs, @types/node, @types/node-fetch, @types/normalize-package-data, @types/object-hash, @types/parse-json, @types/semver, @types/semver-utils, @types/stack-utils, @types/triple-beam, @types/uuid, @types/ws, @types/yargs, @types/yargs-parser, @types/zen-observable. A copy of the source code may be downloaded from https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/aws-lambda), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__core), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__generator), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__template), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__traverse), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/fs-extra), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/glob), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/graceful-fs), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/hjson), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/http-cache-semantics), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/ini), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/istanbul-lib-coverage), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/istanbul-lib-report), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/istanbul-reports), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/jest), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/js-yaml), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/json-schema), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/lodash), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/md5), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/minimatch), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/minimist), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mock-fs), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/node), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/node-fetch), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/normalize-package-data), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/object-hash), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/parse-json), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/semver), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/semver-utils), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/stack-utils), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/triple-beam), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/uuid), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/ws), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/yargs), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/yargs-parser), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/zen-observable). This software contains the following license and notice below: MIT License diff --git a/packages/amplify-graphql-api-construct-tests/package.json b/packages/amplify-graphql-api-construct-tests/package.json index 8f447e312e..cb3edbe0e4 100644 --- a/packages/amplify-graphql-api-construct-tests/package.json +++ b/packages/amplify-graphql-api-construct-tests/package.json @@ -17,20 +17,28 @@ ], "private": true, "scripts": { - "e2e": "jest --verbose --forceExit", - "build-tests": "tsc --build tsconfig.tests.json" + "e2e": "yarn test:e2e", + "build-tests": "tsc --build tsconfig.tests.json", + "test:e2e": "jest --verbose --forceExit", + "test": "jest --verbose --forceExit src/__tests__/unit-tests" }, "dependencies": { "@aws-amplify/auth-construct": "^1.0.0", "@aws-amplify/core": "^2.1.0", "@aws-amplify/graphql-api-construct": "1.15.1", "@aws-cdk/aws-cognito-identitypool-alpha": "2.152.0-alpha.0", + "@aws-crypto/sha256-js": "^5.2.0", "@aws-sdk/client-cognito-identity-provider": "3.624.0", "@aws-sdk/client-lambda": "3.624.0", "@aws-sdk/client-rds": "3.624.0", "@aws-sdk/client-ssm": "3.624.0", "@aws-sdk/client-sts": "3.624.0", + "@aws-sdk/credential-providers": "3.624.0", "@faker-js/faker": "^8.2.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/types": "^3.5.0", + "@types/node-fetch": "^2.6.11", "amplify-category-api-e2e-core": "5.0.4", "aws-amplify": "^4.2.8", "aws-appsync": "^4.1.1", diff --git a/packages/amplify-graphql-api-construct-tests/src/__tests__/backends/custom-type-iam-access-stack/app.ts b/packages/amplify-graphql-api-construct-tests/src/__tests__/backends/custom-type-iam-access-stack/app.ts new file mode 100644 index 0000000000..3184c1f132 --- /dev/null +++ b/packages/amplify-graphql-api-construct-tests/src/__tests__/backends/custom-type-iam-access-stack/app.ts @@ -0,0 +1,211 @@ +#!/usr/bin/env node +/* eslint-disable max-classes-per-file */ +import 'source-map-support/register'; +import * as fs from 'fs'; +import * as path from 'path'; +import { App, CfnOutput, Duration, Stack } from 'aws-cdk-lib'; +import { AccountPrincipal, Effect, PolicyDocument, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam'; +import { Construct } from 'constructs'; +import { AmplifyGraphqlApi, AmplifyGraphqlDefinition, AuthorizationModes } from '@aws-amplify/graphql-api-construct'; +import { BaseDataSource, Code, FunctionRuntime, NoneDataSource } from 'aws-cdk-lib/aws-appsync'; + +// #region Utilities + +/** + * Controls stack-level configurations. TODO: Move TestDefinitions into this structure so we can stop writing so many files. + */ +interface StackConfig { + /** + * Test-defined authorization mode settings. These will be applied to the final stack authorization configuration. If a field is present + * in this structure, it will override the default supplied by the configurable stack. + */ + partialAuthorizationModes?: Partial; + + /** + * The prefix to use when naming stack assets. Keep this short (<=15 characters) so you don't bump up against resource length limits + * (e.g., 140 characters for lambda layers). Prefixes longer than 15 characters will be truncated. + * arn:aws:lambda:ap-northeast-2:012345678901:layer:${PREFIX}ApiAmplifyCodegenAssetsAmplifyCodegenAssetsDeploymentAwsCliLayerABCDEF12:1 + */ + prefix: string; + + /** + * If provided, use the provided Lambda Layer ARN instead of the default retrieved from the Lambda Layer version resolver. Suitable for + * overriding the default layers during tests. + */ + sqlLambdaLayerArn?: string; + + testRoleProps: TestRoleProps; + + /** + * If true, disable Cognito User Pool creation and only use API Key auth in sandbox mode. + */ + useSandbox?: boolean; +} + +/** + * Specifies values for creating an IAM role to be used in tests. + */ +interface TestRoleProps { + /** + * The AWS account that will be allowed to assume the role + */ + assumedByAccount: string; +} + +const readStackConfig = (projRoot: string): StackConfig => { + const configPath = path.join(projRoot, 'stack-config.json'); + const configString = fs.readFileSync(configPath).toString(); + const config = JSON.parse(configString); + config.prefix = config.prefix.substring(0, 15); + return config; +}; + +// #endregion Utilities + +const projRoot = path.normalize(path.join(__dirname, '..')); + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const packageJson = require('../package.json'); + +const stackConfig = readStackConfig(projRoot); + +const app = new App(); +const stackName = packageJson.name.replace(/_/g, '-'); +const stack = new Stack(app, stackName, { + env: { region: process.env.CLI_REGION || 'us-west-2' }, +}); + +const authorizationModes: AuthorizationModes = { + defaultAuthorizationMode: 'API_KEY', + apiKeyConfig: { expires: Duration.days(2) }, + ...stackConfig.partialAuthorizationModes, +}; + +interface AddTestResolverInput { + api: AmplifyGraphqlApi; + fieldName: string; + noneDataSource: NoneDataSource; + prefix: string; + typeName: string; +} + +const addTestResolver = (input: AddTestResolverInput): void => { + const { api, noneDataSource, prefix, typeName, fieldName } = input; + + const returnValue = `"test-value-${typeName}-${fieldName}"`; + const scalarReturnClause = `return ${returnValue}`; + const customTypeReturnClause = `return { value: ${returnValue} }`; + const returnClause = fieldName.match('Scalar') ? scalarReturnClause : customTypeReturnClause; + + api.addResolver(`${prefix}${typeName === 'Query' ? 'Get' : 'Update'}${typeName}${fieldName}Resolver`, { + typeName, + fieldName, + runtime: FunctionRuntime.JS_1_0_0, + dataSource: noneDataSource, + code: Code.fromInline(` + export function request(ctx) { + return {}; + } + + export function response(ctx) { + ${returnClause}; + } + `), + }); +}; + +const api = new AmplifyGraphqlApi(stack, `${stackConfig.prefix}Api`, { + definition: AmplifyGraphqlDefinition.fromString(/* GraphQL */ ` + # Query is required to pass AppSync's validation. Without it, we get an error: "There is no top level schema object defined". + # We could also include a @model, which would cause the model transformer to create model-related Query fields, but it's + # important that we test the behavior in a schema with no @models. For this test, we'll include custom query fields, but it's + # important to note the behavior since it's possible for customers to create schemas with no models and no Query fields. + type Query { + getScalar: String + getCustomType: CustomType + } + + type Mutation { + updateScalar: String + updateCustomType: CustomType + } + + type CustomType { + value: String + } + `), + authorizationModes, + translationBehavior: { + sandboxModeEnabled: stackConfig.useSandbox, + }, +}); + +const dataSourceName = `${stackConfig.prefix}NoneDS`; +const noneDataSource = api.addNoneDataSource(dataSourceName, { name: dataSourceName }); + +addTestResolver({ + api, + noneDataSource, + prefix: stackConfig.prefix, + typeName: 'Query', + fieldName: 'getScalar', +}); + +addTestResolver({ + api, + noneDataSource, + prefix: stackConfig.prefix, + typeName: 'Query', + fieldName: 'getCustomType', +}); + +addTestResolver({ + api, + noneDataSource, + prefix: stackConfig.prefix, + typeName: 'Mutation', + fieldName: 'updateScalar', +}); + +addTestResolver({ + api, + noneDataSource, + prefix: stackConfig.prefix, + typeName: 'Mutation', + fieldName: 'updateCustomType', +}); + +class TestRole extends Role { + constructor(scope: Construct, id: string, props: TestRoleProps & { api: AmplifyGraphqlApi }) { + const { assumedByAccount, api: graphqlApi } = props; + const apiArn = graphqlApi.resources.graphqlApi.arn; + + const policyStatement = new PolicyStatement({ + sid: 'EnableGraphqlForRole', + actions: ['appsync:GraphQL'], + effect: Effect.ALLOW, + resources: [ + `${apiArn}/types/CustomType`, + `${apiArn}/types/CustomType/fields/value`, + `${apiArn}/types/Mutation/fields/updateScalar`, + `${apiArn}/types/Mutation/fields/updateCustomType`, + `${apiArn}/types/Query/fields/getScalar`, + `${apiArn}/types/Query/fields/getCustomType`, + ], + }); + super(scope, id, { + assumedBy: new AccountPrincipal(assumedByAccount), + inlinePolicies: { + allowGraphqlQuery: new PolicyDocument({ + statements: [policyStatement], + }), + }, + }); + } +} + +const testRole = new TestRole(stack, `${stackConfig.prefix}TestRole`, { + api, + assumedByAccount: stackConfig.testRoleProps.assumedByAccount, +}); +new CfnOutput(stack, 'awsIamTestRoleArn', { value: testRole.roleArn }); diff --git a/packages/amplify-graphql-api-construct-tests/src/__tests__/custom-type-iam-access.test.ts b/packages/amplify-graphql-api-construct-tests/src/__tests__/custom-type-iam-access.test.ts new file mode 100644 index 0000000000..7bbf29be4b --- /dev/null +++ b/packages/amplify-graphql-api-construct-tests/src/__tests__/custom-type-iam-access.test.ts @@ -0,0 +1,243 @@ +import * as path from 'path'; +import { createNewProjectDir, deleteProjectDir } from 'amplify-category-api-e2e-core'; +import { initCDKProject, cdkDeploy, cdkDestroy } from '../commands'; +import { + getPayloadStringForGraphqlRequest, + getSigV4SignedAppSyncRequest, + graphqlRequest, + sigV4SignedRequestToNodeFetchRequest, +} from '../graphql-request'; +import { DURATION_1_HOUR } from '../utils/duration-constants'; +import { getAccountFromArn, writeStackConfig } from '../utils'; + +jest.setTimeout(DURATION_1_HOUR); + +const region = process.env.CLI_REGION ?? 'us-west-2'; +const account = process.env.AWS_ACCOUNT ?? getAccountFromArn(process.env.TEST_ACCOUNT_ROLE); +if (!account) { + throw new Error('Must specify either AWS_ACCOUNT or TEST_ACCOUNT_ROLE environment variables'); +} + +// This test suite creates a stack with only custom operations, and a test role. It variously tests creating the stack with and without +// `enableIamAuthorizationMode` enabled. We expect that requests authorized by assuming the test role will succeed if +// enableIamAuthorizationMode is true, and fail otherwise. +describe('Implicit IAM support on custom operations', () => { + describe.each([{ enableIamAuthorizationMode: true }, { enableIamAuthorizationMode: false }])( + 'Supports implicit IAM authorization on custom operations with scalar values when enableIamAuthorizationMode is %b', + ({ enableIamAuthorizationMode }) => { + /** Directory in which the project files are stored */ + let projRoot: string; + + /** Endpoint of the AppSync GraphQL API */ + let apiEndpoint: string; + + /** Prefix for stack resources and sts:AssumeRole sessions */ + let prefix: string; + + /** The Test Role that will be assumed by graphQL calls to test IAM access */ + let testRoleArn: string; + + beforeAll(async () => { + prefix = `Iam${enableIamAuthorizationMode ? 'Enabled' : 'Disabled'}Test`; + const projFolderName = `iam-auth-${enableIamAuthorizationMode}`; + projRoot = await createNewProjectDir(projFolderName); + const templatePath = path.resolve(path.join(__dirname, 'backends', 'custom-type-iam-access-stack')); + const name = await initCDKProject(projRoot, templatePath); + + // Note that we don't need to write a test definition -- we'll reuse a hardcoded definition in the stack itself + writeStackConfig(projRoot, { + prefix, + partialAuthorizationModes: { + iamConfig: { + enableIamAuthorizationMode: enableIamAuthorizationMode, + }, + }, + testRoleProps: { + assumedByAccount: account, + }, + }); + + const testConfig = await deployStack({ + projRoot, + name, + }); + + apiEndpoint = testConfig.apiEndpoint; + testRoleArn = testConfig.testRoleArn; + }); + + afterAll(async () => { + try { + await cdkDestroy(projRoot, '--all'); + } catch (err) { + console.log(`Error invoking 'cdk destroy': ${err}`); + } + + deleteProjectDir(projRoot); + }); + + test(`${ + enableIamAuthorizationMode ? 'Allows' : 'Denies' + } access to a mutation returning a scalar value when enableIamAuthorizationMode is ${enableIamAuthorizationMode}`, async () => { + const query = /* GraphQL */ ` + mutation TestMutation { + updateScalar + } + `; + const signedRequest = await getSigV4SignedAppSyncRequest({ + body: getPayloadStringForGraphqlRequest(query), + endpoint: apiEndpoint, + region, + roleArn: testRoleArn, + sessionNamePrefix: `${prefix}-${Date.now()}`, + }); + + const request = sigV4SignedRequestToNodeFetchRequest(signedRequest); + + const result = await graphqlRequest(request); + + const expectedStatusCode = enableIamAuthorizationMode ? 200 : 400; + expect(result.statusCode).toEqual(expectedStatusCode); + + /* eslint-disable jest/no-conditional-expect */ + if (enableIamAuthorizationMode) { + expect(result.body.data.updateScalar).toEqual('test-value-Mutation-updateScalar'); + expect(result.body.errors).not.toBeDefined(); + } else { + expect(result.body.errors[0].errorType).toEqual('Unauthorized'); + expect(result.body.errors[0].message).toEqual('Not Authorized to access updateScalar on type Mutation'); + } + /* eslint-enable jest/no-conditional-expect */ + }); + + test(`${ + enableIamAuthorizationMode ? 'Allows' : 'Denies' + } access to a query returning a scalar value when enableIamAuthorizationMode is ${enableIamAuthorizationMode}`, async () => { + const query = /* GraphQL */ ` + query TestQuery { + getScalar + } + `; + const signedRequest = await getSigV4SignedAppSyncRequest({ + body: getPayloadStringForGraphqlRequest(query), + endpoint: apiEndpoint, + region, + roleArn: testRoleArn, + sessionNamePrefix: `${prefix}-${Date.now()}`, + }); + + const request = sigV4SignedRequestToNodeFetchRequest(signedRequest); + + const result = await graphqlRequest(request); + + const expectedStatusCode = enableIamAuthorizationMode ? 200 : 400; + expect(result.statusCode).toEqual(expectedStatusCode); + + /* eslint-disable jest/no-conditional-expect */ + if (enableIamAuthorizationMode) { + expect(result.body.data.getScalar).toEqual('test-value-Query-getScalar'); + expect(result.body.errors).not.toBeDefined(); + } else { + expect(result.body.errors[0].errorType).toEqual('Unauthorized'); + expect(result.body.errors[0].message).toEqual('Not Authorized to access getScalar on type Query'); + } + /* eslint-enable jest/no-conditional-expect */ + }); + + test(`${ + enableIamAuthorizationMode ? 'Allows' : 'Denies' + } access to a mutation returning a custom type when enableIamAuthorizationMode is ${enableIamAuthorizationMode}`, async () => { + const query = /* GraphQL */ ` + mutation TestMutation { + updateCustomType { + value + } + } + `; + const signedRequest = await getSigV4SignedAppSyncRequest({ + body: getPayloadStringForGraphqlRequest(query), + endpoint: apiEndpoint, + region, + roleArn: testRoleArn, + sessionNamePrefix: `${prefix}-${Date.now()}`, + }); + + const request = sigV4SignedRequestToNodeFetchRequest(signedRequest); + + const result = await graphqlRequest(request); + + const expectedStatusCode = enableIamAuthorizationMode ? 200 : 400; + expect(result.statusCode).toEqual(expectedStatusCode); + + /* eslint-disable jest/no-conditional-expect */ + if (enableIamAuthorizationMode) { + expect(result.body.data.updateCustomType.value).toEqual('test-value-Mutation-updateCustomType'); + expect(result.body.errors).not.toBeDefined(); + } else { + expect(result.body.errors[0].errorType).toEqual('Unauthorized'); + expect(result.body.errors[0].message).toEqual('Not Authorized to access updateCustomType on type Mutation'); + } + /* eslint-enable jest/no-conditional-expect */ + }); + + test(`${ + enableIamAuthorizationMode ? 'Allows' : 'Denies' + } access to a query returning a custom type when enableIamAuthorizationMode is ${enableIamAuthorizationMode}`, async () => { + const query = /* GraphQL */ ` + query TestQuery { + getCustomType { + value + } + } + `; + const signedRequest = await getSigV4SignedAppSyncRequest({ + body: getPayloadStringForGraphqlRequest(query), + endpoint: apiEndpoint, + region, + roleArn: testRoleArn, + sessionNamePrefix: `${prefix}-${Date.now()}`, + }); + + const request = sigV4SignedRequestToNodeFetchRequest(signedRequest); + + const result = await graphqlRequest(request); + + const expectedStatusCode = enableIamAuthorizationMode ? 200 : 400; + expect(result.statusCode).toEqual(expectedStatusCode); + + /* eslint-disable jest/no-conditional-expect */ + if (enableIamAuthorizationMode) { + expect(result.body.data.getCustomType.value).toEqual('test-value-Query-getCustomType'); + expect(result.body.errors).not.toBeDefined(); + } else { + expect(result.body.errors[0].errorType).toEqual('Unauthorized'); + expect(result.body.errors[0].message).toEqual('Not Authorized to access getCustomType on type Query'); + } + /* eslint-enable jest/no-conditional-expect */ + }); + }, + ); +}); + +interface CommonSetupInput { + projRoot: string; + name: string; +} + +interface CommonSetupOutput { + apiEndpoint: string; + testRoleArn: string; +} + +const deployStack = async (input: CommonSetupInput): Promise => { + const { projRoot, name } = input; + const outputs = await cdkDeploy(projRoot, '--all'); + const { awsAppsyncApiEndpoint: apiEndpoint, awsIamTestRoleArn: testRoleArn } = outputs[name]; + + const output: CommonSetupOutput = { + apiEndpoint, + testRoleArn, + }; + + return output; +}; diff --git a/packages/amplify-graphql-api-construct-tests/src/__tests__/unit-tests/utils.test.ts b/packages/amplify-graphql-api-construct-tests/src/__tests__/unit-tests/utils.test.ts new file mode 100644 index 0000000000..a1c13cdf46 --- /dev/null +++ b/packages/amplify-graphql-api-construct-tests/src/__tests__/unit-tests/utils.test.ts @@ -0,0 +1,80 @@ +import { ArnFormat } from 'aws-cdk-lib/core'; +import { isOperationAuthInputApiKey } from '../../utils'; +import { getAccountFromArn } from '../../utils/account-utils'; + +describe('test utilities', () => { + describe('appsync-graphql', () => { + describe('type predicates', () => { + describe('isOperationAuthInputApiKey', () => { + it('returns true for a stringy api key', () => { + const apiKey = 'testApiKey'; + const apiEndpoint = 'testEndpoint'; + + const args = { + apiEndpoint, + auth: { apiKey }, + }; + + const input = { + ...args, + query: 'mock query', + variables: { + id: 'id123', + owner: 'owner123', + }, + }; + + const { auth } = input; + + expect(isOperationAuthInputApiKey(auth)).toBeTruthy(); + }); + + it('returns false for an undefined api key', () => { + const apiEndpoint = 'testEndpoint'; + + const args = { + apiEndpoint, + auth: { apiKey: undefined }, + }; + + const input = { + ...args, + query: 'mock query', + variables: { + id: 'id123', + owner: 'owner123', + }, + }; + + const { auth } = input; + + expect(isOperationAuthInputApiKey(auth)).toBeFalsy(); + }); + }); + }); + }); + + describe('arn-utils', () => { + describe('getAccountFromArn', () => { + it('returns undefined for an undefined arn', () => { + expect(getAccountFromArn()).not.toBeDefined(); + }); + + it('process a CodeBuild ARN with default format', () => { + const arn = 'arn:aws:codebuild:us-west-2:0123456789:build/codebuild-demo-project:b1e6661e-e4f2-4156-9ab9-82a19EXAMPLE'; + expect(getAccountFromArn(arn)).toEqual('0123456789'); + }); + + it('process a CodeBuild ARN with the correct explicit format', () => { + const arn = 'arn:aws:codebuild:us-west-2:0123456789:build/codebuild-demo-project:b1e6661e-e4f2-4156-9ab9-82a19EXAMPLE'; + expect(getAccountFromArn(arn, ArnFormat.SLASH_RESOURCE_NAME)).toEqual('0123456789'); + }); + + // We expect this to work because we're not interested in the resource, only in the account, which is always separated by colons + it('process a CodeBuild ARN with an incorrect explicit format', () => { + const arn = 'arn:aws:codebuild:us-west-2:0123456789:build/codebuild-demo-project:b1e6661e-e4f2-4156-9ab9-82a19EXAMPLE'; + expect(getAccountFromArn(arn, ArnFormat.COLON_RESOURCE_NAME)).toEqual('0123456789'); + }); + }); + }); +}); diff --git a/packages/amplify-graphql-api-construct-tests/src/__tests__/utils.test.ts b/packages/amplify-graphql-api-construct-tests/src/__tests__/utils.test.ts deleted file mode 100644 index f1b65ec678..0000000000 --- a/packages/amplify-graphql-api-construct-tests/src/__tests__/utils.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { isOperationAuthInputApiKey } from '../utils'; - -describe('test utilities', () => { - describe('appsync-graphql', () => { - describe('type predicates', () => { - describe('isOperationAuthInputApiKey', () => { - it('returns true for a stringy api key', () => { - const apiKey = 'testApiKey'; - const apiEndpoint = 'testEndpoint'; - - const args = { - apiEndpoint, - auth: { apiKey }, - }; - - const input = { - ...args, - query: 'mock query', - variables: { - id: 'id123', - owner: 'owner123', - }, - }; - - const { auth } = input; - - expect(isOperationAuthInputApiKey(auth)).toBeTruthy(); - }); - - it('returns false for an undefined api key', () => { - const apiEndpoint = 'testEndpoint'; - - const args = { - apiEndpoint, - auth: { apiKey: undefined }, - }; - - const input = { - ...args, - query: 'mock query', - variables: { - id: 'id123', - owner: 'owner123', - }, - }; - - const { auth } = input; - - expect(isOperationAuthInputApiKey(auth)).toBeFalsy(); - }); - }); - }); - }); -}); diff --git a/packages/amplify-graphql-api-construct-tests/src/graphql-request.ts b/packages/amplify-graphql-api-construct-tests/src/graphql-request.ts index 5d76b73f1b..0ac9966463 100644 --- a/packages/amplify-graphql-api-construct-tests/src/graphql-request.ts +++ b/packages/amplify-graphql-api-construct-tests/src/graphql-request.ts @@ -1,19 +1,23 @@ -import { default as fetch, Request } from 'node-fetch'; +import { IHttpRequest } from '@smithy/protocol-http'; +import { QueryParameterBag } from '@smithy/types'; +import { makeSignedRequest, makeTemporaryCredentialsProvider, urlToHttpRequestOptions } from './utils'; export type GraphqlResponse = { statusCode: number; body: any; }; -export const graphqlRequest = async (apiEndpoint: string, payload: any): Promise => { +export const graphqlRequest = async (request: Request): Promise => { let statusCode = 200; let body; let response; try { - response = await fetch(new Request(apiEndpoint, payload)); + response = await fetch(request); body = await response.json(); - if (body.errors) statusCode = 400; + if (body.errors) { + statusCode = 400; + } } catch (error) { statusCode = 400; body = { @@ -59,8 +63,8 @@ export const validateGraphql = async ({ return response; }; -export const graphql = async (apiEndpoint: string, apiKey: string, query: string): Promise => - graphqlRequest(apiEndpoint, { +export const graphql = async (apiEndpoint: string, apiKey: string, query: string): Promise => { + const request = new Request(apiEndpoint, { method: 'POST', headers: { 'x-api-key': apiKey, @@ -68,9 +72,11 @@ export const graphql = async (apiEndpoint: string, apiKey: string, query: string }, body: JSON.stringify({ query }), }); + return graphqlRequest(request); +}; -export const graphqlRequestWithLambda = async (apiEndpoint: string, authToken: string, query: string): Promise => - graphqlRequest(apiEndpoint, { +export const graphqlRequestWithLambda = async (apiEndpoint: string, authToken: string, query: string): Promise => { + const request = new Request(apiEndpoint, { method: 'POST', headers: { Authorization: authToken, @@ -78,3 +84,94 @@ export const graphqlRequestWithLambda = async (apiEndpoint: string, authToken: s }, body: JSON.stringify({ query }), }); + return graphqlRequest(request); +}; + +export const graphqlWithSigV4 = async (signedRequest: IHttpRequest): Promise => { + const request = sigV4SignedRequestToNodeFetchRequest(signedRequest); + return graphqlRequest(request); +}; + +const queryBagToQueryString = (bag?: QueryParameterBag): string => { + if (!bag) { + return ''; + } + + const components: string[] = []; + Object.entries(bag).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((val) => components.push(`${key}=${encodeURIComponent(val)}`)); + } else { + components.push(`${key}=${encodeURIComponent(value)}`); + } + }); + + if (components.length > 0) { + return '?' + components.join('&'); + } else { + return ''; + } +}; + +/** + * Returns a query and optional variables in the correct structure for processing a GraphQL request + */ +export const getPayloadStringForGraphqlRequest = (query: string, variables?: any): string => { + const payload: any = { + query: query, + }; + + if (variables) { + payload.variables = variables; + } + + return JSON.stringify(payload); +}; + +export interface GetSignedAppSyncRequestOptions { + body?: string; + endpoint: string; + region: string; + roleArn: string; + sessionNamePrefix: string; +} + +export const getSigV4SignedAppSyncRequest = async (options: GetSignedAppSyncRequestOptions): Promise => { + const { body, endpoint, region, roleArn, sessionNamePrefix } = options; + const credentials = makeTemporaryCredentialsProvider({ roleArn, region, sessionNamePrefix }); + const url = new URL(endpoint); + const requestOptions = urlToHttpRequestOptions(url, 'POST'); + + requestOptions.headers = requestOptions.headers ?? {}; + requestOptions.headers['Content-Type'] = 'application/json'; + requestOptions.headers['Host'] = url.host; + + requestOptions.body = body; + + const signedRequest = await makeSignedRequest({ + credentials, + region, + requestOptions, + service: 'appsync', + }); + + return signedRequest; +}; + +export const sigV4SignedRequestToNodeFetchRequest = (signedRequest: IHttpRequest): Request => { + // Note that IHttpRequest.protocol includes a terminating `:`, not just the alphanumeric scheme + const protocol = `${signedRequest.protocol}//`; + const userPass = signedRequest.username || signedRequest.password ? `${signedRequest.username}:${signedRequest.password}@` : ''; + const hostname = signedRequest.hostname; + const port = signedRequest.port ? `:${signedRequest.port}` : ''; + const path = signedRequest.path; + const query = queryBagToQueryString(signedRequest.query); + const fragment = signedRequest.fragment ? `#${signedRequest.fragment}` : ''; + + const urlComponents = [protocol, userPass, hostname, port, path, query, fragment]; + + const url = urlComponents.join(''); + + const request = new Request(url, signedRequest); + return request; +}; diff --git a/packages/amplify-graphql-api-construct-tests/src/utils/account-utils.ts b/packages/amplify-graphql-api-construct-tests/src/utils/account-utils.ts new file mode 100644 index 0000000000..6f767750a5 --- /dev/null +++ b/packages/amplify-graphql-api-construct-tests/src/utils/account-utils.ts @@ -0,0 +1,11 @@ +import { Arn, ArnFormat } from 'aws-cdk-lib/core'; + +export const getAccountFromArn = (arn?: string, arnFormat: ArnFormat = ArnFormat.SLASH_RESOURCE_NAME): string | undefined => { + if (!arn) { + return undefined; + } + + const components = Arn.split(arn, arnFormat); + + return components.account; +}; diff --git a/packages/amplify-graphql-api-construct-tests/src/utils/appsync-graphql/common.ts b/packages/amplify-graphql-api-construct-tests/src/utils/appsync-graphql/common.ts index ee4e5a5f64..034f12f3da 100644 --- a/packages/amplify-graphql-api-construct-tests/src/utils/appsync-graphql/common.ts +++ b/packages/amplify-graphql-api-construct-tests/src/utils/appsync-graphql/common.ts @@ -45,11 +45,13 @@ export const doAppSyncGraphqlOperation = async (input: any): Promise => { payload.variables = variables; } - const result = await graphqlRequest(apiEndpoint, { + const request = new Request(apiEndpoint, { method: 'POST', headers, body: JSON.stringify(payload), }); + const result = await graphqlRequest(request); + return result; }; diff --git a/packages/amplify-graphql-api-construct-tests/src/utils/auth/aws-sdk-http-request.ts b/packages/amplify-graphql-api-construct-tests/src/utils/auth/aws-sdk-http-request.ts new file mode 100644 index 0000000000..507480a4ba --- /dev/null +++ b/packages/amplify-graphql-api-construct-tests/src/utils/auth/aws-sdk-http-request.ts @@ -0,0 +1,29 @@ +import { HttpMessage, URI } from '@smithy/types'; + +export type HttpRequestOptions = Partial & + Partial & { + method?: string; + }; + +export const urlToHttpRequestOptions = (url: URL, method: string): HttpRequestOptions => { + const query: Record = {}; + const searchParams = Array.from(url.searchParams); + + searchParams.forEach(([key, value]) => { + query[key] = value; + }); + + const requestOptions: HttpRequestOptions = { + method, + protocol: url.protocol, + hostname: url.hostname, + port: parseInt(url.port, 10), + path: url.pathname, + query, + username: url.username, + password: url.password, + fragment: url.hash, + }; + + return requestOptions; +}; diff --git a/packages/amplify-graphql-api-construct-tests/src/utils/auth.ts b/packages/amplify-graphql-api-construct-tests/src/utils/auth/cognito-auth.ts similarity index 100% rename from packages/amplify-graphql-api-construct-tests/src/utils/auth.ts rename to packages/amplify-graphql-api-construct-tests/src/utils/auth/cognito-auth.ts diff --git a/packages/amplify-graphql-api-construct-tests/src/utils/auth/index.ts b/packages/amplify-graphql-api-construct-tests/src/utils/auth/index.ts new file mode 100644 index 0000000000..5887c0a6e8 --- /dev/null +++ b/packages/amplify-graphql-api-construct-tests/src/utils/auth/index.ts @@ -0,0 +1,2 @@ +export * from './cognito-auth'; +export * from './sigv4-signer'; diff --git a/packages/amplify-graphql-api-construct-tests/src/utils/auth/sigv4-signer.ts b/packages/amplify-graphql-api-construct-tests/src/utils/auth/sigv4-signer.ts new file mode 100644 index 0000000000..40562c96c6 --- /dev/null +++ b/packages/amplify-graphql-api-construct-tests/src/utils/auth/sigv4-signer.ts @@ -0,0 +1,50 @@ +import { Sha256 } from '@aws-crypto/sha256-js'; +import { fromTemporaryCredentials } from '@aws-sdk/credential-providers'; +import { SignatureV4 } from '@smithy/signature-v4'; +import { HttpRequest, IHttpRequest } from '@smithy/protocol-http'; +import { AwsCredentialIdentity, Provider } from '@smithy/types'; +import { HttpRequestOptions } from './aws-sdk-http-request'; + +export interface MakeSignedRequestOptions { + requestOptions: HttpRequestOptions; + credentials: AwsCredentialIdentity | Provider; + region: string | Provider; + service: string; +} + +export const makeSignedRequest = async (options: MakeSignedRequestOptions): Promise => { + const { requestOptions, credentials, region, service } = options; + const signer = new SignatureV4({ + credentials, + region, + service, + sha256: Sha256, + uriEscapePath: true, + }); + + const signedRequest = await signer.sign(new HttpRequest(requestOptions)); + + return signedRequest; +}; + +export interface MakeTemporaryCredentialsProviderOptions { + roleArn: string; + region: string | Provider; + sessionNamePrefix: string | Provider; +} + +/** + * Returns a credential provider that will invoke `sts:AssumeRole` to the specified `roleArn` and return the session credentials. Uses + * default resolution for master credentials. See + * https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-credential-providers/ + */ +export const makeTemporaryCredentialsProvider = (options: MakeTemporaryCredentialsProviderOptions): Provider => { + const { roleArn, region, sessionNamePrefix } = options; + return fromTemporaryCredentials({ + params: { + RoleArn: roleArn, + RoleSessionName: `${sessionNamePrefix}-${Date.now()}`, + }, + clientConfig: { region }, + }); +}; diff --git a/packages/amplify-graphql-api-construct-tests/src/utils/index.ts b/packages/amplify-graphql-api-construct-tests/src/utils/index.ts index 4fcff9ddf5..cd18d1f8dd 100644 --- a/packages/amplify-graphql-api-construct-tests/src/utils/index.ts +++ b/packages/amplify-graphql-api-construct-tests/src/utils/index.ts @@ -1,6 +1,8 @@ +export * from './account-utils'; export * from './appsync-graphql'; export * from './auth'; export * from './db'; export * from './sql-lambda-layer'; export * from './stack-config'; export * from './test-definition'; +export * from './test-role-props'; diff --git a/packages/amplify-graphql-api-construct-tests/src/utils/stack-config.ts b/packages/amplify-graphql-api-construct-tests/src/utils/stack-config.ts index eccccd08c1..0303c9fda6 100644 --- a/packages/amplify-graphql-api-construct-tests/src/utils/stack-config.ts +++ b/packages/amplify-graphql-api-construct-tests/src/utils/stack-config.ts @@ -1,7 +1,20 @@ import * as path from 'path'; -import * as fs from 'fs-extra'; +import * as fs from 'fs'; +import { AuthorizationModes } from '@aws-amplify/graphql-api-construct'; +import { TestRoleProps } from './test-role-props'; + +/** + * Controls stack-level configurations. TODO: Move TestDefinitions into this structure so we can stop writing so many files. + */ +interface StackConfig { + /** + * Test-defined authorization mode settings. These will be applied to the final stack authorization configuration. If a field is present + * in this structure, it will override the default supplied by the configurable stack. + * + * **NOTE:** Not consumed by every test stack. Check the source to ensure compatibility + */ + partialAuthorizationModes?: Partial; -export interface StackConfig { /** * The prefix to use when naming stack assets. Keep this short (<=15 characters) so you don't bump up against resource length limits * (e.g., 140 characters for lambda layers). Prefixes longer than 15 characters will be truncated. @@ -9,16 +22,23 @@ export interface StackConfig { */ prefix: string; - /** - * If true, disable Cognito User Pool creation and only use API Key auth in sandbox mode. - */ - useSandbox?: boolean; - /** * If provided, use the provided Lambda Layer ARN instead of the default retrieved from the Lambda Layer version resolver. Suitable for * overriding the default layers during tests. */ sqlLambdaLayerArn?: string; + + /** + * If present, these props will be used to create an IAM role. Be default, the role will be assumable by the current test account. + * + * **NOTE:** Not consumed by every test stack. Check the source to ensure compatibility + */ + testRoleProps?: TestRoleProps; + + /** + * If true, disable Cognito User Pool creation and only use API Key auth in sandbox mode. + */ + useSandbox?: boolean; } export const writeStackConfig = (projRoot: string, stackConfig: StackConfig): void => { diff --git a/packages/amplify-graphql-api-construct-tests/src/utils/test-role-props.ts b/packages/amplify-graphql-api-construct-tests/src/utils/test-role-props.ts new file mode 100644 index 0000000000..f81037cacd --- /dev/null +++ b/packages/amplify-graphql-api-construct-tests/src/utils/test-role-props.ts @@ -0,0 +1,9 @@ +/** + * Specifies values for creating an IAM role to be used in tests. + */ +export interface TestRoleProps { + /** + * The AWS account that will be allowed to assume the role + */ + assumedByAccount: string; +} diff --git a/packages/amplify-graphql-transformer-core/API.md b/packages/amplify-graphql-transformer-core/API.md index 7fce5c4dd5..7a0dcb8d65 100644 --- a/packages/amplify-graphql-transformer-core/API.md +++ b/packages/amplify-graphql-transformer-core/API.md @@ -362,9 +362,6 @@ export interface GraphQLTransformOptions { readonly userDefinedSlots?: Record; } -// @public (undocumented) -export const hasDirectiveWithName: (node: FieldDefinitionNode | InterfaceTypeDefinitionNode | ObjectTypeDefinitionNode, name: string) => boolean; - // @public (undocumented) export type ImportAppSyncAPIInputs = { apiName: string; diff --git a/packages/graphql-transformer-common/API.md b/packages/graphql-transformer-common/API.md index f2b51c5989..ec347a9c8a 100644 --- a/packages/graphql-transformer-common/API.md +++ b/packages/graphql-transformer-common/API.md @@ -58,13 +58,17 @@ export const DEFAULT_SCALARS: ScalarMap; export function defineUnionType(name: string, types?: NamedTypeNode[]): UnionTypeDefinitionNode; // @public (undocumented) -export const directiveExists: (definition: ObjectTypeDefinitionNode, name: string) => DirectiveNode; +export const directiveExists: (node: { + directives?: ReadonlyArray; +}, name: string) => boolean; // @public (undocumented) -export function extendFieldWithDirectives(field: FieldDefinitionNode, directives: DirectiveNode[]): FieldDefinitionNode; +export const extendFieldWithDirectives: (field: FieldDefinitionNode, directives: DirectiveNode[]) => FieldDefinitionNode; // @public (undocumented) -export function extendObjectWithDirectives(object: ObjectTypeDefinitionNode, directives: DirectiveNode[]): ObjectTypeDefinitionNode; +export function extendNodeWithDirectives; +}>(node: T, directives: DirectiveNode[]): T; // @public (undocumented) export function extensionWithDirectives(object: ObjectTypeExtensionNode, directives: DirectiveNode[]): ObjectTypeExtensionNode; diff --git a/yarn.lock b/yarn.lock index 6c626ca166..1139d44a7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7080,6 +7080,13 @@ dependencies: tslib "^2.6.2" +"@smithy/types@^3.5.0": + version "3.5.0" + resolved "https://registry.npmjs.org/@smithy%2ftypes/-/types-3.5.0.tgz#9589e154c50d9c5d00feb7d818112ef8fc285d6e" + integrity sha512-QN0twHNfe8mNJdH9unwsCK13GURU7oEAZqkBI+rsvpv1jrmserO+WnLE7jidR9W/1dxwZ0u/CB01mV2Gms/K2Q== + dependencies: + tslib "^2.6.2" + "@smithy/url-parser@^3.0.3", "@smithy/url-parser@^3.0.4", "@smithy/url-parser@^3.0.6": version "3.0.6" resolved "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.6.tgz#98b426f9a492e0c992fcd5dceac35444c2632837" @@ -7500,6 +7507,14 @@ dependencies: "@types/node" "*" +"@types/node-fetch@^2.6.11": + version "2.6.11" + resolved "https://registry.npmjs.org/@types%2fnode-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" + integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + "@types/node@*", "@types/node@>=12": version "22.5.5" resolved "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz#52f939dd0f65fc552a4ad0b392f3c466cc5d7a44" @@ -13816,9 +13831,9 @@ moment-jdateformatparser@^1.2.1: integrity sha512-lpUeQtMaxmpK+pPPHGWMnqzgsB/nunbAGPg72mzvRNbxxeQ2uBurdq9EJmvJtOiYB6k/4T9kuvQFbb+8Tirn4A== moment-timezone@0.5.35, moment-timezone@^0.5.35: - version "0.5.45" - resolved "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz#cb685acd56bac10e69d93c536366eb65aa6bcf5c" - integrity sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ== + version "0.5.46" + resolved "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.46.tgz#a21aa6392b3c6b3ed916cd5e95858a28d893704a" + integrity sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw== dependencies: moment "^2.29.4"