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

fix: disable searchable nodeToNode encryption unless it is already deployed to mitigate impact from enabling or disabling. #1152

Merged
merged 3 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ const getTransformerFactoryV2 = (
];

if (options?.addSearchableTransformer) {
transformerList.push(new SearchableModelTransformerV2());
const resourceDirParts = resourceDir.split(path.sep);
const apiName = resourceDirParts[resourceDirParts.length - 1];
transformerList.push(new SearchableModelTransformerV2(apiName));
alharris-at marked this conversation as resolved.
Show resolved Hide resolved
}

const customTransformersConfig = await loadProject(resourceDir);
Expand Down
31 changes: 27 additions & 4 deletions packages/amplify-e2e-core/src/categories/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,8 +982,31 @@ export async function validateRestApiMeta(projRoot: string, meta?: any) {
expect(seenAtLeastOneFunc).toBe(true);
}

export function setStackMapping(projRoot: string, projectName: string, stackMapping: Record<string, string>) {
const transformConfig = getTransformConfig(projRoot, projectName);
transformConfig.StackMapping = stackMapping;
setTransformConfig(projRoot, projectName, transformConfig);
export function setStackMapping(projRoot: string, apiName: string, stackMapping: Record<string, string>) {
setTransformConfigValue(projRoot, apiName, 'StackMapping', stackMapping);
}

/**
* Set a specific key in the `transform.conf.json` file.
* @param projRoot root directory for the project
* @param apiName the name of the api to modify
* @param key the key in the transform.conf.json value
* @param value the value to set in the file
*/
export const setTransformConfigValue = (projRoot: string, apiName: string, key: string, value: any): void => {
const transformConfig = getTransformConfig(projRoot, apiName);
transformConfig[key] = value;
setTransformConfig(projRoot, apiName, transformConfig);
};

/**
* Remove a specified key from the `transform.conf.json` file.
* @param projRoot root directory for the project
* @param apiName the name of the api to modify
* @param key the key in the transform.conf.json value
*/
export const removeTransformConfigValue = (projRoot: string, apiName: string, key: string): void => {
const transformConfig = getTransformConfig(projRoot, apiName);
delete transformConfig[key];
setTransformConfig(projRoot, apiName, transformConfig);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import {
initJSProjectWithProfile,
deleteProject,
amplifyPush,
createRandomName,
addAuthWithDefault,
setTransformConfigValue,
} from 'amplify-category-api-e2e-core';
import { addApiWithoutSchema, updateApiSchema, getProjectMeta } from 'amplify-category-api-e2e-core';
import { createNewProjectDir, deleteProjectDir } from 'amplify-category-api-e2e-core';
import gql from 'graphql-tag';
import AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync';
import * as path from 'path';
import * as fs from 'fs-extra';

(global as any).fetch = require('node-fetch');

describe('searchable deployments succeed with various NodeToNodeEncryption flag states', () => {
let projRoot: string;
let projectName: string;
let appSyncClient = undefined;

beforeEach(async () => {
projectName = createRandomName();
projRoot = await createNewProjectDir(createRandomName());
await initJSProjectWithProfile(projRoot, {
name: projectName,
});
await addAuthWithDefault(projRoot, {});
});

afterEach(async () => {
await deleteProject(projRoot);
deleteProjectDir(projRoot);
});

it('succeeds deployment with NodeToNodeEncryption set to false', async () => {
const v2Schema = 'transformer_migration/searchable-v2.graphql';

await addApiWithoutSchema(projRoot, { apiName: projectName });
updateApiSchema(projRoot, projectName, v2Schema);
setTransformConfigValue(projRoot, projectName, 'NodeToNodeEncryption', false);
await amplifyPush(projRoot);

appSyncClient = getAppSyncClientFromProj(projRoot);
await runAndValidateQuery('test1', 'test1', 10);

const searchableStackPath = path.join(projRoot, 'amplify', 'backend', 'api', projectName, 'build', 'stacks', 'SearchableStack.json');
const searchableStack = JSON.parse(fs.readFileSync(searchableStackPath).toString());
const searchDomainProps = searchableStack.Resources.OpenSearchDomain.Properties;

expect(searchDomainProps).not.toHaveProperty('NodeToNodeEncryptionOptions');
});

const getAppSyncClientFromProj = (projRoot: string) => {
const meta = getProjectMeta(projRoot);
const region = meta['providers']['awscloudformation']['Region'] as string;
const { output } = meta.api[projectName];
const url = output.GraphQLAPIEndpointOutput as string;
const apiKey = output.GraphQLAPIKeyOutput as string;

return new AWSAppSyncClient({
url,
region,
disableOffline: true,
auth: {
type: AUTH_TYPE.API_KEY,
apiKey,
},
});
};

const fragments = [`fragment FullTodo on Todo { id name description count }`];

const runMutation = async (query: string) => {
try {
const q = [query, ...fragments].join('\n');
const response = await appSyncClient.mutate({
mutation: gql(q),
fetchPolicy: 'no-cache',
});
return response;
} catch (e) {
console.error(e);
return null;
}
};

const runQuery = async (query: string) => {
try {
const q = [query, ...fragments].join('\n');
const response = await appSyncClient.query({
query: gql(q),
fetchPolicy: 'no-cache',
});
return response;
} catch (e) {
console.error(e);
return null;
}
};

const createEntry = async (name: string, description: string, count: number) => {
return await runMutation(getCreateTodosMutation(name, description, count));
};

const searchTodos = async () => {
return await runQuery(getTodos());
};

function getCreateTodosMutation(name: string, description: string, count: number): string {
return `mutation {
createTodo(input: {
name: "${name}"
description: "${description}"
count: ${count}
}) { ...FullTodo }
}`;
}

function getTodos() {
return `query {
searchTodos {
items {
...FullTodo
}
}
}`;
}

const runAndValidateQuery = async (name: string, description: string, count: number) => {
const response = await createEntry(name, description, count);
expect(response).toBeDefined();
expect(response.errors).toBeUndefined();
expect(response.data).toBeDefined();
expect(response.data.createTodo).toBeDefined();

await waitForOSPropagate();
const searchResponse = await searchTodos();

const expectedRows = 1;
expect(searchResponse).toBeDefined();
expect(searchResponse.errors).toBeUndefined();
expect(searchResponse.data).toBeDefined();
expect(searchResponse.data.searchTodos).toBeDefined();
expect(searchResponse.data.searchTodos.items).toHaveLength(expectedRows);
};

const waitForOSPropagate = async (initialWaitSeconds = 5, maxRetryCount = 5) => {
const expectedCount = 1;
let waitInMilliseconds = initialWaitSeconds * 1000;
let currentRetryCount = 0;
let searchResponse;

do {
await new Promise(r => setTimeout(r, waitInMilliseconds));
searchResponse = await searchTodos();
currentRetryCount += 1;
waitInMilliseconds = waitInMilliseconds * 2;
} while (searchResponse.data.searchTodos?.items?.length < expectedCount && currentRetryCount <= maxRetryCount);
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import {
initJSProjectWithProfile,
deleteProject,
amplifyPush,
createRandomName,
addAuthWithDefault,
setTransformConfigValue,
} from 'amplify-category-api-e2e-core';
import { addApiWithoutSchema, updateApiSchema, getProjectMeta } from 'amplify-category-api-e2e-core';
import { createNewProjectDir, deleteProjectDir } from 'amplify-category-api-e2e-core';
import gql from 'graphql-tag';
import AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync';
import * as path from 'path';
import * as fs from 'fs-extra';

(global as any).fetch = require('node-fetch');

describe('searchable deployments succeed with various NodeToNodeEncryption flag states', () => {
let projRoot: string;
let projectName: string;
let appSyncClient = undefined;

beforeEach(async () => {
projectName = createRandomName();
projRoot = await createNewProjectDir(createRandomName());
await initJSProjectWithProfile(projRoot, {
name: projectName,
});
await addAuthWithDefault(projRoot, {});
});

afterEach(async () => {
await deleteProject(projRoot);
deleteProjectDir(projRoot);
});

it('succeeds deployment with NodeToNodeEncryption set to true', async () => {
const v2Schema = 'transformer_migration/searchable-v2.graphql';

await addApiWithoutSchema(projRoot, { apiName: projectName });
updateApiSchema(projRoot, projectName, v2Schema);
setTransformConfigValue(projRoot, projectName, 'NodeToNodeEncryption', true);
await amplifyPush(projRoot);

appSyncClient = getAppSyncClientFromProj(projRoot);
await runAndValidateQuery('test1', 'test1', 10);

const searchableStackPath = path.join(projRoot, 'amplify', 'backend', 'api', projectName, 'build', 'stacks', 'SearchableStack.json');
const searchableStack = JSON.parse(fs.readFileSync(searchableStackPath).toString());
const searchDomainProps = searchableStack.Resources.OpenSearchDomain.Properties;

expect(searchDomainProps).toHaveProperty('NodeToNodeEncryptionOptions');
expect(searchDomainProps.NodeToNodeEncryptionOptions.Enabled).toEqual(true);
});

const getAppSyncClientFromProj = (projRoot: string) => {
const meta = getProjectMeta(projRoot);
const region = meta['providers']['awscloudformation']['Region'] as string;
const { output } = meta.api[projectName];
const url = output.GraphQLAPIEndpointOutput as string;
const apiKey = output.GraphQLAPIKeyOutput as string;

return new AWSAppSyncClient({
url,
region,
disableOffline: true,
auth: {
type: AUTH_TYPE.API_KEY,
apiKey,
},
});
};

const fragments = [`fragment FullTodo on Todo { id name description count }`];

const runMutation = async (query: string) => {
try {
const q = [query, ...fragments].join('\n');
const response = await appSyncClient.mutate({
mutation: gql(q),
fetchPolicy: 'no-cache',
});
return response;
} catch (e) {
console.error(e);
return null;
}
};

const runQuery = async (query: string) => {
try {
const q = [query, ...fragments].join('\n');
const response = await appSyncClient.query({
query: gql(q),
fetchPolicy: 'no-cache',
});
return response;
} catch (e) {
console.error(e);
return null;
}
};

const createEntry = async (name: string, description: string, count: number) => {
return await runMutation(getCreateTodosMutation(name, description, count));
};

const searchTodos = async () => {
return await runQuery(getTodos());
};

function getCreateTodosMutation(name: string, description: string, count: number): string {
return `mutation {
createTodo(input: {
name: "${name}"
description: "${description}"
count: ${count}
}) { ...FullTodo }
}`;
}

function getTodos() {
return `query {
searchTodos {
items {
...FullTodo
}
}
}`;
}

const runAndValidateQuery = async (name: string, description: string, count: number) => {
const response = await createEntry(name, description, count);
expect(response).toBeDefined();
expect(response.errors).toBeUndefined();
expect(response.data).toBeDefined();
expect(response.data.createTodo).toBeDefined();

await waitForOSPropagate();
const searchResponse = await searchTodos();

const expectedRows = 1;
expect(searchResponse).toBeDefined();
expect(searchResponse.errors).toBeUndefined();
expect(searchResponse.data).toBeDefined();
expect(searchResponse.data.searchTodos).toBeDefined();
expect(searchResponse.data.searchTodos.items).toHaveLength(expectedRows);
};

const waitForOSPropagate = async (initialWaitSeconds = 5, maxRetryCount = 5) => {
const expectedCount = 1;
let waitInMilliseconds = initialWaitSeconds * 1000;
let currentRetryCount = 0;
let searchResponse;

do {
await new Promise(r => setTimeout(r, waitInMilliseconds));
searchResponse = await searchTodos();
currentRetryCount += 1;
waitInMilliseconds = waitInMilliseconds * 2;
} while (searchResponse.data.searchTodos?.items?.length < expectedCount && currentRetryCount <= maxRetryCount);
};
});
Loading