Skip to content

Commit

Permalink
feat: prep work for SMS Sandbox support (#7302)
Browse files Browse the repository at this point in the history
* feat: add support for banner message

* chore: add check for SMS sandbox status

* chore: address review comments

* chore: fix test

* chore: fix lgtm error

* chore: change env var name
  • Loading branch information
yuth authored May 13, 2021
1 parent 44d25e2 commit d1f85d2
Show file tree
Hide file tree
Showing 19 changed files with 377 additions and 17 deletions.
2 changes: 1 addition & 1 deletion packages/amplify-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"node-emoji": "^1.10.0",
"ora": "^4.0.3",
"rimraf": "^3.0.0",
"semver": "^7.1.1",
"semver": "^7.3.5",
"strip-ansi": "^6.0.0",
"xcode": "^2.1.0",
"yargs": "^15.1.0"
Expand Down
19 changes: 19 additions & 0 deletions packages/amplify-category-auth/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ const { projectHasAuth } = require('./provider-utils/awscloudformation/utils/pro
const { attachPrevParamsToContext } = require('./provider-utils/awscloudformation/utils/attach-prev-params-to-context');
const { stateManager } = require('amplify-cli-core');

const {
doesConfigurationIncludeSMS,
loadResourceParameters,
loadImportedAuthParameters,
} = require('./provider-utils/awscloudformation/utils/auth-sms-workflow-helper');

// this function is being kept for temporary compatability.
async function add(context) {
const { amplify } = context;
Expand Down Expand Up @@ -424,6 +430,18 @@ async function importAuth(context) {
return providerController.importResource(context, serviceSelection, undefined, undefined, false);
}

async function isSMSWorkflowEnabled(context, resourceName) {
const { imported, userPoolId } = context.amplify.getImportedAuthProperties(context);
let userNameAndMfaConfig;
if (imported) {
userNameAndMfaConfig = await loadImportedAuthParameters(context, userPoolId);
} else {
userNameAndMfaConfig = loadResourceParameters(context, resourceName);
}
const result = doesConfigurationIncludeSMS(userNameAndMfaConfig);
return result;
}

module.exports = {
externalAuthEnable,
checkRequirements,
Expand All @@ -439,4 +457,5 @@ module.exports = {
uploadFiles,
category,
importAuth,
isSMSWorkflowEnabled,
};
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export interface ProviderUtils {
privateParams: $TSObject,
envSpecificParams: string[],
): void;
loadResourceParameters(context: $TSContext, category: string, resourceName: string): Record<string, any>;
}

export type ImportAuthHeadlessParameters = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { $TSContext } from 'amplify-cli-core';
import { ProviderUtils } from '../import/types';

import { ServiceQuestionsResult } from '../service-walkthrough-types';
import { supportedServices } from '../../supported-services';

export type UserPoolMessageConfiguration = {
mfaConfiguration?: string;
mfaTypes?: string[];
usernameAttributes?: string[];
};

export const doesConfigurationIncludeSMS = (request: UserPoolMessageConfiguration): 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;
};

const getProviderPlugin = (context: $TSContext): ProviderUtils => {
const serviceMetaData = supportedServices.Cognito;
const { provider } = serviceMetaData;

return context.amplify.getPluginInstance(context, provider);
};
export const loadResourceParameters = (context: $TSContext, resourceName: string): UserPoolMessageConfiguration => {
const providerPlugin = getProviderPlugin(context);
return providerPlugin.loadResourceParameters(context, 'auth', resourceName) as ServiceQuestionsResult;
};

export const loadImportedAuthParameters = async (context: $TSContext, userPoolName: string): Promise<UserPoolMessageConfiguration> => {
const providerPlugin = getProviderPlugin(context);
const cognitoUserPoolService = await providerPlugin.createCognitoUserPoolService(context);
const userPoolDetails = await cognitoUserPoolService.getUserPoolDetails(userPoolName);
const mfaConfig = await cognitoUserPoolService.getUserPoolMfaConfig(userPoolName);
return {
mfaConfiguration: mfaConfig.MfaConfiguration,
usernameAttributes: userPoolDetails.UsernameAttributes,
mfaTypes: mfaConfig.SmsMfaConfiguration ? ['SMS Text Message'] : [],
};
};
6 changes: 5 additions & 1 deletion packages/amplify-cli-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@
"hjson": "^3.2.1",
"js-yaml": "^4.0.0",
"lodash": "^4.17.19",
"open": "^7.3.1"
"node-fetch": "^2.6.1",
"open": "^7.3.1",
"proxy-agent": "^4.0.1",
"semver": "^7.3.5"
},
"devDependencies": {
"@types/fs-extra": "^8.0.1",
Expand All @@ -44,6 +47,7 @@
"@types/rimraf": "^3.0.0",
"@types/uuid": "^8.0.0",
"amplify-function-plugin-interface": "1.7.2",
"nock": "^13.0.11",
"rimraf": "^3.0.0"
},
"jest": {
Expand Down
117 changes: 117 additions & 0 deletions packages/amplify-cli-core/src/__tests__/banner-message/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import nock from 'nock';
import url from 'url';
import { BannerMessage, AWS_AMPLIFY_DEFAULT_BANNER_URL, Message } from '../../banner-message';

const ONE_DAY = 1000 * 60 * 60 * 24;
let mockServer: nock.Interceptor;
let serverResponse: { version: string; messages: Message[] };
describe('BannerMessage', () => {
beforeEach(async () => {
serverResponse = {
version: '1.0.0',
messages: [
{
message: 'first message',
id: 'first',
conditions: {
enabled: true,
cliVersions: '4.41.0',
startTime: new Date(Date.now() - ONE_DAY).toISOString(),
endTime: new Date(Date.now() + ONE_DAY).toISOString(),
},
},
],
};

const urlInfo = url.parse(AWS_AMPLIFY_DEFAULT_BANNER_URL);
mockServer = nock(`${urlInfo.protocol}//${urlInfo.host}`).get(urlInfo.pathname!);

await BannerMessage.initialize('4.41.0');
});
afterEach(() => {
BannerMessage.releaseInstance();
});

it('should return message by fetching it from remote url', async () => {
mockServer.reply(200, serverResponse, { 'Content-Type': 'application/json' });
const result = await BannerMessage.getMessage('first');
expect(result).toEqual('first message');
});

it('should return message when there are no conditions', async () => {
delete serverResponse.messages[0].conditions;
mockServer.reply(200, serverResponse, { 'Content-Type': 'application/json' });
const result = await BannerMessage.getMessage('first');
expect(result).toEqual('first message');
});

it('should not throw error when server sends 404', async () => {
mockServer.reply(404, 'page not found');
const result = await BannerMessage.getMessage('first');
expect(result).toBeUndefined();
});

it('Should not process the Banner response if the response version is not supported', async () => {
serverResponse.version = '20.2';
mockServer.reply(200, serverResponse, { 'Content-Type': 'application/json' });
const result = await BannerMessage.getMessage('first');
expect(result).toBeUndefined();
});

it('should not return message when the message version does not match', async () => {
serverResponse.messages[0].conditions!.cliVersions! = '110022.0.0';
mockServer.reply(200, serverResponse, { 'Content-Type': 'application/json' });
const result = await BannerMessage.getMessage('first');
expect(result).toBeUndefined();
});

it('should not show message when message is not enabled', async () => {
serverResponse.messages[0].conditions!.enabled = false;
mockServer.reply(200, serverResponse, { 'Content-Type': 'application/json' });
const result = await BannerMessage.getMessage('first');
expect(result).toBeUndefined();
});

it('should show message when conditions.enabled is undefined', async () => {
(serverResponse.messages[0].conditions!.enabled as any) = undefined;
mockServer.reply(200, serverResponse, { 'Content-Type': 'application/json' });
const result = await BannerMessage.getMessage('first');
expect(result).toEqual('first message');
});

it('should not show message when startDate is after current Date', async () => {
serverResponse.messages[0].conditions!.endTime = undefined;
serverResponse.messages[0].conditions!.startTime = new Date(Date.now() + ONE_DAY).toISOString();
mockServer.reply(200, serverResponse, { 'Content-Type': 'application/json' });
const result = await BannerMessage.getMessage('first');
expect(result).toBeUndefined();
});

it('should not show message when endDate is before current Date', async () => {
serverResponse.messages[0].conditions!.startTime = undefined;
serverResponse.messages[0].conditions!.endTime = new Date(Date.now() - ONE_DAY).toISOString();
mockServer.reply(200, serverResponse, { 'Content-Type': 'application/json' });
const result = await BannerMessage.getMessage('first');
expect(result).toBeUndefined();
});

it('should show message when start and endDate are not defined', async () => {
delete serverResponse.messages[0].conditions!.startTime;
delete serverResponse.messages[0]!.conditions!.endTime;
mockServer.reply(200, serverResponse, { 'Content-Type': 'application/json' });
const result = await BannerMessage.getMessage('first');
expect(result).toEqual('first message');
});

it('should show message when cliVersions is undefined', async () => {
delete serverResponse.messages[0].conditions!.cliVersions;
mockServer.reply(200, serverResponse, { 'Content-Type': 'application/json' });
const result = await BannerMessage.getMessage('first');
expect(result).toEqual('first message');
});

it('should throw error when BannerMessage is not initialized', async () => {
BannerMessage.releaseInstance();
await expect(() => BannerMessage.getMessage('first')).rejects.toThrowError('BannerMessage is not initialized');
});
});
96 changes: 96 additions & 0 deletions packages/amplify-cli-core/src/banner-message/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import fetch from 'node-fetch';
import semver from 'semver';
import ProxyAgent from 'proxy-agent';
import { getLogger } from '../logger';

export type Message = {
message: string;
id: string;
conditions?: {
enabled: boolean;
cliVersions?: string;
startTime?: string;
endTime?: string;
};
};

export const AWS_AMPLIFY_DEFAULT_BANNER_URL: string = 'https://aws-amplify.github.io/amplify-cli/banner-message.json';
const MAX_SUPPORTED_MESSAGE_CONFIG_VERSION = '1.0.0';

const logger = getLogger('amplify-cli-core', 'banner-message/index.ts');

export class BannerMessage {
private static instance?: BannerMessage;
private messages: Message[] = [];

public static initialize = (cliVersion: string): BannerMessage => {
if (!BannerMessage.instance) {
BannerMessage.instance = new BannerMessage(cliVersion);
}

return BannerMessage.instance;
};

private static ensureInitialized = () => {
if (!BannerMessage.instance) {
throw new Error('BannerMessage is not initialized');
}
};

private constructor(private cliVersion: string) {}

private fetchMessages = async (url: string): Promise<void> => {
try {
logger.info(`fetch banner messages from ${url}`);
const proxy = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
const fetchOptions = proxy ? { agent: new ProxyAgent(proxy) } : {};
const result = await fetch(url, fetchOptions);
const body = await result.json();
if (!semver.satisfies(body.version, MAX_SUPPORTED_MESSAGE_CONFIG_VERSION)) {
return;
}
this.messages = body.messages ?? [];
} catch (e) {
// network error should not cause CLI execution failure
logger.error('fetch banner message failed', e);
}
};

public static getMessage = async (messageId: string): Promise<string | undefined> => {
BannerMessage.ensureInitialized();
return BannerMessage.instance!.getMessages(messageId);
};

getMessages = async (messageId: string): Promise<string | undefined> => {
if (!this.messages.length) {
await this.fetchMessages(process.env.AMPLIFY_CLI_BANNER_MESSAGE_URL ?? AWS_AMPLIFY_DEFAULT_BANNER_URL);
}

const matchingMessageItems = this.messages.filter(
m =>
m.id === messageId &&
m.conditions?.enabled !== false &&
(m.conditions?.cliVersions ? semver.satisfies(this.cliVersion, m.conditions.cliVersions) : true),
);

const messageItem = matchingMessageItems.find(m => {
if (m.conditions) {
const currentTime = Date.now();
const startTime = m.conditions?.startTime ? Date.parse(m.conditions?.startTime) : currentTime;
const endTime = m.conditions?.endTime ? Date.parse(m.conditions?.endTime) : currentTime;
return currentTime >= startTime && currentTime <= endTime;
}
return true;
});

return messageItem?.message;
};

/**
* @internal
* package private method used in unit tests to release the instance
*/
public static releaseInstance = (): void => {
BannerMessage.instance = undefined;
};
}
3 changes: 2 additions & 1 deletion packages/amplify-cli-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export * from './cliConstants';
export * from './deploymentSecretsHelper';
export * from './deploymentState';
export * from './utils';
export * from './banner-message';

// Temporary types until we can finish full type definition across the whole CLI

Expand Down Expand Up @@ -174,7 +175,7 @@ interface AmplifyToolkit {
getEnvDetails: () => $TSAny;
getEnvInfo: () => $TSAny;
getProviderPlugins: (context: $TSContext) => $TSAny;
getPluginInstance: () => $TSAny;
getPluginInstance: (context: $TSContext, pluginName: string) => $TSAny;
getProjectConfig: () => $TSAny;
getProjectDetails: () => $TSAny;
getProjectMeta: () => $TSMeta;
Expand Down
12 changes: 12 additions & 0 deletions packages/amplify-cli-core/src/logger/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Redactor, logger } from 'amplify-cli-logger';

export const getLogger = (moduleName: string, fileName: string) => {
return {
info: (message: string, args: any = {}) => {
logger.logInfo({ message: `${moduleName}.${fileName}.${message}(${Redactor(JSON.stringify(args))}` });
},
error: (message: string, error: Error) => {
logger.logError({ message: `${moduleName}.${fileName}.${message}`, error });
},
};
};
2 changes: 1 addition & 1 deletion packages/amplify-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
"parse-json": "^5.0.0",
"progress": "^2.0.3",
"promise-sequential": "^1.1.1",
"semver": "^7.1.1",
"semver": "^7.3.5",
"tar-fs": "^2.1.1",
"update-notifier": "^4.1.0",
"which": "^2.0.2",
Expand Down
3 changes: 3 additions & 0 deletions packages/amplify-cli/src/__tests__/test-aborting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ describe('test SIGINT with execute', () => {
FeatureFlags: {
initialize: jest.fn(),
},
BannerMessage: {
initialize: jest.fn(),
},
PathConstants: {
TeamProviderFileName: 'team-provider-info.json',
DeploymentSecretsFileName: 'deployment-secrets.json',
Expand Down
Loading

0 comments on commit d1f85d2

Please sign in to comment.