Skip to content

Commit

Permalink
feat: prep work for Cognito SMS Sandbox #2 (aws-amplify#7338)
Browse files Browse the repository at this point in the history
* feat: display SNS sandbox status

* chore: SNS API error handling

* chore: add support for SMS Sandbox check

* chore: auth add show sandbox warning

* chore: add some tests

* chore: address review comments and remove unused code
  • Loading branch information
yuth authored May 20, 2021
1 parent 9caad85 commit 3dbb3bf
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { printSMSSandboxWarning } from '../../../../provider-utils/awscloudformation/utils/message-printer';
import { BannerMessage } from 'amplify-cli-core';
jest.mock('amplify-cli-core');
const printMock = {
info: jest.fn(),
};

describe('printSMSSandboxWarning', () => {
const mockedGetMessage = jest.spyOn(BannerMessage, 'getMessage');

beforeEach(() => {
jest.resetAllMocks();
});

it('should print warning when the message is present', async () => {
const message = 'BannerMessage';
mockedGetMessage.mockResolvedValueOnce(message);
await printSMSSandboxWarning(printMock);
expect(printMock.info).toHaveBeenCalledWith(`${message}\n`);
expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_CATEGORY_AUTH_ADD_OR_UPDATE_INFO');
});

it('should not print warning when the banner message is missing', async () => {
mockedGetMessage.mockResolvedValueOnce(undefined);
await printSMSSandboxWarning(printMock);
expect(printMock.info).not.toHaveBeenCalled();
expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_CATEGORY_AUTH_ADD_OR_UPDATE_INFO');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { ServiceQuestionsResult } from '../service-walkthrough-types';
import { getAddAuthDefaultsApplier, getUpdateAuthDefaultsApplier } from '../utils/auth-defaults-appliers';
import { getResourceSynthesizer, getResourceUpdater } from '../utils/synthesize-resources';
import { getPostAddAuthMetaUpdater, getPostUpdateAuthMetaUpdater } from '../utils/amplify-meta-updaters';
import { getPostAddAuthMessagePrinter, getPostUpdateAuthMessagePrinter } from '../utils/message-printer';
import { getPostAddAuthMessagePrinter, getPostUpdateAuthMessagePrinter, printSMSSandboxWarning } from '../utils/message-printer';
import { supportedServices } from '../../supported-services';
import { doesConfigurationIncludeSMS } from '../utils/auth-sms-workflow-helper';

/**
* Factory function that returns a ServiceQuestionsResult consumer that handles all of the resource generation logic.
Expand All @@ -13,43 +14,48 @@ import { supportedServices } from '../../supported-services';
export const getAddAuthHandler = (context: any) => async (request: ServiceQuestionsResult) => {
const serviceMetadata = supportedServices[request.serviceName];
const { cfnFilename, defaultValuesFilename, provider } = serviceMetadata;

let projectName = context.amplify.getProjectConfig().projectName.toLowerCase();
const disallowedChars = /[^A-Za-z0-9]+/g;
projectName = projectName.replace(disallowedChars, '');

const requestWithDefaults = await getAddAuthDefaultsApplier(context, defaultValuesFilename, projectName)(request);
await getResourceSynthesizer(
context,
cfnFilename,
provider,
)(requestWithDefaults)
.then(req => req.resourceName!)
.then(getPostAddAuthMetaUpdater(context, { service: requestWithDefaults.serviceName, providerName: provider }))
.then(getPostAddAuthMessagePrinter(context.print))
.catch(err => {
context.print.info(err.stack);
context.print.error('There was an error adding the auth resource');
context.usageData.emitError(err);
process.exitCode = 1;
});

try {
await getResourceSynthesizer(context, cfnFilename, provider)(requestWithDefaults);
await getPostAddAuthMetaUpdater(context, { service: requestWithDefaults.serviceName, providerName: provider })(
requestWithDefaults.resourceName!,
);
await getPostAddAuthMessagePrinter(context.print)(requestWithDefaults.resourceName!);

if (doesConfigurationIncludeSMS(request)) {
await printSMSSandboxWarning(context.print);
}
} catch (err) {
context.print.info(err.stack);
context.print.error('There was an error adding the auth resource');
context.usageData.emitError(err);
process.exitCode = 1;
}
return requestWithDefaults.resourceName!;
};

export const getUpdateAuthHandler = (context: any) => async (request: ServiceQuestionsResult) => {
const { cfnFilename, defaultValuesFilename, provider } = supportedServices[request.serviceName];
const requestWithDefaults = await getUpdateAuthDefaultsApplier(context, defaultValuesFilename, context.updatingAuth)(request);
await getResourceUpdater(
context,
cfnFilename,
provider,
)(requestWithDefaults)
.then(req => req.resourceName!)
.then(getPostUpdateAuthMetaUpdater(context))
.then(getPostUpdateAuthMessagePrinter(context.print))
.catch(err => {
context.print.info(err.stack);
context.print.error('There was an error updating the auth resource');
context.usageData.emitError(err);
process.exitCode = 1;
});
try {
await getResourceUpdater(context, cfnFilename, provider)(requestWithDefaults);
await getPostUpdateAuthMetaUpdater(context)(requestWithDefaults.resourceName!);
await getPostUpdateAuthMessagePrinter(context.print)(requestWithDefaults.resourceName!);

if (doesConfigurationIncludeSMS(requestWithDefaults)) {
await printSMSSandboxWarning(context.print);
}
} catch (err) {
context.print.info(err.stack);
context.print.error('There was an error updating the auth resource');
context.usageData.emitError(err);
process.exitCode = 1;
}
return requestWithDefaults.resourceName!;
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ServiceQuestionsResult } from '../service-walkthrough-types';
import { BannerMessage } from 'amplify-cli-core';
/**
* A factory function that returns a function that prints the "success message" after adding auth
* @param print The amplify print object
Expand Down Expand Up @@ -25,3 +27,20 @@ const printCommonText = (print: any) => {
);
print.info('');
};

export const printSMSSandboxWarning = async (print: any) => {
const postAddUpdateSMSSandboxInfo = await BannerMessage.getMessage('COGNITO_SMS_SANDBOX_CATEGORY_AUTH_ADD_OR_UPDATE_INFO');
postAddUpdateSMSSandboxInfo && print.info(`${postAddUpdateSMSSandboxInfo}\n`);
};

export const doesConfigurationIncludeSMS = (request: ServiceQuestionsResult): boolean => {
if ((request.mfaConfiguration === 'OPTIONAL' || request.mfaConfiguration === 'ON') && request.mfaTypes?.includes('SMS Text Message')) {
return true;
}

if (request.usernameAttributes?.includes('phone_number')) {
return true;
}

return false;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { showSMSSandboxWarning } from '../display-helpful-urls';
import { BannerMessage } from 'amplify-cli-core';
import { SNS } from '../aws-utils/aws-sns';
import { AWSError } from 'aws-sdk';

jest.mock('../aws-utils/aws-sns');
jest.mock('amplify-cli-core');

describe('showSMSSandBoxWarning', () => {
const mockedGetMessage = jest.spyOn(BannerMessage, 'getMessage');
const mockedSNSClientInstance = {
isInSandboxMode: jest.fn(),
};

let mockedSNSClass;
const context = {
print: {
warning: jest.fn(),
},
};

beforeEach(() => {
jest.resetAllMocks();
mockedSNSClass = jest.spyOn(SNS, 'getInstance').mockResolvedValue((mockedSNSClientInstance as unknown) as SNS);
});

describe('when API is missing in SDK', () => {
beforeEach(() => {
mockedSNSClientInstance.isInSandboxMode.mockRejectedValue(new TypeError());
});

it('should not show warning when SNS client is missing sandbox api and there is no banner message associated', async () => {
await showSMSSandboxWarning(context);

expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_UPDATE_WARNING');
expect(context.print.warning).not.toHaveBeenCalled();
});

it('should show warning when SNS Client is missing sandbox API and there is a banner message associated', async () => {
const message = 'UPGRADE YOUR CLI!!!!';
mockedGetMessage.mockImplementation(async messageId => (messageId === 'COGNITO_SMS_SANDBOX_UPDATE_WARNING' ? message : undefined));

await showSMSSandboxWarning(context);

expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_UPDATE_WARNING');
expect(context.print.warning).toHaveBeenCalledWith(message);
});
});

describe('when IAM user is missing sandbox permission', () => {
beforeEach(() => {
const authError = new Error() as AWSError;
authError.code = 'AuthorizationError';
mockedSNSClientInstance.isInSandboxMode.mockRejectedValue(authError);
});
it('should not show any warning if there is no message associated', async () => {
await showSMSSandboxWarning(context);

expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_MISSING_PERMISSION');
expect(context.print.warning).not.toHaveBeenCalled();
});

it('should show any warning if there is no message associated', async () => {
const message = 'UPDATE YOUR PROFILE USER WITH SANDBOX PERMISSION';

mockedGetMessage.mockImplementation(async messageId => {
switch (messageId) {
case 'COGNITO_SMS_SANDBOX_MISSING_PERMISSION':
return message;
case 'COGNITO_SMS_SANDBOX_UPDATE_WARNING':
return 'enabled';
}
});

await showSMSSandboxWarning(context);

expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_MISSING_PERMISSION');
expect(context.print.warning).toHaveBeenCalledWith(message);
});
});

describe('it should not show any warning message when the SNS API is not deployed', () => {
beforeEach(() => {
const resourceNotFoundError = new Error() as AWSError;
resourceNotFoundError.code = 'ResourceNotFound';
mockedSNSClientInstance.isInSandboxMode.mockRejectedValue(resourceNotFoundError);
});
it('should not print error', async () => {
const message = 'UPGRADE YOUR CLI!!!!';
mockedGetMessage.mockImplementation(async messageId => (messageId === 'COGNITO_SMS_SANDBOX_UPDATE_WARNING' ? message : undefined));

await showSMSSandboxWarning(context);

expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_UPDATE_WARNING');
expect(context.print.warning).not.toHaveBeenCalledWith(message);
});
});

describe('it should not show any warning message when there is a network error', () => {
beforeEach(() => {
const networkError = new Error() as AWSError;
networkError.code = 'UnknownEndpoint';
mockedSNSClientInstance.isInSandboxMode.mockRejectedValue(networkError);
});

it('should not print error', async () => {
const message = 'UPGRADE YOUR CLI!!!!';
mockedGetMessage.mockImplementation(async messageId => (messageId === 'COGNITO_SMS_SANDBOX_UPDATE_WARNING' ? message : undefined));

await showSMSSandboxWarning(context);

expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_UPDATE_WARNING');
expect(context.print.warning).not.toHaveBeenCalledWith(message);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { $TSAny, $TSContext } from 'amplify-cli-core';
import { loadConfiguration } from '../configuration-manager';
import aws from './aws.js';

export class SNS {
private static instance: SNS;
private readonly sns: AWS.SNS;

static async getInstance(context: $TSContext, options = {}): Promise<SNS> {
if (!SNS.instance) {
let cred = {};
try {
cred = await loadConfiguration(context);
} catch (e) {
// ignore missing config
}

SNS.instance = new SNS(context, cred, options);
}
return SNS.instance;
}

private constructor(context: $TSContext, cred: $TSAny, options = {}) {
this.sns = new aws.SNS({ ...cred, ...options });
}

public async isInSandboxMode(): Promise<boolean> {
// AWS SDK still does not have getSMSSandboxAccountStatus. Casting sns to any to avoid compile error
// Todo: remove any casting once aws-sdk is updated
const snsClient = this.sns as any;
const result = await snsClient.getSMSSandboxAccountStatus().promise();
return result.IsInSandbox;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const chalk = require('chalk');
const { BannerMessage } = require('amplify-cli-core');
const { fileLogger } = require('./utils/aws-logger');
const { SNS } = require('./aws-utils/aws-sns');

const logger = fileLogger('display-helpful-urls');

Expand Down Expand Up @@ -171,11 +172,6 @@ function showHostedUIURLs(context, resourcesToBeCreated) {
}

async function showCognitoSandBoxMessage(context, resources) {
const smsSandBoxMessage = await BannerMessage.getMessage('COGNITO_SMS_SANDBOX_UPDATE_WARNING');
if (!smsSandBoxMessage) {
return;
}

const cognitoResource = resources.filter(resource => resource.service === 'Cognito');

if (cognitoResource.length > 0) {
Expand All @@ -187,13 +183,11 @@ async function showCognitoSandBoxMessage(context, resources) {
cognitoResource[0].resourceName,
]);
if (smsWorkflowEnabled) {
context.print.warning(smsSandBoxMessage);
return;
await showSMSSandboxWarning(context);
}
} catch (e) {
if (e.name !== 'MethodNotFound') {
log(e);
}
log(e);
throw e;
}
}
}
Expand Down Expand Up @@ -221,6 +215,45 @@ async function showRekognitionURLS(context, resourcesToBeCreated) {
}
}

async function showSMSSandboxWarning(context) {
const log = logger('showSMSSandBoxWarning', []);

// This message will be set only after SNS Sandbox Sandbox API is available and AWS SDK gets updated
const cliUpdateWarning = await BannerMessage.getMessage('COGNITO_SMS_SANDBOX_UPDATE_WARNING');
const smsSandBoxMissingPermissionWarning = await BannerMessage.getMessage('COGNITO_SMS_SANDBOX_MISSING_PERMISSION');
const sandboxModeWarning = await BannerMessage.getMessage('COGNITO_SMS_SANDBOX_SANDBOXED_MODE_WARNING');
const productionModeInfo = await BannerMessage.getMessage('COGNITO_SMS_SANDBOX_PRODUCTION_MODE_INFO');
if (!cliUpdateWarning) {
return;
}

try {
const snsClient = await SNS.getInstance(context);
const sandboxStatus = await snsClient.isInSandboxMode();

if (sandboxStatus) {
sandboxModeWarning && context.print.warning(sandboxModeWarning);
} else {
productionModeInfo && context.print.warning(productionModeInfo);
}
} catch (e) {
if (e.code === 'AuthorizationError') {
smsSandBoxMissingPermissionWarning && context.print.warning(smsSandBoxMissingPermissionWarning);
} else if (e instanceof TypeError) {
context.print.warning(cliUpdateWarning);
} else if (e.code === 'ResourceNotFound') {
// API is not public yet. Ignore it for now. This error should not occur as `COGNITO_SMS_SANDBOX_UPDATE_WARNING` will not be set
} else if (e.code === 'UnknownEndpoint') {
// Network error. Sandbox status is for informational purpose and should not stop deployment
log(e);
} else {
log(e);
throw e;
}
}
}

module.exports = {
displayHelpfulURLs,
showSMSSandboxWarning,
};

0 comments on commit 3dbb3bf

Please sign in to comment.