Skip to content

Commit 3dbb3bf

Browse files
authored
feat: prep work for Cognito SMS Sandbox #2 (#7338)
* 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
1 parent 9caad85 commit 3dbb3bf

6 files changed

Lines changed: 276 additions & 39 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { printSMSSandboxWarning } from '../../../../provider-utils/awscloudformation/utils/message-printer';
2+
import { BannerMessage } from 'amplify-cli-core';
3+
jest.mock('amplify-cli-core');
4+
const printMock = {
5+
info: jest.fn(),
6+
};
7+
8+
describe('printSMSSandboxWarning', () => {
9+
const mockedGetMessage = jest.spyOn(BannerMessage, 'getMessage');
10+
11+
beforeEach(() => {
12+
jest.resetAllMocks();
13+
});
14+
15+
it('should print warning when the message is present', async () => {
16+
const message = 'BannerMessage';
17+
mockedGetMessage.mockResolvedValueOnce(message);
18+
await printSMSSandboxWarning(printMock);
19+
expect(printMock.info).toHaveBeenCalledWith(`${message}\n`);
20+
expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_CATEGORY_AUTH_ADD_OR_UPDATE_INFO');
21+
});
22+
23+
it('should not print warning when the banner message is missing', async () => {
24+
mockedGetMessage.mockResolvedValueOnce(undefined);
25+
await printSMSSandboxWarning(printMock);
26+
expect(printMock.info).not.toHaveBeenCalled();
27+
expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_CATEGORY_AUTH_ADD_OR_UPDATE_INFO');
28+
});
29+
});

packages/amplify-category-auth/src/provider-utils/awscloudformation/handlers/resource-handlers.ts

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { ServiceQuestionsResult } from '../service-walkthrough-types';
22
import { getAddAuthDefaultsApplier, getUpdateAuthDefaultsApplier } from '../utils/auth-defaults-appliers';
33
import { getResourceSynthesizer, getResourceUpdater } from '../utils/synthesize-resources';
44
import { getPostAddAuthMetaUpdater, getPostUpdateAuthMetaUpdater } from '../utils/amplify-meta-updaters';
5-
import { getPostAddAuthMessagePrinter, getPostUpdateAuthMessagePrinter } from '../utils/message-printer';
5+
import { getPostAddAuthMessagePrinter, getPostUpdateAuthMessagePrinter, printSMSSandboxWarning } from '../utils/message-printer';
66
import { supportedServices } from '../../supported-services';
7+
import { doesConfigurationIncludeSMS } from '../utils/auth-sms-workflow-helper';
78

89
/**
910
* Factory function that returns a ServiceQuestionsResult consumer that handles all of the resource generation logic.
@@ -13,43 +14,48 @@ import { supportedServices } from '../../supported-services';
1314
export const getAddAuthHandler = (context: any) => async (request: ServiceQuestionsResult) => {
1415
const serviceMetadata = supportedServices[request.serviceName];
1516
const { cfnFilename, defaultValuesFilename, provider } = serviceMetadata;
17+
1618
let projectName = context.amplify.getProjectConfig().projectName.toLowerCase();
1719
const disallowedChars = /[^A-Za-z0-9]+/g;
1820
projectName = projectName.replace(disallowedChars, '');
21+
1922
const requestWithDefaults = await getAddAuthDefaultsApplier(context, defaultValuesFilename, projectName)(request);
20-
await getResourceSynthesizer(
21-
context,
22-
cfnFilename,
23-
provider,
24-
)(requestWithDefaults)
25-
.then(req => req.resourceName!)
26-
.then(getPostAddAuthMetaUpdater(context, { service: requestWithDefaults.serviceName, providerName: provider }))
27-
.then(getPostAddAuthMessagePrinter(context.print))
28-
.catch(err => {
29-
context.print.info(err.stack);
30-
context.print.error('There was an error adding the auth resource');
31-
context.usageData.emitError(err);
32-
process.exitCode = 1;
33-
});
23+
24+
try {
25+
await getResourceSynthesizer(context, cfnFilename, provider)(requestWithDefaults);
26+
await getPostAddAuthMetaUpdater(context, { service: requestWithDefaults.serviceName, providerName: provider })(
27+
requestWithDefaults.resourceName!,
28+
);
29+
await getPostAddAuthMessagePrinter(context.print)(requestWithDefaults.resourceName!);
30+
31+
if (doesConfigurationIncludeSMS(request)) {
32+
await printSMSSandboxWarning(context.print);
33+
}
34+
} catch (err) {
35+
context.print.info(err.stack);
36+
context.print.error('There was an error adding the auth resource');
37+
context.usageData.emitError(err);
38+
process.exitCode = 1;
39+
}
3440
return requestWithDefaults.resourceName!;
3541
};
3642

3743
export const getUpdateAuthHandler = (context: any) => async (request: ServiceQuestionsResult) => {
3844
const { cfnFilename, defaultValuesFilename, provider } = supportedServices[request.serviceName];
3945
const requestWithDefaults = await getUpdateAuthDefaultsApplier(context, defaultValuesFilename, context.updatingAuth)(request);
40-
await getResourceUpdater(
41-
context,
42-
cfnFilename,
43-
provider,
44-
)(requestWithDefaults)
45-
.then(req => req.resourceName!)
46-
.then(getPostUpdateAuthMetaUpdater(context))
47-
.then(getPostUpdateAuthMessagePrinter(context.print))
48-
.catch(err => {
49-
context.print.info(err.stack);
50-
context.print.error('There was an error updating the auth resource');
51-
context.usageData.emitError(err);
52-
process.exitCode = 1;
53-
});
46+
try {
47+
await getResourceUpdater(context, cfnFilename, provider)(requestWithDefaults);
48+
await getPostUpdateAuthMetaUpdater(context)(requestWithDefaults.resourceName!);
49+
await getPostUpdateAuthMessagePrinter(context.print)(requestWithDefaults.resourceName!);
50+
51+
if (doesConfigurationIncludeSMS(requestWithDefaults)) {
52+
await printSMSSandboxWarning(context.print);
53+
}
54+
} catch (err) {
55+
context.print.info(err.stack);
56+
context.print.error('There was an error updating the auth resource');
57+
context.usageData.emitError(err);
58+
process.exitCode = 1;
59+
}
5460
return requestWithDefaults.resourceName!;
5561
};

packages/amplify-category-auth/src/provider-utils/awscloudformation/utils/message-printer.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { ServiceQuestionsResult } from '../service-walkthrough-types';
2+
import { BannerMessage } from 'amplify-cli-core';
13
/**
24
* A factory function that returns a function that prints the "success message" after adding auth
35
* @param print The amplify print object
@@ -25,3 +27,20 @@ const printCommonText = (print: any) => {
2527
);
2628
print.info('');
2729
};
30+
31+
export const printSMSSandboxWarning = async (print: any) => {
32+
const postAddUpdateSMSSandboxInfo = await BannerMessage.getMessage('COGNITO_SMS_SANDBOX_CATEGORY_AUTH_ADD_OR_UPDATE_INFO');
33+
postAddUpdateSMSSandboxInfo && print.info(`${postAddUpdateSMSSandboxInfo}\n`);
34+
};
35+
36+
export const doesConfigurationIncludeSMS = (request: ServiceQuestionsResult): boolean => {
37+
if ((request.mfaConfiguration === 'OPTIONAL' || request.mfaConfiguration === 'ON') && request.mfaTypes?.includes('SMS Text Message')) {
38+
return true;
39+
}
40+
41+
if (request.usernameAttributes?.includes('phone_number')) {
42+
return true;
43+
}
44+
45+
return false;
46+
};
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { showSMSSandboxWarning } from '../display-helpful-urls';
2+
import { BannerMessage } from 'amplify-cli-core';
3+
import { SNS } from '../aws-utils/aws-sns';
4+
import { AWSError } from 'aws-sdk';
5+
6+
jest.mock('../aws-utils/aws-sns');
7+
jest.mock('amplify-cli-core');
8+
9+
describe('showSMSSandBoxWarning', () => {
10+
const mockedGetMessage = jest.spyOn(BannerMessage, 'getMessage');
11+
const mockedSNSClientInstance = {
12+
isInSandboxMode: jest.fn(),
13+
};
14+
15+
let mockedSNSClass;
16+
const context = {
17+
print: {
18+
warning: jest.fn(),
19+
},
20+
};
21+
22+
beforeEach(() => {
23+
jest.resetAllMocks();
24+
mockedSNSClass = jest.spyOn(SNS, 'getInstance').mockResolvedValue((mockedSNSClientInstance as unknown) as SNS);
25+
});
26+
27+
describe('when API is missing in SDK', () => {
28+
beforeEach(() => {
29+
mockedSNSClientInstance.isInSandboxMode.mockRejectedValue(new TypeError());
30+
});
31+
32+
it('should not show warning when SNS client is missing sandbox api and there is no banner message associated', async () => {
33+
await showSMSSandboxWarning(context);
34+
35+
expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_UPDATE_WARNING');
36+
expect(context.print.warning).not.toHaveBeenCalled();
37+
});
38+
39+
it('should show warning when SNS Client is missing sandbox API and there is a banner message associated', async () => {
40+
const message = 'UPGRADE YOUR CLI!!!!';
41+
mockedGetMessage.mockImplementation(async messageId => (messageId === 'COGNITO_SMS_SANDBOX_UPDATE_WARNING' ? message : undefined));
42+
43+
await showSMSSandboxWarning(context);
44+
45+
expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_UPDATE_WARNING');
46+
expect(context.print.warning).toHaveBeenCalledWith(message);
47+
});
48+
});
49+
50+
describe('when IAM user is missing sandbox permission', () => {
51+
beforeEach(() => {
52+
const authError = new Error() as AWSError;
53+
authError.code = 'AuthorizationError';
54+
mockedSNSClientInstance.isInSandboxMode.mockRejectedValue(authError);
55+
});
56+
it('should not show any warning if there is no message associated', async () => {
57+
await showSMSSandboxWarning(context);
58+
59+
expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_MISSING_PERMISSION');
60+
expect(context.print.warning).not.toHaveBeenCalled();
61+
});
62+
63+
it('should show any warning if there is no message associated', async () => {
64+
const message = 'UPDATE YOUR PROFILE USER WITH SANDBOX PERMISSION';
65+
66+
mockedGetMessage.mockImplementation(async messageId => {
67+
switch (messageId) {
68+
case 'COGNITO_SMS_SANDBOX_MISSING_PERMISSION':
69+
return message;
70+
case 'COGNITO_SMS_SANDBOX_UPDATE_WARNING':
71+
return 'enabled';
72+
}
73+
});
74+
75+
await showSMSSandboxWarning(context);
76+
77+
expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_MISSING_PERMISSION');
78+
expect(context.print.warning).toHaveBeenCalledWith(message);
79+
});
80+
});
81+
82+
describe('it should not show any warning message when the SNS API is not deployed', () => {
83+
beforeEach(() => {
84+
const resourceNotFoundError = new Error() as AWSError;
85+
resourceNotFoundError.code = 'ResourceNotFound';
86+
mockedSNSClientInstance.isInSandboxMode.mockRejectedValue(resourceNotFoundError);
87+
});
88+
it('should not print error', async () => {
89+
const message = 'UPGRADE YOUR CLI!!!!';
90+
mockedGetMessage.mockImplementation(async messageId => (messageId === 'COGNITO_SMS_SANDBOX_UPDATE_WARNING' ? message : undefined));
91+
92+
await showSMSSandboxWarning(context);
93+
94+
expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_UPDATE_WARNING');
95+
expect(context.print.warning).not.toHaveBeenCalledWith(message);
96+
});
97+
});
98+
99+
describe('it should not show any warning message when there is a network error', () => {
100+
beforeEach(() => {
101+
const networkError = new Error() as AWSError;
102+
networkError.code = 'UnknownEndpoint';
103+
mockedSNSClientInstance.isInSandboxMode.mockRejectedValue(networkError);
104+
});
105+
106+
it('should not print error', async () => {
107+
const message = 'UPGRADE YOUR CLI!!!!';
108+
mockedGetMessage.mockImplementation(async messageId => (messageId === 'COGNITO_SMS_SANDBOX_UPDATE_WARNING' ? message : undefined));
109+
110+
await showSMSSandboxWarning(context);
111+
112+
expect(mockedGetMessage).toHaveBeenCalledWith('COGNITO_SMS_SANDBOX_UPDATE_WARNING');
113+
expect(context.print.warning).not.toHaveBeenCalledWith(message);
114+
});
115+
});
116+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { $TSAny, $TSContext } from 'amplify-cli-core';
2+
import { loadConfiguration } from '../configuration-manager';
3+
import aws from './aws.js';
4+
5+
export class SNS {
6+
private static instance: SNS;
7+
private readonly sns: AWS.SNS;
8+
9+
static async getInstance(context: $TSContext, options = {}): Promise<SNS> {
10+
if (!SNS.instance) {
11+
let cred = {};
12+
try {
13+
cred = await loadConfiguration(context);
14+
} catch (e) {
15+
// ignore missing config
16+
}
17+
18+
SNS.instance = new SNS(context, cred, options);
19+
}
20+
return SNS.instance;
21+
}
22+
23+
private constructor(context: $TSContext, cred: $TSAny, options = {}) {
24+
this.sns = new aws.SNS({ ...cred, ...options });
25+
}
26+
27+
public async isInSandboxMode(): Promise<boolean> {
28+
// AWS SDK still does not have getSMSSandboxAccountStatus. Casting sns to any to avoid compile error
29+
// Todo: remove any casting once aws-sdk is updated
30+
const snsClient = this.sns as any;
31+
const result = await snsClient.getSMSSandboxAccountStatus().promise();
32+
return result.IsInSandbox;
33+
}
34+
}

packages/amplify-provider-awscloudformation/src/display-helpful-urls.js

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const chalk = require('chalk');
33
const { BannerMessage } = require('amplify-cli-core');
44
const { fileLogger } = require('./utils/aws-logger');
5+
const { SNS } = require('./aws-utils/aws-sns');
56

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

@@ -171,11 +172,6 @@ function showHostedUIURLs(context, resourcesToBeCreated) {
171172
}
172173

173174
async function showCognitoSandBoxMessage(context, resources) {
174-
const smsSandBoxMessage = await BannerMessage.getMessage('COGNITO_SMS_SANDBOX_UPDATE_WARNING');
175-
if (!smsSandBoxMessage) {
176-
return;
177-
}
178-
179175
const cognitoResource = resources.filter(resource => resource.service === 'Cognito');
180176

181177
if (cognitoResource.length > 0) {
@@ -187,13 +183,11 @@ async function showCognitoSandBoxMessage(context, resources) {
187183
cognitoResource[0].resourceName,
188184
]);
189185
if (smsWorkflowEnabled) {
190-
context.print.warning(smsSandBoxMessage);
191-
return;
186+
await showSMSSandboxWarning(context);
192187
}
193188
} catch (e) {
194-
if (e.name !== 'MethodNotFound') {
195-
log(e);
196-
}
189+
log(e);
190+
throw e;
197191
}
198192
}
199193
}
@@ -221,6 +215,45 @@ async function showRekognitionURLS(context, resourcesToBeCreated) {
221215
}
222216
}
223217

218+
async function showSMSSandboxWarning(context) {
219+
const log = logger('showSMSSandBoxWarning', []);
220+
221+
// This message will be set only after SNS Sandbox Sandbox API is available and AWS SDK gets updated
222+
const cliUpdateWarning = await BannerMessage.getMessage('COGNITO_SMS_SANDBOX_UPDATE_WARNING');
223+
const smsSandBoxMissingPermissionWarning = await BannerMessage.getMessage('COGNITO_SMS_SANDBOX_MISSING_PERMISSION');
224+
const sandboxModeWarning = await BannerMessage.getMessage('COGNITO_SMS_SANDBOX_SANDBOXED_MODE_WARNING');
225+
const productionModeInfo = await BannerMessage.getMessage('COGNITO_SMS_SANDBOX_PRODUCTION_MODE_INFO');
226+
if (!cliUpdateWarning) {
227+
return;
228+
}
229+
230+
try {
231+
const snsClient = await SNS.getInstance(context);
232+
const sandboxStatus = await snsClient.isInSandboxMode();
233+
234+
if (sandboxStatus) {
235+
sandboxModeWarning && context.print.warning(sandboxModeWarning);
236+
} else {
237+
productionModeInfo && context.print.warning(productionModeInfo);
238+
}
239+
} catch (e) {
240+
if (e.code === 'AuthorizationError') {
241+
smsSandBoxMissingPermissionWarning && context.print.warning(smsSandBoxMissingPermissionWarning);
242+
} else if (e instanceof TypeError) {
243+
context.print.warning(cliUpdateWarning);
244+
} else if (e.code === 'ResourceNotFound') {
245+
// API is not public yet. Ignore it for now. This error should not occur as `COGNITO_SMS_SANDBOX_UPDATE_WARNING` will not be set
246+
} else if (e.code === 'UnknownEndpoint') {
247+
// Network error. Sandbox status is for informational purpose and should not stop deployment
248+
log(e);
249+
} else {
250+
log(e);
251+
throw e;
252+
}
253+
}
254+
}
255+
224256
module.exports = {
225257
displayHelpfulURLs,
258+
showSMSSandboxWarning,
226259
};

0 commit comments

Comments
 (0)