From e75466103ffc22ddad5f8e9a1a2e0d76cdb0ba49 Mon Sep 17 00:00:00 2001 From: Danielle Adams <6271256+danielleadams@users.noreply.github.com> Date: Thu, 9 Sep 2021 08:28:06 -0400 Subject: [PATCH] feat(cli): add sandbox mode warning to amplify status (#8078) --- .../cfn-api-artifact-handler.test.ts.snap | 1 + ...p-sync-auth-type-bi-di-mapper.test.ts.snap | 2 + .../appSync-walkthrough.ts | 3 + ...nfig-to-app-sync-auth-type-bi-di-mapper.ts | 2 + packages/amplify-cli-core/src/index.ts | 5 +- .../src/__tests__/commands/status.test.ts | 15 +-- .../get-api-key-config.test.ts | 105 ++++++++++++++++++ .../show-global-sandbox-mode-warning.test.ts | 105 ++++++++++++++++++ .../mockLocalCloud/amplify-meta-2.json | 25 +++++ .../mockLocalCloud/amplify-meta-3.json | 16 +++ .../testData/mockLocalCloud/amplify-meta.json | 30 +++++ packages/amplify-cli/src/commands/status.ts | 26 +++-- .../amplify-cli/src/domain/amplify-toolkit.ts | 10 +- .../amplify-helpers/get-api-key-config.ts | 51 +++++++++ .../show-global-sandbox-mode-warning.ts | 31 ++++++ .../src/config/transformer-config.ts | 1 + .../src/transformation/transform.ts | 4 +- .../src/interface/api/add.ts | 1 + .../src/ModelAuthTransformer.ts | 11 +- 19 files changed, 409 insertions(+), 35 deletions(-) create mode 100644 packages/amplify-cli/src/__tests__/extensions/amplify-helpers/get-api-key-config.test.ts create mode 100644 packages/amplify-cli/src/__tests__/extensions/amplify-helpers/show-global-sandbox-mode-warning.test.ts create mode 100644 packages/amplify-cli/src/__tests__/extensions/amplify-helpers/testData/mockLocalCloud/amplify-meta-2.json create mode 100644 packages/amplify-cli/src/__tests__/extensions/amplify-helpers/testData/mockLocalCloud/amplify-meta-3.json create mode 100644 packages/amplify-cli/src/__tests__/extensions/amplify-helpers/testData/mockLocalCloud/amplify-meta.json create mode 100644 packages/amplify-cli/src/extensions/amplify-helpers/get-api-key-config.ts create mode 100644 packages/amplify-cli/src/extensions/amplify-helpers/show-global-sandbox-mode-warning.ts diff --git a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/__snapshots__/cfn-api-artifact-handler.test.ts.snap b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/__snapshots__/cfn-api-artifact-handler.test.ts.snap index 36f39965a9b..3b9aa6928f1 100644 --- a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/__snapshots__/cfn-api-artifact-handler.test.ts.snap +++ b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/__snapshots__/cfn-api-artifact-handler.test.ts.snap @@ -18,6 +18,7 @@ Object { }, Object { "apiKeyConfig": Object { + "apiKeyExpirationDate": undefined, "apiKeyExpirationDays": undefined, "description": undefined, }, diff --git a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/utils/__snapshots__/auth-config-to-app-sync-auth-type-bi-di-mapper.test.ts.snap b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/utils/__snapshots__/auth-config-to-app-sync-auth-type-bi-di-mapper.test.ts.snap index 73049e4ca95..3c18c0fa241 100644 --- a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/utils/__snapshots__/auth-config-to-app-sync-auth-type-bi-di-mapper.test.ts.snap +++ b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/utils/__snapshots__/auth-config-to-app-sync-auth-type-bi-di-mapper.test.ts.snap @@ -12,6 +12,7 @@ Object { exports[`AppSyncAuthType to authConfig maps API_KEY correctly 1`] = ` Object { "apiKeyConfig": Object { + "apiKeyExpirationDate": undefined, "apiKeyExpirationDays": 120, "description": undefined, }, @@ -47,6 +48,7 @@ Object { exports[`authConfig to AppSyncAuthType maps API_KEY auth correctly 1`] = ` Object { + "apiKeyExpirationDate": undefined, "expirationTime": 120, "keyDescription": "api key description", "mode": "API_KEY", 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 7ed94645ec8..d3bf8905d8b 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 @@ -21,6 +21,7 @@ import { $TSContext, open, } from 'amplify-cli-core'; +import { Duration, Expiration } from '@aws-cdk/core'; import { defineGlobalSandboxMode } from '../utils/global-sandbox-mode'; const serviceName = 'AppSync'; @@ -625,6 +626,8 @@ async function askApiKeyQuestions() { ]; const apiKeyConfig = await inquirer.prompt(apiKeyQuestions); + const apiKeyExpirationDaysNum = Number(apiKeyConfig.apiKeyExpirationDays); + apiKeyConfig.apiKeyExpirationDate = Expiration.after(Duration.days(apiKeyExpirationDaysNum)).date; return { authenticationType: 'API_KEY', diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/auth-config-to-app-sync-auth-type-bi-di-mapper.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/auth-config-to-app-sync-auth-type-bi-di-mapper.ts index d52094eae50..c1a8dde916a 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/auth-config-to-app-sync-auth-type-bi-di-mapper.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/auth-config-to-app-sync-auth-type-bi-di-mapper.ts @@ -30,6 +30,7 @@ const authConfigToAppSyncAuthTypeMap: Record AppSyn API_KEY: authConfig => ({ mode: 'API_KEY', expirationTime: authConfig.apiKeyConfig.apiKeyExpirationDays, + apiKeyExpirationDate: authConfig.apiKeyConfig?.apiKeyExpirationDate, keyDescription: authConfig.apiKeyConfig.description, }), AWS_IAM: () => ({ @@ -54,6 +55,7 @@ const appSyncAuthTypeToAuthConfigMap: Record $TSAny; sharedQuestions: () => $TSAny; showAllHelp: () => $TSAny; + showGlobalSandboxModeWarning: (context: $TSContext) => $TSAny; showHelp: (header: string, commands: { name: string; description: string }[]) => $TSAny; showHelpfulProviderLinks: (context: $TSContext) => $TSAny; showResourceTable: () => $TSAny; @@ -310,9 +311,7 @@ interface AmplifyToolkit { leaveBreadcrumbs: (category: string, resourceName: string, breadcrumbs: unknown) => void; readBreadcrumbs: (category: string, resourceName: string) => $TSAny; loadRuntimePlugin: (context: $TSContext, pluginId: string) => Promise<$TSAny>; - getImportedAuthProperties: ( - context: $TSContext, - ) => { + getImportedAuthProperties: (context: $TSContext) => { imported: boolean; userPoolId?: string; authRoleArn?: string; diff --git a/packages/amplify-cli/src/__tests__/commands/status.test.ts b/packages/amplify-cli/src/__tests__/commands/status.test.ts index d22a798a354..30f9dc59605 100644 --- a/packages/amplify-cli/src/__tests__/commands/status.test.ts +++ b/packages/amplify-cli/src/__tests__/commands/status.test.ts @@ -1,5 +1,3 @@ -import { UnknownArgumentError } from 'amplify-cli-core'; - describe('amplify status: ', () => { const { run } = require('../../commands/status'); const runStatusCmd = run; @@ -11,17 +9,10 @@ describe('amplify status: ', () => { }); it('status run method should call context.amplify.showStatusTable', async () => { - const cliInput = { - command: 'status', - subCommands: [], - options: { - verbose: true, - }, - }; - const mockContextNoCLArgs = { amplify: { showStatusTable: jest.fn(), + showGlobalSandboxModeWarning: jest.fn(), showHelpfulProviderLinks: jest.fn(), getCategoryPluginInfo: jest.fn().mockReturnValue({ packageLocation: mockPath }), }, @@ -43,6 +34,7 @@ describe('amplify status: ', () => { const mockContextWithVerboseOptionAndCLArgs = { amplify: { showStatusTable: jest.fn(), + showGlobalSandboxModeWarning: jest.fn(), showHelpfulProviderLinks: jest.fn(), getCategoryPluginInfo: jest.fn().mockReturnValue({ packageLocation: statusPluginInfo }), }, @@ -61,6 +53,7 @@ describe('amplify status: ', () => { const mockContextWithVerboseOptionWithCategoriesAndCLArgs = { amplify: { showStatusTable: jest.fn(), + showGlobalSandboxModeWarning: jest.fn(), showHelpfulProviderLinks: jest.fn(), getCategoryPluginInfo: jest.fn().mockReturnValue({ packageLocation: statusPluginInfo }), }, @@ -82,6 +75,7 @@ describe('amplify status: ', () => { const mockContextWithHelpSubcommandAndCLArgs = { amplify: { showStatusTable: jest.fn(), + showGlobalSandboxModeWarning: jest.fn(), showHelpfulProviderLinks: jest.fn(), getCategoryPluginInfo: jest.fn().mockReturnValue({ packageLocation: statusPluginInfo }), }, @@ -94,5 +88,4 @@ describe('amplify status: ', () => { //TBD: to move ViewResourceTableParams into a separate file for mocking instance functions. expect(mockContextWithHelpSubcommandAndCLArgs.amplify.showStatusTable.mock.calls.length).toBe(0); }); - }); diff --git a/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/get-api-key-config.test.ts b/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/get-api-key-config.test.ts new file mode 100644 index 00000000000..7ff8797ddf1 --- /dev/null +++ b/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/get-api-key-config.test.ts @@ -0,0 +1,105 @@ +import fs from 'fs'; +import { getAppSyncApiConfig, getApiKeyConfig, apiKeyIsActive, hasApiKey } from '../../../extensions/amplify-helpers/get-api-key-config'; +import { stateManager } from 'amplify-cli-core'; + +let amplifyMeta; + +jest.mock('amplify-cli-core', () => { + const original = jest.requireActual('amplify-cli-core'); + return { + ...original, + stateManager: { + metaFileExists: jest.fn(), + getMeta: jest.fn().mockImplementation(() => JSON.parse(amplifyMeta.toString())), + }, + }; +}); + +const stateManager_mock = stateManager as jest.Mocked; + +describe('getAppSyncApiConfig', () => { + beforeAll(() => { + amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta.json`); + }); + + it('returns the api object', async () => { + const result = getAppSyncApiConfig(); + + expect(result).toStrictEqual({ + service: 'AppSync', + providerPlugin: 'awscloudformation', + output: { + authConfig: { + defaultAuthentication: { + authenticationType: 'AWS_IAM', + }, + additionalAuthenticationProviders: [ + { + authenticationType: 'API_KEY', + apiKeyConfig: { + apiKeyExpirationDays: 2, + apiKeyExpirationDate: '2021-08-20T20:38:07.585Z', + description: '', + }, + }, + ], + }, + globalSandboxModeConfig: { + dev: { + enabled: true, + }, + }, + }, + }); + }); +}); + +describe('getApiKeyConfig', () => { + beforeAll(() => { + amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta.json`); + }); + + it('returns the api key config', () => { + const result = getApiKeyConfig(); + + expect(result).toStrictEqual({ + apiKeyExpirationDays: 2, + apiKeyExpirationDate: '2021-08-20T20:38:07.585Z', + description: '', + }); + }); +}); + +describe('apiKeyIsActive', () => { + describe('with expired key', () => { + beforeAll(() => { + amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta.json`); + }); + + it('returns false', () => { + expect(apiKeyIsActive()).toBe(false); + }); + }); + + describe('with no api key config', () => { + beforeAll(() => { + amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta-3.json`); + }); + + it('returns false', () => { + expect(apiKeyIsActive()).toBe(false); + }); + }); +}); + +describe('hasApiKey', () => { + describe('if api key config is present', () => { + beforeAll(() => { + amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta.json`); + }); + + it('returns true if api key is present', () => { + expect(hasApiKey()).toBe(true); + }); + }); +}); diff --git a/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/show-global-sandbox-mode-warning.test.ts b/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/show-global-sandbox-mode-warning.test.ts new file mode 100644 index 00000000000..db74c838d87 --- /dev/null +++ b/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/show-global-sandbox-mode-warning.test.ts @@ -0,0 +1,105 @@ +import { + globalSandboxModeEnabled, + showGlobalSandboxModeWarning, +} from '../../../extensions/amplify-helpers/show-global-sandbox-mode-warning'; +import { $TSContext } from '../../../../../amplify-cli-core/lib'; +import fs from 'fs'; +import chalk from 'chalk'; + +let ctx, amplifyMeta; + +jest.mock('amplify-cli-core', () => ({ + stateManager: { + getMeta: jest.fn(() => JSON.parse(amplifyMeta.toString())), + }, +})); + +describe('global sandbox mode warning', () => { + beforeEach(() => { + const envName = 'dev'; + ctx = { + amplify: { + getEnvInfo() { + return { envName }; + }, + }, + print: { + info() { + // noop + }, + }, + } as unknown as $TSContext; + }); + + describe('globalSandboxModeEnabled', () => { + describe('enabled', () => { + beforeAll(() => { + amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta.json`); + }); + + it('returns true', async () => { + expect(globalSandboxModeEnabled(ctx)).toBe(true); + }); + }); + + describe('not specified', () => { + beforeAll(() => { + amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta-2.json`); + }); + + it('returns false', async () => { + expect(globalSandboxModeEnabled(ctx)).toBe(false); + }); + }); + }); + + describe('showGlobalSandboxModeWarning', () => { + describe('sandbox mode enabled', () => { + beforeAll(() => { + amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta.json`); + }); + + it('prints warning message', async () => { + jest.spyOn(ctx.print, 'info'); + + await showGlobalSandboxModeWarning(ctx); + + expect(ctx.print.info).toBeCalledWith(` +⚠️ WARNING: ${chalk.green('"type AMPLIFY_GLOBAL @allow_public_data_access_with_api_key"')} in your GraphQL schema +allows public create, read, update, and delete access to all models via API Key. This +should only be used for testing purposes. API Key expiration date is: 8/20/2021 + +To configure PRODUCTION-READY authorization rules, review: https://docs.amplify.aws/cli/graphql-transformer/auth +`); + }); + }); + + describe('sandbox mode not specified', () => { + beforeAll(() => { + amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta-2.json`); + }); + + it('does not print warning', async () => { + jest.spyOn(ctx.print, 'info'); + + await showGlobalSandboxModeWarning(ctx); + + expect(ctx.print.info).toBeCalledTimes(0); + }); + }); + + describe('no api key config', () => { + beforeAll(() => { + amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta-3.json`); + }); + + it('does not print warning', async () => { + jest.spyOn(ctx.print, 'info'); + + await showGlobalSandboxModeWarning(ctx); + + expect(ctx.print.info).toBeCalledTimes(0); + }); + }); + }); +}); diff --git a/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/testData/mockLocalCloud/amplify-meta-2.json b/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/testData/mockLocalCloud/amplify-meta-2.json new file mode 100644 index 00000000000..9bd3f8293be --- /dev/null +++ b/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/testData/mockLocalCloud/amplify-meta-2.json @@ -0,0 +1,25 @@ +{ + "api": { + "ampapp": { + "service": "AppSync", + "providerPlugin": "awscloudformation", + "output": { + "authConfig": { + "defaultAuthentication": { + "authenticationType": "AWS_IAM" + }, + "additionalAuthenticationProviders": [ + { + "authenticationType": "API_KEY", + "apiKeyConfig": { + "apiKeyExpirationDays": 2, + "apiKeyExpirationDate": "2021-08-20T20:38:07.585Z", + "description": "" + } + } + ] + } + } + } + } +} diff --git a/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/testData/mockLocalCloud/amplify-meta-3.json b/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/testData/mockLocalCloud/amplify-meta-3.json new file mode 100644 index 00000000000..7aa92695860 --- /dev/null +++ b/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/testData/mockLocalCloud/amplify-meta-3.json @@ -0,0 +1,16 @@ +{ + "api": { + "ampapp": { + "service": "AppSync", + "providerPlugin": "awscloudformation", + "output": { + "authConfig": { + "defaultAuthentication": { + "authenticationType": "AWS_IAM" + }, + "additionalAuthenticationProviders": [] + } + } + } + } +} diff --git a/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/testData/mockLocalCloud/amplify-meta.json b/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/testData/mockLocalCloud/amplify-meta.json new file mode 100644 index 00000000000..aa3b39e78bc --- /dev/null +++ b/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/testData/mockLocalCloud/amplify-meta.json @@ -0,0 +1,30 @@ +{ + "api": { + "ampapp": { + "service": "AppSync", + "providerPlugin": "awscloudformation", + "output": { + "authConfig": { + "defaultAuthentication": { + "authenticationType": "AWS_IAM" + }, + "additionalAuthenticationProviders": [ + { + "authenticationType": "API_KEY", + "apiKeyConfig": { + "apiKeyExpirationDays": 2, + "apiKeyExpirationDate": "2021-08-20T20:38:07.585Z", + "description": "" + } + } + ] + }, + "globalSandboxModeConfig": { + "dev": { + "enabled": true + } + } + } + } + } +} diff --git a/packages/amplify-cli/src/commands/status.ts b/packages/amplify-cli/src/commands/status.ts index 3e11576346a..c821f9c1c94 100644 --- a/packages/amplify-cli/src/commands/status.ts +++ b/packages/amplify-cli/src/commands/status.ts @@ -1,26 +1,28 @@ -import { ViewResourceTableParams, CLIParams, $TSContext } from "amplify-cli-core"; +import { ViewResourceTableParams, CLIParams, $TSContext } from 'amplify-cli-core'; +export const run = async (context: $TSContext) => { + const cliParams: CLIParams = { + cliCommand: context?.input?.command, + cliSubcommands: context?.input?.subCommands, + cliOptions: context?.input?.options, + }; -export const run = async (context : $TSContext) => { - const cliParams:CLIParams = { cliCommand : context?.input?.command, - cliSubcommands: context?.input?.subCommands, - cliOptions : context?.input?.options } - - const view = new ViewResourceTableParams( cliParams ); - if ( context?.input?.subCommands?.includes("help")){ - context.print.info( view.getStyledHelp() ); + const view = new ViewResourceTableParams(cliParams); + if (context?.input?.subCommands?.includes('help')) { + context.print.info(view.getStyledHelp()); } else { try { - await context.amplify.showStatusTable( view ); + await context.amplify.showStatusTable(view); + await context.amplify.showGlobalSandboxModeWarning(context); await context.amplify.showHelpfulProviderLinks(context); await showAmplifyConsoleHostingStatus(context); - } catch ( e ){ + } catch (e) { view.logErrorException(e, context); } } }; -async function showAmplifyConsoleHostingStatus( context) { +async function showAmplifyConsoleHostingStatus(context) { const pluginInfo = context.amplify.getCategoryPluginInfo(context, 'hosting', 'amplifyhosting'); if (pluginInfo && pluginInfo.packageLocation) { const { status } = await import(pluginInfo.packageLocation); diff --git a/packages/amplify-cli/src/domain/amplify-toolkit.ts b/packages/amplify-cli/src/domain/amplify-toolkit.ts index 55f1b41b753..512d7fefd9c 100644 --- a/packages/amplify-cli/src/domain/amplify-toolkit.ts +++ b/packages/amplify-cli/src/domain/amplify-toolkit.ts @@ -38,6 +38,7 @@ export class AmplifyToolkit { private _removeResource: any; private _sharedQuestions: any; private _showAllHelp: any; + private _showGlobalSandboxModeWarning: any; private _showHelp: any; private _showHelpfulProviderLinks: any; private _showResourceTable: any; @@ -240,6 +241,12 @@ export class AmplifyToolkit { this._sharedQuestions = this._sharedQuestions || require(path.join(this._amplifyHelpersDirPath, 'shared-questions')).sharedQuestions; return this._sharedQuestions; } + get showGlobalSandboxModeWarning(): any { + this._showGlobalSandboxModeWarning = + this._showGlobalSandboxModeWarning || + require(path.join(this._amplifyHelpersDirPath, 'show-global-sandbox-mode-warning')).showGlobalSandboxModeWarning; + return this._showGlobalSandboxModeWarning; + } get showHelp(): any { this._showHelp = this._showHelp || require(path.join(this._amplifyHelpersDirPath, 'show-help')).showHelp; return this._showHelp; @@ -261,8 +268,7 @@ export class AmplifyToolkit { } get showStatusTable(): any { - this._showStatusTable = - this._showStatusTable || require(path.join(this._amplifyHelpersDirPath, 'resource-status')).showStatusTable; + this._showStatusTable = this._showStatusTable || require(path.join(this._amplifyHelpersDirPath, 'resource-status')).showStatusTable; return this._showStatusTable; } diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/get-api-key-config.ts b/packages/amplify-cli/src/extensions/amplify-helpers/get-api-key-config.ts new file mode 100644 index 00000000000..b3f9a1d770e --- /dev/null +++ b/packages/amplify-cli/src/extensions/amplify-helpers/get-api-key-config.ts @@ -0,0 +1,51 @@ +import { stateManager } from 'amplify-cli-core'; +import { ApiKeyConfig } from '@aws-amplify/graphql-transformer-core'; + +export function getAppSyncApiConfig(): any { + const apiConfig = stateManager.getMeta()?.api; + let appSyncApi; + + Object.keys(apiConfig).forEach(k => { + if (apiConfig[k]['service'] === 'AppSync') appSyncApi = apiConfig[k]; + }); + + return appSyncApi; +} + +function getDefaultIfApiKey(): ApiKeyConfig | void { + const authConfig = getAppSyncApiConfig()?.output?.authConfig; + const { defaultAuthentication } = authConfig; + + if (defaultAuthentication.authenticationType === 'API_KEY') return defaultAuthentication.apiKeyConfig; +} + +function getAdditionalApiKeyConfig(): ApiKeyConfig | void { + const authConfig = getAppSyncApiConfig()?.output?.authConfig; + const { additionalAuthenticationProviders } = authConfig; + let apiKeyConfig; + + additionalAuthenticationProviders.forEach(authProvider => { + if (authProvider.authenticationType === 'API_KEY') apiKeyConfig = authProvider.apiKeyConfig; + }); + + return apiKeyConfig; +} + +export function getApiKeyConfig(): ApiKeyConfig | void { + return getDefaultIfApiKey() || getAdditionalApiKeyConfig(); +} + +export function apiKeyIsActive(): boolean { + const today = new Date(); + const { apiKeyExpirationDate } = getApiKeyConfig() || {}; + + if (!apiKeyExpirationDate) return false; + + return new Date(apiKeyExpirationDate) > today; +} + +export function hasApiKey(): boolean { + const apiKeyConfig = getApiKeyConfig(); + + return !!apiKeyConfig && !!apiKeyConfig?.apiKeyExpirationDate; +} diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/show-global-sandbox-mode-warning.ts b/packages/amplify-cli/src/extensions/amplify-helpers/show-global-sandbox-mode-warning.ts new file mode 100644 index 00000000000..439f11db60b --- /dev/null +++ b/packages/amplify-cli/src/extensions/amplify-helpers/show-global-sandbox-mode-warning.ts @@ -0,0 +1,31 @@ +import chalk from 'chalk'; +import { $TSContext } from 'amplify-cli-core'; +import { getAppSyncApiConfig, getApiKeyConfig } from './get-api-key-config'; + +export function globalSandboxModeEnabled(context: $TSContext): boolean { + const appSyncApi = getAppSyncApiConfig(); + const currEnvName = context.amplify.getEnvInfo().envName; + const { globalSandboxModeConfig } = appSyncApi.output || {}; + + if (!globalSandboxModeConfig) return false; + + return globalSandboxModeConfig[currEnvName]?.enabled; +} + +export function showGlobalSandboxModeWarning(context: $TSContext): void { + const apiKeyConfig = getApiKeyConfig(); + + if (!apiKeyConfig?.apiKeyExpirationDate) return; + + const expirationDate = new Date(apiKeyConfig.apiKeyExpirationDate); + + if (apiKeyConfig && globalSandboxModeEnabled(context)) { + context.print.info(` +⚠️ WARNING: ${chalk.green('"type AMPLIFY_GLOBAL @allow_public_data_access_with_api_key"')} in your GraphQL schema +allows public create, read, update, and delete access to all models via API Key. This +should only be used for testing purposes. API Key expiration date is: ${expirationDate.toLocaleDateString()} + +To configure PRODUCTION-READY authorization rules, review: https://docs.amplify.aws/cli/graphql-transformer/auth +`); + } +} diff --git a/packages/amplify-graphql-transformer-core/src/config/transformer-config.ts b/packages/amplify-graphql-transformer-core/src/config/transformer-config.ts index 5abc2caa026..b17b249bf13 100644 --- a/packages/amplify-graphql-transformer-core/src/config/transformer-config.ts +++ b/packages/amplify-graphql-transformer-core/src/config/transformer-config.ts @@ -36,6 +36,7 @@ export type AppSyncAuthConfigurationOIDCEntry = { export type ApiKeyConfig = { description?: string; apiKeyExpirationDays: number; + apiKeyExpirationDate?: Date; }; export type UserPoolConfig = { userPoolId: string; diff --git a/packages/amplify-graphql-transformer-core/src/transformation/transform.ts b/packages/amplify-graphql-transformer-core/src/transformation/transform.ts index 1e5422bd20e..1d6c2c7286e 100644 --- a/packages/amplify-graphql-transformer-core/src/transformation/transform.ts +++ b/packages/amplify-graphql-transformer-core/src/transformation/transform.ts @@ -6,7 +6,7 @@ import { TransformHostProvider, } from '@aws-amplify/graphql-transformer-interfaces'; import { AuthorizationMode, AuthorizationType } from '@aws-cdk/aws-appsync'; -import { App, Aws, CfnOutput, Fn } from '@aws-cdk/core'; +import { App, Aws, CfnOutput, Fn, Duration, Expiration } from '@aws-cdk/core'; import assert from 'assert'; import { EnumTypeDefinitionNode, @@ -88,12 +88,14 @@ export class GraphQLTransform { } const sortedTransformers = sortTransformerPlugins(options.transformers); this.transformers = sortedTransformers; + const apiKeyExpirationDate = Expiration.after(Duration.days(7)).date; this.authConfig = options.authConfig || { defaultAuthentication: { authenticationType: 'API_KEY', apiKeyConfig: { apiKeyExpirationDays: 7, + apiKeyExpirationDate, description: 'Default API Key', }, }, diff --git a/packages/amplify-headless-interface/src/interface/api/add.ts b/packages/amplify-headless-interface/src/interface/api/add.ts index 51f44124538..081eaf07e57 100644 --- a/packages/amplify-headless-interface/src/interface/api/add.ts +++ b/packages/amplify-headless-interface/src/interface/api/add.ts @@ -130,6 +130,7 @@ export type AppSyncAuthType = export interface AppSyncAPIKeyAuthType { mode: 'API_KEY'; expirationTime?: number; + apiKeyExpirationDate?: Date; keyDescription?: string; } diff --git a/packages/graphql-auth-transformer/src/ModelAuthTransformer.ts b/packages/graphql-auth-transformer/src/ModelAuthTransformer.ts index cf2fab44ac3..6e41670abd6 100644 --- a/packages/graphql-auth-transformer/src/ModelAuthTransformer.ts +++ b/packages/graphql-auth-transformer/src/ModelAuthTransformer.ts @@ -98,6 +98,7 @@ export type AppSyncAuthConfigurationEntry = { export type ApiKeyConfig = { description?: string; apiKeyExpirationDays: number; + apiKeyExpirationDate?: Date; }; export type UserPoolConfig = { userPoolId: string; @@ -1387,9 +1388,8 @@ operations will be generated by the CLI.`, // In create mutations, the dynamic group and ownership authorization checks // are done before calling PutItem. - const dynamicGroupAuthorizationExpression = this.resources.dynamicGroupAuthorizationExpressionForCreateOperations( - dynamicGroupAuthorizationRules, - ); + const dynamicGroupAuthorizationExpression = + this.resources.dynamicGroupAuthorizationExpressionForCreateOperations(dynamicGroupAuthorizationRules); const fieldIsList = (fieldName: string) => { const field = parent.fields.find(field => field.name.value === fieldName); if (field) { @@ -1547,9 +1547,8 @@ operations will be generated by the CLI.`, ]), ); - const throwIfNotStaticGroupAuthorizedOrAuthConditionIsEmpty = this.resources.throwIfNotStaticGroupAuthorizedOrAuthConditionIsEmpty( - field, - ); + const throwIfNotStaticGroupAuthorizedOrAuthConditionIsEmpty = + this.resources.throwIfNotStaticGroupAuthorizedOrAuthConditionIsEmpty(field); // If we've any modes to check, then add the authMode check code block // to the start of the resolver.