Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add global sandbox mode #8063

Closed
190 changes: 110 additions & 80 deletions .circleci/config.yml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Object {
},
Object {
"apiKeyConfig": Object {
"apiKeyExpirationDate": undefined,
"apiKeyExpirationDays": undefined,
"description": undefined,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,64 @@ describe('update artifacts', () => {
expect(context_stub.print.warning.mock.calls.length).toBe(3);
});
});

describe('update artifacts without graphql compile', () => {
let cfnApiArtifactHandler: ApiArtifactHandler;
let updateRequestStub: UpdateApiRequest;
const updateRequestStubBase: UpdateApiRequest = {
version: 1,
serviceModification: {
serviceName: 'AppSync',
},
};

beforeAll(() => {
getAppSyncResourceName_mock.mockImplementation(() => testApiName);
getAppSyncAuthConfig_mock.mockImplementation(() => ({
defaultAuthentication: {
authenticationType: 'AMAZON_COGNITO_USER_POOLS',
userPoolConfig: {
userPoolId: 'myUserPoolId',
},
},
additionalAuthenticationProviders: [],
}));
});

beforeEach(() => {
jest.clearAllMocks();
updateRequestStub = _.cloneDeep(updateRequestStubBase);
cfnApiArtifactHandler = getCfnApiArtifactHandler(context_stub);
});

it('throws error if no GQL API in project', () => {
getAppSyncResourceName_mock.mockImplementationOnce(() => undefined);
return expect(cfnApiArtifactHandler.updateArtifactsWithoutCompile(updateRequestStub)).rejects.toMatchInlineSnapshot(
`[Error: No AppSync API configured in the project. Use 'amplify add api' to create an API.]`,
);
});

it('writes new schema if specified', async () => {
const newSchemaContents = 'a new schema';
updateRequestStub.serviceModification.transformSchema = newSchemaContents;
await cfnApiArtifactHandler.updateArtifactsWithoutCompile(updateRequestStub);
expect(fs_mock.writeFileSync.mock.calls.length).toBe(1);
expect(fs_mock.writeFileSync.mock.calls[0][1]).toBe(newSchemaContents);
});

it('updates resolver config if not empty', async () => {
updateRequestStub.serviceModification.conflictResolution = {
defaultResolutionStrategy: {
type: 'OPTIMISTIC_CONCURRENCY',
},
};
await cfnApiArtifactHandler.updateArtifactsWithoutCompile(updateRequestStub);
expect(writeTransformerConfiguration_mock.mock.calls.length).toBe(1);
});

it('updates meta files after update', async () => {
await cfnApiArtifactHandler.updateArtifactsWithoutCompile(updateRequestStub);
expect(context_stub.amplify.updateamplifyMetaAfterResourceUpdate.mock.calls.length).toBe(1);
expect(context_stub.amplify.updateBackendConfigAfterResourceUpdate.mock.calls.length).toBe(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import chalk from 'chalk';
import { $TSContext } from 'amplify-cli-core';
import { promptToAddApiKey } from '../../../provider-utils/awscloudformation/prompt-to-add-api-key';
import * as walkthrough from '../../../provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough';
import * as cfnApiArtifactHandler from '../../../provider-utils/awscloudformation/cfn-api-artifact-handler';

jest.mock('../../../provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough', () => ({
askApiKeyQuestions: jest.fn(),
}));

jest.mock('../../../provider-utils/awscloudformation/cfn-api-artifact-handler', () => ({
getCfnApiArtifactHandler: jest.fn(() => {
return { updateArtifactsWithoutCompile: jest.fn() };
}),
}));

describe('prompt to add Api Key', () => {
it('runs through expected user flow: print info, update files', async () => {
const envName = 'envone';
const ctx = {
amplify: {
getEnvInfo() {
return { envName };
},
},
prompt: {
confirm: jest.fn(() => true),
},
} as unknown as $TSContext;

jest.spyOn(ctx.prompt, 'confirm');
jest.spyOn(walkthrough, 'askApiKeyQuestions');
jest.spyOn(cfnApiArtifactHandler, 'getCfnApiArtifactHandler');

await promptToAddApiKey(ctx);

expect(ctx.prompt.confirm).toHaveBeenCalledWith('Would you like to create an API Key?', true);
expect(walkthrough.askApiKeyQuestions).toHaveBeenCalledTimes(1);
expect(cfnApiArtifactHandler.getCfnApiArtifactHandler).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
askAdditionalAuthQuestions,
} from '../../../../provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough';
import { authConfigHasApiKey, getAppSyncAuthConfig } from '../../../../provider-utils/awscloudformation/utils/amplify-meta-utils';
import { FeatureFlags, CLIEnvironmentProvider, FeatureFlagRegistration } from 'amplify-cli-core';
import { FeatureFlags } from 'amplify-cli-core';
jest.mock('../../../../provider-utils/awscloudformation/utils/amplify-meta-utils', () => ({
getAppSyncAuthConfig: jest.fn(),
authConfigHasApiKey: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Object {
exports[`AppSyncAuthType to authConfig maps API_KEY correctly 1`] = `
Object {
"apiKeyConfig": Object {
"apiKeyExpirationDate": undefined,
"apiKeyExpirationDays": 120,
"description": undefined,
},
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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
`);
});
});
1 change: 1 addition & 0 deletions packages/amplify-category-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export {
processDockerConfig,
} from './provider-utils/awscloudformation/utils/containers-artifacts';
export { getContainers } from './provider-utils/awscloudformation/docker-compose';
export { promptToAddApiKey } from './provider-utils/awscloudformation/prompt-to-add-api-key';

const category = 'api';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { AddApiRequest, UpdateApiRequest } from 'amplify-headless-interface';
export interface ApiArtifactHandler {
createArtifacts(request: AddApiRequest): Promise<string>;
updateArtifacts(request: UpdateApiRequest): Promise<void>;
updateArtifactsWithoutCompile(request: UpdateApiRequest): Promise<void>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import _ from 'lodash';
import { getAppSyncResourceName, getAppSyncAuthConfig, checkIfAuthExists, authConfigHasApiKey } from './utils/amplify-meta-utils';
import { printApiKeyWarnings } from './utils/print-api-key-warnings';
import { isNameUnique } from './utils/check-case-sensitivity';
import { FeatureFlags } from 'amplify-cli-core';

// keep in sync with ServiceName in amplify-category-function, but probably it will not change
const FunctionServiceNameLambdaFunction = 'Lambda';
Expand Down Expand Up @@ -90,7 +91,20 @@ class CfnApiArtifactHandler implements ApiArtifactHandler {
authConfig,
});

this.context.amplify.updateamplifyMetaAfterResourceAdd(category, serviceConfig.apiName, this.createAmplifyMeta(authConfig));
const useExperimentalPipelineTransformer = FeatureFlags.getBoolean('graphQLTransformer.useExperimentalPipelinedTransformer');
let globalSandboxModeConfig;

if (useExperimentalPipelineTransformer) {
const envName = this.context.amplify.getEnvInfo().envName;
globalSandboxModeConfig = {};
globalSandboxModeConfig[envName] = { enabled: true };
}

this.context.amplify.updateamplifyMetaAfterResourceAdd(
category,
serviceConfig.apiName,
this.createAmplifyMeta(authConfig, globalSandboxModeConfig),
);
return serviceConfig.apiName;
};

Expand Down Expand Up @@ -129,17 +143,42 @@ class CfnApiArtifactHandler implements ApiArtifactHandler {
printApiKeyWarnings(this.context, oldConfigHadApiKey, authConfigHasApiKey(authConfig));
};

updateArtifactsWithoutCompile = async (request: UpdateApiRequest): Promise<void> => {
const updates = request.serviceModification;
const apiName = getAppSyncResourceName(this.context.amplify.getProjectMeta());
if (!apiName) {
throw new Error(`No AppSync API configured in the project. Use 'amplify add api' to create an API.`);
}
const resourceDir = this.getResourceDir(apiName);
if (updates.transformSchema) {
this.writeSchema(resourceDir, updates.transformSchema);
}
if (updates.conflictResolution) {
updates.conflictResolution = await this.createResolverResources(updates.conflictResolution);
await writeResolverConfig(updates.conflictResolution, resourceDir);
}
const authConfig = getAppSyncAuthConfig(this.context.amplify.getProjectMeta());

if (updates.defaultAuthType) authConfig.defaultAuthentication = appSyncAuthTypeToAuthConfig(updates.defaultAuthType);
if (updates.additionalAuthTypes)
authConfig.additionalAuthenticationProviders = updates.additionalAuthTypes.map(appSyncAuthTypeToAuthConfig);

this.context.amplify.updateamplifyMetaAfterResourceUpdate(category, apiName, 'output', { authConfig });
this.context.amplify.updateBackendConfigAfterResourceUpdate(category, apiName, 'output', { authConfig });
};

private writeSchema = (resourceDir: string, schema: string) => {
fs.writeFileSync(path.join(resourceDir, gqlSchemaFilename), schema);
};

private getResourceDir = (apiName: string) => path.join(this.context.amplify.pathManager.getBackendDirPath(), category, apiName);

private createAmplifyMeta = authConfig => ({
private createAmplifyMeta = (authConfig, globalSandboxModeConfig) => ({
service: 'AppSync',
providerPlugin: provider,
output: {
authConfig,
globalSandboxModeConfig,
},
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { $TSContext } from 'amplify-cli-core';
import { askApiKeyQuestions } from './service-walkthroughs/appSync-walkthrough';
import { authConfigToAppSyncAuthType } from './utils/auth-config-to-app-sync-auth-type-bi-di-mapper';
import { getCfnApiArtifactHandler } from './cfn-api-artifact-handler';

export async function promptToAddApiKey(context: $TSContext): Promise<void> {
if (await context.prompt.confirm('Would you like to create an API Key?', true)) {
const apiKeyConfig = await askApiKeyQuestions();
const authConfig = [apiKeyConfig];

await getCfnApiArtifactHandler(context).updateArtifactsWithoutCompile({
version: 1,
serviceModification: {
serviceName: 'AppSync',
additionalAuthTypes: authConfig.map(authConfigToAppSyncAuthType),
},
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
$TSContext,
open,
} from 'amplify-cli-core';
import { defineGlobalSandboxMode } from '../utils/global-sandbox-mode';
import { Duration, Expiration } from '@aws-cdk/core';

const serviceName = 'AppSync';
const elasticContainerServiceName = 'ElasticContainer';
Expand Down Expand Up @@ -208,6 +210,9 @@ export const serviceWalkthrough = async (context: $TSContext, defaultValuesFilen
schemaContent = fs.readFileSync(schemaFilePath, 'utf8');
askToEdit = false;
} else {
const useExperimentalPipelineTransformer = FeatureFlags.getBoolean('graphQLTransformer.useExperimentalPipelinedTransformer');
schemaContent += useExperimentalPipelineTransformer ? defineGlobalSandboxMode(context) : '';

// Schema template selection
const templateSelectionQuestion = {
type: inputs[4].type,
Expand All @@ -219,7 +224,7 @@ export const serviceWalkthrough = async (context: $TSContext, defaultValuesFilen

const { templateSelection } = await inquirer.prompt(templateSelectionQuestion);
const schemaFilePath = path.join(graphqlSchemaDir, templateSelection);
schemaContent = fs.readFileSync(schemaFilePath, 'utf8');
schemaContent += fs.readFileSync(schemaFilePath, 'utf8');
}

return {
Expand Down Expand Up @@ -600,7 +605,7 @@ async function askUserPoolQuestions(context) {
};
}

async function askApiKeyQuestions() {
export async function askApiKeyQuestions() {
const apiKeyQuestions = [
{
type: 'input',
Expand All @@ -619,6 +624,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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const authConfigToAppSyncAuthTypeMap: Record<string, (authConfig: any) => AppSyn
API_KEY: authConfig => ({
mode: 'API_KEY',
expirationTime: authConfig.apiKeyConfig.apiKeyExpirationDays,
apiKeyExpirationDate: authConfig.apiKeyConfig?.apiKeyExpirationDate,
keyDescription: authConfig.apiKeyConfig.description,
}),
AWS_IAM: () => ({
Expand All @@ -54,6 +55,7 @@ const appSyncAuthTypeToAuthConfigMap: Record<string, (authType: AppSyncAuthType)
authenticationType: 'API_KEY',
apiKeyConfig: {
apiKeyExpirationDays: authType.expirationTime,
apiKeyExpirationDate: authType?.apiKeyExpirationDate,
description: authType.keyDescription,
},
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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
`;
}
5 changes: 2 additions & 3 deletions packages/amplify-cli-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ interface AmplifyToolkit {
) => $TSAny;
sharedQuestions: () => $TSAny;
showAllHelp: () => $TSAny;
showGlobalSandboxModeWarning: (context: $TSContext) => $TSAny;
showHelp: (header: string, commands: { name: string; description: string }[]) => $TSAny;
showHelpfulProviderLinks: (context: $TSContext) => $TSAny;
showResourceTable: () => $TSAny;
Expand Down Expand Up @@ -309,9 +310,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;
Expand Down
Loading