-
Notifications
You must be signed in to change notification settings - Fork 820
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: prep work for SMS Sandbox support (#7302)
* 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
Showing
19 changed files
with
377 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
46 changes: 46 additions & 0 deletions
46
...lify-category-auth/src/provider-utils/awscloudformation/utils/auth-sms-workflow-helper.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'] : [], | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
117 changes: 117 additions & 0 deletions
117
packages/amplify-cli-core/src/__tests__/banner-message/index.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
}, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.