diff --git a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/utils/global-sandbox-mode.test.ts b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/utils/global-sandbox-mode.test.ts index a895da7f667..315084e96bd 100644 --- a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/utils/global-sandbox-mode.test.ts +++ b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/utils/global-sandbox-mode.test.ts @@ -1,21 +1,10 @@ import { defineGlobalSandboxMode } from '../../../../provider-utils/awscloudformation/utils/global-sandbox-mode'; -import { $TSContext } from 'amplify-cli-core'; describe('global sandbox mode GraphQL directive', () => { - it('returns AMPLIFY_DIRECTIVE type with code comment, directive, and env name', () => { - const envName = 'envone'; - const ctx = <$TSContext>{ - amplify: { - getEnvInfo() { - return { envName }; - }, - }, - }; - - expect(defineGlobalSandboxMode(ctx)) - .toBe(`# This allows public create, read, update, and delete access for a limited time to all models via API Key. -# To configure PRODUCTION-READY authorization rules, review: https://docs.amplify.aws/cli/graphql-transformer/auth -type AMPLIFY_GLOBAL @allow_public_data_access_with_api_key(in: \"${envName}\") # FOR TESTING ONLY!\n + it('returns input AMPLIFY with code comment', () => { + expect(defineGlobalSandboxMode()).toEqual(`# This "input" configures a global authorization rule to enable public access to +# all models in this schema. Learn more about authorization rules here: https://docs.amplify.aws/cli/graphql-transformer/auth +input AMPLIFY { global_auth_rule: AuthorizationRule = { allow: public } } # FOR TESTING ONLY!\n `); }); }); diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.ts index 21780837768..204aaba2c67 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.ts @@ -419,7 +419,7 @@ export const serviceWalkthrough = async (context: $TSContext, defaultValuesFilen const { templateSelection } = await inquirer.prompt(templateSelectionQuestion); const schemaFilePath = path.join(graphqlSchemaDir, templateSelection); - schemaContent += transformerVersion === 2 && templateSelection !== blankSchemaFile ? defineGlobalSandboxMode(context) : ''; + schemaContent += transformerVersion === 2 ? defineGlobalSandboxMode() : ''; schemaContent += fs.readFileSync(schemaFilePath, 'utf8'); return { @@ -664,8 +664,8 @@ async function addLambdaAuthorizerChoice(context) { const transformerVersion = providerPlugin.getTransformerVersion(context); if (transformerVersion === 2 && !authProviderChoices.some(choice => choice.value == 'AWS_LAMBDA')) { authProviderChoices.push({ - name: 'Lambda', - value: 'AWS_LAMBDA', + name: 'Lambda', + value: 'AWS_LAMBDA', }); } } @@ -1073,7 +1073,7 @@ async function askLambdaQuestion(context) { const lambdaAuthorizerConfig = { lambdaFunction, ttlSeconds, - } + }; return { authenticationType: 'AWS_LAMBDA', @@ -1130,9 +1130,7 @@ async function askLambdaFromProject(context: $TSContext) { default: lambdaFunctions[0], }); - await context.amplify.invokePluginMethod(context, 'function', undefined, 'addAppSyncInvokeMethodPermission', [ - answer.lambdaFunction, - ]); + await context.amplify.invokePluginMethod(context, 'function', undefined, 'addAppSyncInvokeMethodPermission', [answer.lambdaFunction]); return { lambdaFunction: answer.lambdaFunction }; } @@ -1180,4 +1178,4 @@ async function createLambdaAuthorizerFunction(context: $TSContext) { context.print.success(`Successfully added ${functionName} function locally`); return functionName; -}; \ No newline at end of file +} diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/global-sandbox-mode.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/global-sandbox-mode.ts index 27fc26f1f30..e0a703267c3 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/global-sandbox-mode.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/global-sandbox-mode.ts @@ -1,10 +1,6 @@ -import { $TSContext } from 'amplify-cli-core'; - -export function defineGlobalSandboxMode(context: $TSContext): string { - const envName = context.amplify.getEnvInfo().envName; - - return `# This allows public create, read, update, and delete access for a limited time to all models via API Key. -# To configure PRODUCTION-READY authorization rules, review: https://docs.amplify.aws/cli/graphql-transformer/auth -type AMPLIFY_GLOBAL @allow_public_data_access_with_api_key(in: \"${envName}\") # FOR TESTING ONLY!\n +export function defineGlobalSandboxMode(): string { + return `# This "input" configures a global authorization rule to enable public access to +# all models in this schema. Learn more about authorization rules here: https://docs.amplify.aws/cli/graphql-transformer/auth +input AMPLIFY { global_auth_rule: AuthorizationRule = { allow: public } } # FOR TESTING ONLY!\n `; } diff --git a/packages/amplify-e2e-core/src/categories/api.ts b/packages/amplify-e2e-core/src/categories/api.ts index a672c29944f..ae892f36deb 100644 --- a/packages/amplify-e2e-core/src/categories/api.ts +++ b/packages/amplify-e2e-core/src/categories/api.ts @@ -65,6 +65,59 @@ export function addApiWithoutSchema(cwd: string, opts: Partial = {}) { + const options = _.assign(defaultOptions, opts); + return new Promise((resolve, reject) => { + spawn(getCLIPath(options.testingWithLatestCodebase), ['add', 'api'], { cwd, stripColors: true }) + .wait('Please select from one of the below mentioned services:') + .sendCarriageReturn() + .wait(/.*Here is the GraphQL API that we will create. Select a setting to edit or continue.*/) + .sendCarriageReturn() + .wait('Choose a schema template:') + .sendCarriageReturn() + .wait('Do you want to edit the schema now?') + .sendConfirmNo() + .wait( + '"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud', + ) + .sendEof() + .run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} + +export function addApiWithThreeModels(cwd: string, opts: Partial = {}) { + const options = _.assign(defaultOptions, opts); + return new Promise((resolve, reject) => { + spawn(getCLIPath(options.testingWithLatestCodebase), ['add', 'api'], { cwd, stripColors: true }) + .wait('Please select from one of the below mentioned services:') + .sendCarriageReturn() + .wait(/.*Here is the GraphQL API that we will create. Select a setting to edit or continue.*/) + .sendCarriageReturn() + .wait('Choose a schema template:') + .sendKeyDown(1) + .sendCarriageReturn() + .wait('Do you want to edit the schema now?') + .sendConfirmNo() + .wait( + '"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud', + ) + .sendEof() + .run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} + export function addApiWithBlankSchema(cwd: string, opts: Partial = {}) { const options = _.assign(defaultOptions, opts); return new Promise((resolve, reject) => { diff --git a/packages/amplify-e2e-tests/schemas/model_with_sandbox_mode.graphql b/packages/amplify-e2e-tests/schemas/model_with_sandbox_mode.graphql index 22c833e9c67..d44e95b1a84 100644 --- a/packages/amplify-e2e-tests/schemas/model_with_sandbox_mode.graphql +++ b/packages/amplify-e2e-tests/schemas/model_with_sandbox_mode.graphql @@ -1,4 +1,6 @@ -type AMPLIFY_GLOBAL @allow_public_data_access_with_api_key(in: "dev") +input AMPLIFY { + global_auth_rule: AuthorizationRule = { allow: public } +} type Todo @model { id: ID! diff --git a/packages/amplify-e2e-tests/src/__tests__/global_sandbox.test.ts b/packages/amplify-e2e-tests/src/__tests__/global_sandbox.test.ts new file mode 100644 index 00000000000..cff50442b5e --- /dev/null +++ b/packages/amplify-e2e-tests/src/__tests__/global_sandbox.test.ts @@ -0,0 +1,47 @@ +import { + initJSProjectWithProfile, + deleteProject, + addFeatureFlag, + addApiWithoutSchema, + addApiWithOneModel, + addApiWithThreeModels, + updateApiSchema, + apiGqlCompile, + amplifyPush, +} from 'amplify-e2e-core'; +import { createNewProjectDir, deleteProjectDir } from 'amplify-e2e-core'; + +describe('global sandbox mode', () => { + let projectDir: string; + let apiName = 'sandbox'; + + beforeEach(async () => { + projectDir = await createNewProjectDir('sandbox'); + await initJSProjectWithProfile(projectDir); + addFeatureFlag(projectDir, 'graphqltransformer', 'useexperimentalpipelinedtransformer', true); + }); + + afterEach(async () => { + await deleteProject(projectDir); + deleteProjectDir(projectDir); + }); + + it('compiles schema with one model and pushes to cloud', async () => { + await addApiWithOneModel(projectDir); + await apiGqlCompile(projectDir, true); + await amplifyPush(projectDir, true); + }); + + it.skip('compiles schema with three models and pushes to cloud', async () => { + await addApiWithThreeModels(projectDir); + await apiGqlCompile(projectDir, true); + await amplifyPush(projectDir, true); + }); + + it('compiles schema user-added schema and pushes to cloud', async () => { + await addApiWithoutSchema(projectDir, { apiName }); + updateApiSchema(projectDir, apiName, 'model_with_sandbox_mode.graphql'); + await apiGqlCompile(projectDir, true); + await amplifyPush(projectDir, true); + }); +}); diff --git a/packages/amplify-graphql-transformer-core/src/transformation/validation.ts b/packages/amplify-graphql-transformer-core/src/transformation/validation.ts index 8d6bbd68434..8cd31547cec 100644 --- a/packages/amplify-graphql-transformer-core/src/transformation/validation.ts +++ b/packages/amplify-graphql-transformer-core/src/transformation/validation.ts @@ -103,6 +103,7 @@ scalar AWSPhone scalar AWSIPAddress scalar BigInt scalar Double +scalar AuthorizationRule `); export const EXTRA_DIRECTIVES_DOCUMENT = parse(` diff --git a/packages/amplify-graphql-transformer-core/src/transformer-context/output.ts b/packages/amplify-graphql-transformer-core/src/transformer-context/output.ts index d8429a657af..ba65d780485 100644 --- a/packages/amplify-graphql-transformer-core/src/transformer-context/output.ts +++ b/packages/amplify-graphql-transformer-core/src/transformer-context/output.ts @@ -27,6 +27,8 @@ import { DEFAULT_SCHEMA_DEFINITION } from '../utils/defaultSchema'; import { TransformerContextOutputProvider } from '@aws-amplify/graphql-transformer-interfaces'; import assert from 'assert'; +const AMPLIFY = 'AMPLIFY'; + export function blankObject(name: string): ObjectTypeDefinitionNode { return { kind: 'ObjectTypeDefinition', @@ -73,6 +75,7 @@ export class TransformerOutput implements TransformerContextOutputProvider { case Kind.ENUM_TYPE_DEFINITION: case Kind.UNION_TYPE_DEFINITION: const typeDef = inputDef as TypeDefinitionNode; + if (this.isAmplifyInput(typeDef.name.value)) break; if (!this.getType(typeDef.name.value)) { this.addType(typeDef); } @@ -612,4 +615,8 @@ export class TransformerOutput implements TransformerContextOutputProvider { directives: [], }; } + + private isAmplifyInput(inputName: string): boolean { + return inputName === AMPLIFY; + } } diff --git a/packages/amplify-provider-awscloudformation/src/__tests__/utils/sandbox-mode-helpers.test.ts b/packages/amplify-provider-awscloudformation/src/__tests__/utils/sandbox-mode-helpers.test.ts index c653c5ff48f..760318d4418 100644 --- a/packages/amplify-provider-awscloudformation/src/__tests__/utils/sandbox-mode-helpers.test.ts +++ b/packages/amplify-provider-awscloudformation/src/__tests__/utils/sandbox-mode-helpers.test.ts @@ -1,4 +1,4 @@ -import { showSandboxModePrompts, removeSandboxDirectiveFromSchema, showGlobalSandboxModeWarning } from '../../utils/sandbox-mode-helpers'; +import { showSandboxModePrompts, showGlobalSandboxModeWarning, schemaHasSandboxModeEnabled } from '../../utils/sandbox-mode-helpers'; import { $TSContext } from 'amplify-cli-core'; import chalk from 'chalk'; import * as prompts from 'amplify-prompts'; @@ -35,9 +35,9 @@ describe('sandbox mode helpers', () => { expect(prompts.printer.info).toBeCalledWith( ` ⚠️ WARNING: Global Sandbox Mode has been enabled, which requires a valid API key. If -you'd like to disable, remove ${chalk.green('"type AMPLIFY_GLOBAL @allow_public_data_access_with_api_key"')} +you'd like to disable, remove ${chalk.green('"input AMPLIFY { global_auth_rule: AuthorizationRule = { allow: public } }"')} from your GraphQL schema and run 'amplify push' again. If you'd like to proceed with -sandbox mode disabled in '${ctx.amplify.getEnvInfo().envName}', do not create an API Key. +sandbox mode disabled, do not create an API Key. `, 'yellow', ); @@ -59,59 +59,72 @@ sandbox mode disabled in '${ctx.amplify.getEnvInfo().envName}', do not create an }); }); - describe('removeSandboxDirectiveFromSchema', () => { - it('removes sandbox mode directive', () => { + describe('schemaHasSandboxModeEnabled', () => { + it('parses sandbox AMPLIFY input on schema', () => { const schema = ` -type AMPLIFY_GLOBAL @allow_public_data_access_with_api_key(in: "dev") + input AMPLIFY { global_auth_rule: AuthorizationRule = { allow: public } } `; - expect(removeSandboxDirectiveFromSchema(schema)).toEqual(` - - `); + expect(schemaHasSandboxModeEnabled(schema)).toEqual(true); }); - it('does not change user schema with directive', () => { + it('passes through when AMPLIFY input is not present', () => { const schema = ` -type AMPLIFY_GLOBAL @allow_public_data_access_with_api_key(in: "dev10105") # FOR TESTING ONLY! - -type Todo @model { - id: ID! - name: String! - description: String -} + type Todo @model { + id: ID! + content: String + } `; - expect(removeSandboxDirectiveFromSchema(schema)).toEqual(` - # FOR TESTING ONLY! - -type Todo @model { - id: ID! - name: String! - description: String -} - `); + expect(schemaHasSandboxModeEnabled(schema)).toEqual(false); }); - it('does not change user schema with directive and single quotes', () => { - const schema = ` -type AMPLIFY_GLOBAL @allow_public_data_access_with_api_key(in: 'dev10105') # FOR TESTING ONLY! + describe('input AMPLIFY has incorrect values', () => { + it('checks for "global_auth_rule"', () => { + const schema = ` + input AMPLIFY { auth_rule: AuthenticationRule = { allow: public } } + `; -type Todo @model { - id: ID! - name: String! - description: String -} - `; + expect(() => schemaHasSandboxModeEnabled(schema)).toThrow( + Error('input AMPLIFY requires "global_auth_rule" field. Learn more here: https://docs.amplify.aws/cli/graphql-transformer/auth'), + ); + }); - expect(removeSandboxDirectiveFromSchema(schema)).toEqual(` - # FOR TESTING ONLY! + it('guards for AuthorizationRule', () => { + const schema = ` + input AMPLIFY { global_auth_rule: AuthenticationRule = { allow: public } } + `; -type Todo @model { - id: ID! - name: String! - description: String -} - `); + expect(() => schemaHasSandboxModeEnabled(schema)).toThrow( + Error( + 'There was a problem with your auth configuration. Learn more about auth here: https://docs.amplify.aws/cli/graphql-transformer/auth', + ), + ); + }); + + it('checks for "allow" field name', () => { + const schema = ` + input AMPLIFY { global_auth_rule: AuthorizationRule = { allows: public } } + `; + + expect(() => schemaHasSandboxModeEnabled(schema)).toThrow( + Error( + 'There was a problem with your auth configuration. Learn more about auth here: https://docs.amplify.aws/cli/graphql-transformer/auth', + ), + ); + }); + + it('checks for "public" value from "allow" field', () => { + const schema = ` + input AMPLIFY { global_auth_rule: AuthorizationRule = { allow: private } } + `; + + expect(() => schemaHasSandboxModeEnabled(schema)).toThrowError( + Error( + 'There was a problem with your auth configuration. Learn more about auth here: https://docs.amplify.aws/cli/graphql-transformer/auth', + ), + ); + }); }); }); }); diff --git a/packages/amplify-provider-awscloudformation/src/graphql-transformer/transform-graphql-schema.ts b/packages/amplify-provider-awscloudformation/src/graphql-transformer/transform-graphql-schema.ts index 70d344b2819..6d6d2ce5c2e 100644 --- a/packages/amplify-provider-awscloudformation/src/graphql-transformer/transform-graphql-schema.ts +++ b/packages/amplify-provider-awscloudformation/src/graphql-transformer/transform-graphql-schema.ts @@ -8,7 +8,6 @@ import { getAppSyncServiceExtraDirectives, GraphQLTransform, collectDirectivesByTypeNames, - collectDirectives, TransformerProjectConfig, } from '@aws-amplify/graphql-transformer-core'; import { ModelTransformer } from '@aws-amplify/graphql-model-transformer'; @@ -38,8 +37,7 @@ import { ResourceConstants } from 'graphql-transformer-common'; import { showGlobalSandboxModeWarning, showSandboxModePrompts, - getSandboxModeEnvNameFromDirectiveSet, - removeSandboxDirectiveFromSchema, + schemaHasSandboxModeEnabled, } from '../utils/sandbox-mode-helpers'; import { printer } from 'amplify-prompts'; import { GraphQLSanityCheck, SanityCheckRules } from './sanity-check'; @@ -299,9 +297,7 @@ export async function transformGraphQLSchema(context, options) { ? await loadProject(previouslyDeployedBackendDir) : undefined; - const { envName } = context.amplify._getEnvInfo(); - const sandboxModeEnv = getSandboxModeEnvNameFromDirectiveSet(collectDirectives(project.schema)); - const sandboxModeEnabled = envName === sandboxModeEnv; + const sandboxModeEnabled = schemaHasSandboxModeEnabled(project.schema); const directiveMap = collectDirectivesByTypeNames(project.schema); const hasApiKey = authConfig.defaultAuthentication.authenticationType === 'API_KEY' || @@ -500,9 +496,7 @@ async function _buildProject(opts: ProjectOptions) { sandboxModeEnabled: opts.sandboxModeEnabled, }); - let schema = userProjectConfig.schema.toString(); - if (opts.sandboxModeEnabled) schema = removeSandboxDirectiveFromSchema(schema); - + const schema = userProjectConfig.schema.toString(); const transformOutput = transform.transform(schema); return mergeUserConfigWithTransformOutput(userProjectConfig, transformOutput); diff --git a/packages/amplify-provider-awscloudformation/src/utils/sandbox-mode-helpers.ts b/packages/amplify-provider-awscloudformation/src/utils/sandbox-mode-helpers.ts index 0c3e55d3c67..2dff5984df3 100644 --- a/packages/amplify-provider-awscloudformation/src/utils/sandbox-mode-helpers.ts +++ b/packages/amplify-provider-awscloudformation/src/utils/sandbox-mode-helpers.ts @@ -2,15 +2,22 @@ import chalk from 'chalk'; import { $TSContext } from 'amplify-cli-core'; import { hasApiKey } from './api-key-helpers'; import { printer } from 'amplify-prompts'; +import { parse } from 'graphql'; + +const AMPLIFY = 'AMPLIFY'; +const GLOBAL_AUTH_RULE = 'global_auth_rule'; +const AUTHORIZATION_RULE = 'AuthorizationRule'; +const ALLOW = 'allow'; +const PUBLIC = 'public'; export async function showSandboxModePrompts(context: $TSContext): Promise { if (!hasApiKey()) { printer.info( ` ⚠️ WARNING: Global Sandbox Mode has been enabled, which requires a valid API key. If -you'd like to disable, remove ${chalk.green('"type AMPLIFY_GLOBAL @allow_public_data_access_with_api_key"')} +you'd like to disable, remove ${chalk.green('"input AMPLIFY { global_auth_rule: AuthorizationRule = { allow: public } }"')} from your GraphQL schema and run 'amplify push' again. If you'd like to proceed with -sandbox mode disabled in '${context.amplify.getEnvInfo().envName}', do not create an API Key. +sandbox mode disabled, do not create an API Key. `, 'yellow', ); @@ -27,16 +34,33 @@ export function showGlobalSandboxModeWarning(): void { ); } -export function getSandboxModeEnvNameFromDirectiveSet(input: any): string { - const sandboxModeDirective = input.find((el: any) => el.name.value === 'allow_public_data_access_with_api_key'); +export function schemaHasSandboxModeEnabled(schema: string): boolean { + const { definitions } = parse(schema); + const amplifyInputType: any = definitions.find((d: any) => d.kind === 'InputObjectTypeDefinition' && d.name.value === AMPLIFY); + + if (!amplifyInputType) { + return false; + } - if (!sandboxModeDirective) return ''; + const authRuleField = amplifyInputType.fields.find(f => f.name.value === GLOBAL_AUTH_RULE); - const inField = sandboxModeDirective.arguments.find((el: any) => el.name.value === 'in'); - return inField.value.value; -} + if (!authRuleField) { + throw Error('input AMPLIFY requires "global_auth_rule" field. Learn more here: https://docs.amplify.aws/cli/graphql-transformer/auth'); + } + + const typeName = authRuleField.type.name.value; + const defaultValueField = authRuleField.defaultValue.fields[0]; + const defaultValueName = defaultValueField.name.value; + const defaultValueValue = defaultValueField.value.value; + const authScalarMatch = typeName === AUTHORIZATION_RULE; + const defaultValueNameMatch = defaultValueName === ALLOW; + const defaultValueValueMatch = defaultValueValue === PUBLIC; -export function removeSandboxDirectiveFromSchema(schema): string { - const ampGlobalRegex = /(type AMPLIFY_GLOBAL @allow_public_data_access_with_api_key\(in:)+(.*?)+(\))/g; - return schema.replace(ampGlobalRegex, ''); + if (authScalarMatch && defaultValueNameMatch && defaultValueValueMatch) { + return true; + } else { + throw Error( + 'There was a problem with your auth configuration. Learn more about auth here: https://docs.amplify.aws/cli/graphql-transformer/auth', + ); + } }