From 2c0f74ad541eec09687ece0eb7571a4f38c9e56e Mon Sep 17 00:00:00 2001 From: Danielle Adams Date: Fri, 27 Aug 2021 18:37:23 -0400 Subject: [PATCH] feat(cli): add sandbox mode warning to amplify status --- .../cfn-api-artifact-handler.test.ts.snap | 1 + ...p-sync-auth-type-bi-di-mapper.test.ts.snap | 2 + .../appSync-walkthrough.ts | 22 ++-- ...nfig-to-app-sync-auth-type-bi-di-mapper.ts | 2 + packages/amplify-cli-core/src/index.ts | 5 +- .../amplify-helpers/api-key.test.ts | 78 ++++++++++++ .../show-global-sandbox-mode-warning.test.ts | 67 +++++++++++ .../mockLocalCloud/amplify-meta-2.json | 25 ++++ .../testData/mockLocalCloud/amplify-meta.json | 30 +++++ packages/amplify-cli/src/commands/status.ts | 26 ++-- .../amplify-cli/src/domain/amplify-toolkit.ts | 10 +- .../src/extensions/amplify-helpers/api-key.ts | 51 ++++++++ .../amplify-helpers/push-resources.ts | 2 + .../show-global-sandbox-mode-warning.ts | 31 +++++ .../src/config/transformer-config.ts | 1 + .../src/transformation/transform.ts | 4 +- .../src/transformation/transformer-config.ts | 111 ++++++++++++++++++ .../src/interface/api/add.ts | 1 + .../src/ModelAuthTransformer.ts | 11 +- 19 files changed, 448 insertions(+), 32 deletions(-) create mode 100644 packages/amplify-cli/src/__tests__/extensions/amplify-helpers/api-key.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.json create mode 100644 packages/amplify-cli/src/extensions/amplify-helpers/api-key.ts create mode 100644 packages/amplify-cli/src/extensions/amplify-helpers/show-global-sandbox-mode-warning.ts create mode 100644 packages/amplify-graphql-transformer-core/src/transformation/transformer-config.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 f2878dbb503..2ba8e7edd61 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'; const serviceName = 'AppSync'; const elasticContainerServiceName = 'ElasticContainer'; @@ -508,9 +509,11 @@ export async function askAdditionalAuthQuestions(context, authConfig, defaultAut if (await context.prompt.confirm('Configure additional auth types?')) { // Get additional auth configured const remainingAuthProviderChoices = authProviderChoices.filter(p => p.value !== defaultAuthType); - const currentAdditionalAuth = ((currentAuthConfig && currentAuthConfig.additionalAuthenticationProviders - ? currentAuthConfig.additionalAuthenticationProviders - : []) as any[]).map(authProvider => authProvider.authenticationType); + const currentAdditionalAuth = ( + (currentAuthConfig && currentAuthConfig.additionalAuthenticationProviders + ? currentAuthConfig.additionalAuthenticationProviders + : []) as any[] + ).map(authProvider => authProvider.authenticationType); const additionalProvidersQuestion: CheckboxQuestion = { type: 'checkbox', @@ -619,6 +622,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', @@ -677,9 +682,10 @@ function validateDays(input) { } function validateIssuerUrl(input) { - const isValid = /^(((?!http:\/\/(?!localhost))([a-zA-Z0-9.]{1,}):\/\/([a-zA-Z0-9-._~:?#@!$&'()*+,;=/]{1,})\/)|(?!http)(?!https)([a-zA-Z0-9.]{1,}):\/\/)$/.test( - input, - ); + const isValid = + /^(((?!http:\/\/(?!localhost))([a-zA-Z0-9.]{1,}):\/\/([a-zA-Z0-9-._~:?#@!$&'()*+,;=/]{1,})\/)|(?!http)(?!https)([a-zA-Z0-9.]{1,}):\/\/)$/.test( + input, + ); if (!isValid) { return 'The value must be a valid URI with a trailing forward slash. HTTPS must be used instead of HTTP unless you are using localhost.'; @@ -779,8 +785,8 @@ const buildPolicyResource = (resourceName: string, path: string | null) => { { Ref: `${category}${resourceName}GraphQLAPIIdOutput`, }, - ...(path ? [path] : []) - ] + ...(path ? [path] : []), + ], ], }; }; 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__/extensions/amplify-helpers/api-key.test.ts b/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/api-key.test.ts new file mode 100644 index 00000000000..afb2d5deb61 --- /dev/null +++ b/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/api-key.test.ts @@ -0,0 +1,78 @@ +import fs from 'fs'; +import { getAppSyncApiConfig, getApiKeyConfig, apiKeyIsActive, hasApiKey } from '../../../extensions/amplify-helpers/api-key'; +import { stateManager } from 'amplify-cli-core'; + +jest.mock('amplify-cli-core', () => { + const original = jest.requireActual('amplify-cli-core'); + const amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta.json`); + return { + ...original, + stateManager: { + metaFileExists: jest.fn(), + getMeta: jest.fn().mockImplementation(() => JSON.parse(amplifyMeta.toString())), + }, + }; +}); + +const stateManager_mock = stateManager as jest.Mocked; + +describe('getAppSyncApiConfig', () => { + 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', () => { + it('returns the api key config', () => { + const result = getApiKeyConfig(); + + expect(result).toStrictEqual({ + apiKeyExpirationDays: 2, + apiKeyExpirationDate: '2021-08-20T20:38:07.585Z', + description: '', + }); + }); +}); + +describe('apiKeyIsActive', () => { + it('returns true if api key is active', () => { + const result = apiKeyIsActive(); + + expect(result).toBe(false); + }); +}); + +describe('hasApiKey', () => { + it('returns true if api key is present', () => { + const result = hasApiKey(); + + expect(result).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..ef1212c7771 --- /dev/null +++ b/packages/amplify-cli/src/__tests__/extensions/amplify-helpers/show-global-sandbox-mode-warning.test.ts @@ -0,0 +1,67 @@ +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', () => { + it('returns true', async () => { + amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta.json`); + expect(globalSandboxModeEnabled(ctx)).toBe(true); + }); + }); + + describe('not specified', () => { + it('returns false', async () => { + amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta-2.json`); + expect(globalSandboxModeEnabled(ctx)).toBe(false); + }); + }); + }); + + describe('showGlobalSandboxModeWarning', () => { + it('prints warning message', async () => { + amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta.json`); + + 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 +`); + }); + }); +}); 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.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/api-key.ts b/packages/amplify-cli/src/extensions/amplify-helpers/api-key.ts new file mode 100644 index 00000000000..b3f9a1d770e --- /dev/null +++ b/packages/amplify-cli/src/extensions/amplify-helpers/api-key.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/push-resources.ts b/packages/amplify-cli/src/extensions/amplify-helpers/push-resources.ts index 4e71549cc77..7dd0a64455b 100644 --- a/packages/amplify-cli/src/extensions/amplify-helpers/push-resources.ts +++ b/packages/amplify-cli/src/extensions/amplify-helpers/push-resources.ts @@ -4,6 +4,8 @@ import { onCategoryOutputsChange } from './on-category-outputs-change'; import { initializeEnv } from '../../initialize-env'; import { getProviderPlugins } from './get-provider-plugins'; import { getEnvInfo } from './get-env-info'; +import { globalSandboxModeEnabled, showGlobalSandboxModeWarning } from './show-global-sandbox-mode-warning'; +import { apiKeyIsActive, hasApiKey } from './api-key'; import { EnvironmentDoesNotExistError, exitOnNextTick, stateManager, $TSAny, $TSContext } from 'amplify-cli-core'; export async function pushResources( 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..5456bd062bd --- /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 './api-key'; + +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 925e3a14ee1..e1f9d0ac3f5 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-graphql-transformer-core/src/transformation/transformer-config.ts b/packages/amplify-graphql-transformer-core/src/transformation/transformer-config.ts new file mode 100644 index 00000000000..3be94d1faa8 --- /dev/null +++ b/packages/amplify-graphql-transformer-core/src/transformation/transformer-config.ts @@ -0,0 +1,111 @@ +export interface TransformMigrationConfig { + V1?: { + Resources: string[]; + }; +} + +// Auth Config +export type AppSyncAuthMode = 'API_KEY' | 'AMAZON_COGNITO_USER_POOLS' | 'AWS_IAM' | 'OPENID_CONNECT'; +export type AppSyncAuthConfiguration = { + defaultAuthentication: AppSyncAuthConfigurationEntry; + additionalAuthenticationProviders: Array; +}; + +export type AppSyncAuthConfigurationEntry = + | AppSyncAuthConfigurationUserPoolEntry + | AppSyncAuthConfigurationAPIKeyEntry + | AppSyncAuthConfigurationIAMEntry + | AppSyncAuthConfigurationOIDCEntry; +export type AppSyncAuthConfigurationAPIKeyEntry = { + authenticationType: 'API_KEY'; + apiKeyConfig: ApiKeyConfig; +}; +export type AppSyncAuthConfigurationUserPoolEntry = { + authenticationType: 'AMAZON_COGNITO_USER_POOLS'; + userPoolConfig: UserPoolConfig; +}; +export type AppSyncAuthConfigurationIAMEntry = { + authenticationType: 'AWS_IAM'; +}; + +export type AppSyncAuthConfigurationOIDCEntry = { + authenticationType: 'OPENID_CONNECT'; + openIDConnectConfig: OpenIDConnectConfig; +}; + +export type ApiKeyConfig = { + description?: string; + apiKeyExpirationDays: number; + apiKeyExpirationDate?: Date; +}; +export type UserPoolConfig = { + userPoolId: string; +}; +export type OpenIDConnectConfig = { + name: string; + issuerUrl: string; + clientId?: string; + iatTTL?: number; + authTTL?: number; +}; + +// Sync Config +export const enum ConflictHandlerType { + OPTIMISTIC = 'OPTIMISTIC_CONCURRENCY', + AUTOMERGE = 'AUTOMERGE', + LAMBDA = 'LAMBDA', +} + +export type ConflictDetectionType = 'VERSION' | 'NONE'; +export type SyncConfigOPTIMISTIC = { + ConflictDetection: ConflictDetectionType; + ConflictHandler: ConflictHandlerType.OPTIMISTIC; +}; +export type SyncConfigSERVER = { + ConflictDetection: ConflictDetectionType; + ConflictHandler: ConflictHandlerType.AUTOMERGE; +}; +export type SyncConfigLAMBDA = { + ConflictDetection: ConflictDetectionType; + ConflictHandler: ConflictHandlerType.LAMBDA; + LambdaConflictHandler: { + name: string; + region?: string; + lambdaArn?: any; + }; +}; +export type SyncConfig = SyncConfigOPTIMISTIC | SyncConfigSERVER | SyncConfigLAMBDA; + +export type ResolverConfig = { + project?: SyncConfig; + models?: { + [key: string]: SyncConfig; + }; +}; +export interface TransformConfig { + /** + * The transform library uses a "StackMapping" to determine which stack + * a particular resource belongs to. This "StackMapping" allows individual + * transformer implementations to add resources to a single context and + * reference resources as if they were all members of the same stack. The + * transform formatter takes the single context and the stack mapping + * and splits the context into a valid nested stack where any Fn::Ref or Fn::GetAtt + * is replaced by a Import/Export or Parameter. Users may provide mapping + * overrides to get specific behavior out of the transformer. Users may + * override the default stack mapping to customize behavior. + */ + StackMapping?: { + [resourceId: string]: string; + }; + + /** + * Provide build time options to GraphQL Transformer constructor functions. + * Certain options cannot be configured via CloudFormation parameters and + * need to be set at build time. E.G. DeletionPolicies cannot depend on parameters. + */ + TransformerOptions?: { + [transformer: string]: { + [option: string]: any; + }; + }; +} 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.