From 4ce413829f14aa90ca9ca27510249f1c6c39909f Mon Sep 17 00:00:00 2001 From: Yathi <511386+yuth@users.noreply.github.com> Date: Wed, 20 Jan 2021 14:19:27 -0800 Subject: [PATCH] feat: add support for importing userpool with no appclient secret (#6404) Update auth import support importing cognito user pool app client (for native) with or without secret. The CLI has been updated (in #6333) to support generation of App client for native not to generate App Secret. By updating the import path, customers can import the auth provisioned by the CLI in a different project. --- .../awscloudformation/import/index.ts | 147 +++++++++--------- .../awscloudformation/import/messages.ts | 5 +- .../src/__tests__/import_auth_1.test.ts | 118 ++++++++++++-- .../src/__tests__/import_auth_2.test.ts | 20 +-- .../src/import-helpers/types.ts | 17 ++ .../src/import-helpers/utilities.ts | 63 +++++++- .../src/import-helpers/walkthroughs.ts | 60 +++++-- 7 files changed, 315 insertions(+), 115 deletions(-) diff --git a/packages/amplify-category-auth/src/provider-utils/awscloudformation/import/index.ts b/packages/amplify-category-auth/src/provider-utils/awscloudformation/import/index.ts index 96f5528a778..bc58a99b355 100644 --- a/packages/amplify-category-auth/src/provider-utils/awscloudformation/import/index.ts +++ b/packages/amplify-category-auth/src/provider-utils/awscloudformation/import/index.ts @@ -1,17 +1,4 @@ -import Enquirer from 'enquirer'; -import _ from 'lodash'; -import uuid from 'uuid'; -import { - IdentityProviderType, - UserPoolClientType, - UserPoolDescriptionType, - UserPoolType, -} from 'aws-sdk/clients/cognitoidentityserviceprovider'; -import { CognitoIdentityProvider, IdentityPool } from 'aws-sdk/clients/cognitoidentity'; - import { $TSContext, ServiceSelection, stateManager } from 'amplify-cli-core'; -import { ICognitoUserPoolService, IIdentityPoolService } from 'amplify-util-import'; -import { importMessages } from './messages'; import { AuthSelections, BackendConfiguration, @@ -25,6 +12,19 @@ import { ProviderUtils, ResourceParameters, } from './types'; +import { CognitoIdentityProvider, IdentityPool } from 'aws-sdk/clients/cognitoidentity'; +import { ICognitoUserPoolService, IIdentityPoolService } from 'amplify-util-import'; +import { + IdentityProviderType, + UserPoolClientType, + UserPoolDescriptionType, + UserPoolType, +} from 'aws-sdk/clients/cognitoidentityserviceprovider'; + +import Enquirer from 'enquirer'; +import _ from 'lodash'; +import { importMessages } from './messages'; +import uuid from 'uuid'; // Currently the CLI only supports the output generation of these providers const supportedIdentityProviders = ['COGNITO', 'Facebook', 'Google', 'LoginWithAmazon']; @@ -355,17 +355,13 @@ const validateUserPool = async ( ): Promise => { const userPoolClients = await cognito.listUserPoolClients(userPoolId); const webClients = userPoolClients.filter(c => !c.ClientSecret); - const nativeClients = userPoolClients.filter(c => c.ClientSecret !== undefined); + const nativeClients = userPoolClients; - // Check if the selected user pool has at least 1 native and 1 web app client configured. + // Check if the selected user pool has at least 1 web app client configured. if (webClients?.length < 1) { return importMessages.NoAtLeastOneAppClient('Web'); } - if (nativeClients?.length < 1) { - return importMessages.NoAtLeastOneAppClient('Native'); - } - // If authSelections involves the selection of an Identity Pool as well then we have to look for an // IdentityPool that has the selected UserPool configured. This is an upfront validation for better DX // We can't validate until fully until AppClients are selected later. @@ -419,68 +415,77 @@ const selectAppClients = async ( answers: ImportAnswers, ): Promise => { let autoSelected = 0; + let changeAppClientSelction = false; + do { + // Select web application clients + if (questionParameters.webClients!.length === 1) { + answers.appClientWeb = questionParameters.webClients![0]; - // Select web application clients - if (questionParameters.webClients!.length === 1) { - answers.appClientWeb = questionParameters.webClients![0]; - - context.print.info(importMessages.SingleAppClientSelected('Web', answers.appClientWeb.ClientName!)); - - autoSelected++; - } else { - const appClientChoices = questionParameters - .webClients!.map(c => ({ - message: `${c.ClientName!} (${c.ClientId})`, - value: c.ClientId, - })) - .sort((a, b) => a.message.localeCompare(b.message)); + context.print.info(importMessages.SingleAppClientSelected('Web', answers.appClientWeb.ClientName!)); - const appClientSelectQuestion = { - type: 'select', - name: 'appClientWebId', - message: importMessages.Questions.SelectAppClient('Web'), - required: true, - choices: appClientChoices, - }; + autoSelected++; + } else { + const appClientChoices = questionParameters + .webClients!.map(c => ({ + message: `${c.ClientName!} (${c.ClientId})`, + value: c.ClientId, + })) + .sort((a, b) => a.message.localeCompare(b.message)); - context.print.info(importMessages.MultipleAppClients('Web')); + const appClientSelectQuestion = { + type: 'autocomplete', + name: 'appClientWebId', + message: importMessages.Questions.SelectAppClient('Web'), + required: true, + choices: appClientChoices, + limit: 5, + footer: importMessages.Questions.AutoCompleteFooter, + }; - const { appClientWebId } = await enquirer.prompt(appClientSelectQuestion); - answers.appClientWeb = questionParameters.webClients!.find(c => c.ClientId! === appClientWebId); - answers.appClientWebId = undefined; // Only to be used by enquirer - } + context.print.info(importMessages.MultipleAppClients('Web')); - // Select Native application client - if (questionParameters.nativeClients!.length === 1) { - answers.appClientNative = questionParameters.nativeClients![0]; + const { appClientWebId } = await enquirer.prompt(appClientSelectQuestion); + answers.appClientWeb = questionParameters.webClients!.find(c => c.ClientId! === appClientWebId); + answers.appClientWebId = undefined; // Only to be used by enquirer + } - context.print.info(importMessages.SingleAppClientSelected('Native', answers.appClientNative.ClientName!)); + // Select Native application client + if (questionParameters.nativeClients!.length === 1) { + answers.appClientNative = questionParameters.nativeClients![0]; - autoSelected++; - } else { - const appClientChoices = questionParameters - .nativeClients!.map(c => ({ - message: `${c.ClientName!} (${c.ClientId})`, - value: c.ClientId, - })) - .sort((a, b) => a.message.localeCompare(b.message)); + context.print.info(importMessages.SingleAppClientSelected('Native', answers.appClientNative.ClientName!)); + context.print.warning(importMessages.WarnAppClientReuse); + autoSelected++; + } else { + const appClientChoices = questionParameters + .nativeClients!.map(c => ({ + message: `${c.ClientName!} (${c.ClientId}) ${c.ClientSecret ? '(has app client secret)' : ''}`, + value: c.ClientId, + })) + .sort((a, b) => a.message.localeCompare(b.message)); - const appClientSelectQuestion = { - type: 'select', - name: 'appClientNativeId', - message: importMessages.Questions.SelectAppClient('Native'), - required: true, - choices: appClientChoices, - }; + const appClientSelectQuestion = { + type: 'autocomplete', + name: 'appClientNativeId', + message: importMessages.Questions.SelectAppClient('Native'), + required: true, + choices: appClientChoices, + limit: 5, + footer: importMessages.Questions.AutoCompleteFooter, + }; - context.print.info(importMessages.MultipleAppClients('Native')); + context.print.info(importMessages.MultipleAppClients('Native')); - const { appClientNativeId } = await enquirer.prompt(appClientSelectQuestion); - answers.appClientNative = questionParameters.nativeClients!.find(c => c.ClientId! === appClientNativeId); - answers.appClientNativeId = undefined; // Only to be used by enquirer - } + const { appClientNativeId } = await enquirer.prompt(appClientSelectQuestion); + answers.appClientNative = questionParameters.nativeClients!.find(c => c.ClientId! === appClientNativeId); + answers.appClientNativeId = undefined; // Only to be used by enquirer - questionParameters.bothAppClientsWereAutoSelected = autoSelected === 2; + if (answers.appClientNative === answers.appClientWeb) { + changeAppClientSelction = await context.prompt.confirm(importMessages.ConfirmUseDifferentAppClient); + } + } + questionParameters.bothAppClientsWereAutoSelected = autoSelected === 2; + } while (changeAppClientSelction); }; const appClientsOAuthPropertiesMatching = async ( @@ -726,7 +731,7 @@ const createMetaOutput = (answers: ImportAnswers, hasOAuthConfig: boolean): Meta UserPoolId: userPool.Id!, UserPoolName: userPool.Name!, AppClientID: answers.appClientNative!.ClientId, - AppClientSecret: answers.appClientNative!.ClientSecret, + ...(answers.appClientNative!.ClientSecret ? { AppClientSecret: answers.appClientNative!.ClientSecret } : {}), AppClientIDWeb: answers.appClientWeb!.ClientId, HostedUIDomain: userPool.Domain, }; diff --git a/packages/amplify-category-auth/src/provider-utils/awscloudformation/import/messages.ts b/packages/amplify-category-auth/src/provider-utils/awscloudformation/import/messages.ts index d45b2c6df51..b4baf0cec6c 100644 --- a/packages/amplify-category-auth/src/provider-utils/awscloudformation/import/messages.ts +++ b/packages/amplify-category-auth/src/provider-utils/awscloudformation/import/messages.ts @@ -25,7 +25,6 @@ export const importMessages = { `The selected Cognito User Pool does not have at least 1 ${type} app client configured. ${type} app clients are app clients ${ type === 'Web' ? 'without' : 'with' } a client secret.`, - OneIdentityPoolValid: (identityPoolName: string, identityPoolId: string) => `${greenCheck} Only one Identity Pool resource found: '${identityPoolName}' (${identityPoolId}) was automatically selected.`, MultipleIdentityPools: ` Multiple Identity Pools are configured for the selected Cognito User Pool.`, @@ -40,7 +39,9 @@ export const importMessages = { ImportPreviousResourceFooter: `If you choose No, then an import walkthrough will run to import a different resource into the new environment.`, ImportNewResourceRequired: (resourceName: string) => `Imported resource: '${resourceName}' found, parameters are required for environment creation.`, - + ConfirmUseDifferentAppClient: + 'It is recommended to use different app clients for web and native application, You have chosen the same app client for both. Do you want to change this?', + WarnAppClientReuse: '⚠️ It is recommended to use different app client for web and native application.', Questions: { UserPoolSelection: 'Select the User Pool you want to import:', IdentityPoolSelection: `Select the Identity Pool you want to import:`, diff --git a/packages/amplify-e2e-tests/src/__tests__/import_auth_1.test.ts b/packages/amplify-e2e-tests/src/__tests__/import_auth_1.test.ts index e287cc5b651..53479bb4710 100644 --- a/packages/amplify-e2e-tests/src/__tests__/import_auth_1.test.ts +++ b/packages/amplify-e2e-tests/src/__tests__/import_auth_1.test.ts @@ -1,9 +1,11 @@ -import * as path from 'path'; import * as fs from 'fs-extra'; +import * as path from 'path'; + +import { $TSObject, JSONUtilities } from 'amplify-cli-core'; import { + AddAuthUserPoolOnlyWithOAuthSettings, addApiWithCognitoUserPoolAuthTypeWhenAuthExists, addAuthUserPoolOnlyWithOAuth, - AddAuthUserPoolOnlyWithOAuthSettings, addFunction, amplifyPull, amplifyPush, @@ -18,30 +20,37 @@ import { initJSProjectWithProfile, initProjectWithAccessKey, } from 'amplify-e2e-core'; -import { randomizedFunctionName } from '../schema-api-directives/functionTester'; -import { getCognitoResourceName } from '../schema-api-directives/authHelper'; -import { addEnvironmentWithImportedAuth, checkoutEnvironment, removeEnvironment } from '../environment/env'; import { + AppClientSettings, + AuthProjectDetails, + addAppClientWithSecret, + addAppClientWithoutSecret, addS3WithAuthConfigurationMismatchErrorExit, createUserPoolOnlyWithOAuthSettings, + deleteAppClient, expectApiHasCorrectAuthConfig, - expectLocalAndCloudMetaFilesMatching, expectAuthLocalAndOGMetaFilesOutputMatching, + expectAuthProjectDetailsMatch, + expectLocalAndCloudMetaFilesMatching, expectLocalAndPulledBackendConfigMatching, expectLocalTeamInfoHasNoCategories, expectNoAuthInMeta, - expectAuthProjectDetailsMatch, - getOGAuthProjectDetails, getAuthProjectDetails, + getOGAuthProjectDetails, getShortId, + importIdentityPoolAndUserPool, importUserPoolOnly, - AuthProjectDetails, readRootStack, removeImportedAuthWithDefault, } from '../import-helpers'; -import { $TSObject, JSONUtilities } from 'amplify-cli-core'; +import { addEnvironmentWithImportedAuth, checkoutEnvironment, removeEnvironment } from '../environment/env'; + +import { getCognitoResourceName } from '../schema-api-directives/authHelper'; +import { randomizedFunctionName } from '../schema-api-directives/functionTester'; describe('auth import userpool only', () => { + const profileName = 'amplify-integ-test-user'; + const projectPrefix = 'auimpup'; const ogProjectPrefix = 'ogauimpup'; @@ -120,7 +129,7 @@ describe('auth import userpool only', () => { it('status should reflect correct values for imported auth', async () => { await initJSProjectWithProfile(projectRoot, projectSettings); - await importUserPoolOnly(projectRoot, ogSettings.userPoolName); + await importUserPoolOnly(projectRoot, ogSettings.userPoolName, { native: '_app_client ' }); let projectDetails = getAuthProjectDetails(projectRoot); @@ -148,7 +157,7 @@ describe('auth import userpool only', () => { it('imported auth with graphql api and cognito should push', async () => { await initJSProjectWithProfile(projectRoot, projectSettings); - await importUserPoolOnly(projectRoot, ogSettings.userPoolName); + await importUserPoolOnly(projectRoot, ogSettings.userPoolName, { native: '_app_client ' }); // space at to make sure its not web client await addApiWithCognitoUserPoolAuthTypeWhenAuthExists(projectRoot); await amplifyPush(projectRoot); @@ -157,7 +166,7 @@ describe('auth import userpool only', () => { it('imported auth with function and crud on auth should push', async () => { await initJSProjectWithProfile(projectRoot, projectSettings); - await importUserPoolOnly(projectRoot, ogSettings.userPoolName); + await importUserPoolOnly(projectRoot, ogSettings.userPoolName, { native: '_app_client ' }); const functionName = randomizedFunctionName('authimpfunc'); const authResourceName = getCognitoResourceName(projectRoot); @@ -215,7 +224,7 @@ describe('auth import userpool only', () => { it('imported userpool only auth, s3 storage add should fail with error', async () => { await initJSProjectWithProfile(projectRoot, projectSettings); - await importUserPoolOnly(projectRoot, ogSettings.userPoolName); + await importUserPoolOnly(projectRoot, ogSettings.userPoolName, { native: '_app_client ' }); // Imported auth resources cannot be used together with \'storage\' category\'s authenticated and unauthenticated access. await expect(addS3WithAuthConfigurationMismatchErrorExit(projectRoot, {})).rejects.toThrowError( @@ -228,7 +237,7 @@ describe('auth import userpool only', () => { ...projectSettings, disableAmplifyAppCreation: false, }); - await importUserPoolOnly(projectRoot, ogSettings.userPoolName); + await importUserPoolOnly(projectRoot, ogSettings.userPoolName, { native: '_app_client ' }); const functionName = randomizedFunctionName('authimpfunc'); const authResourceName = getCognitoResourceName(projectRoot); @@ -271,7 +280,7 @@ describe('auth import userpool only', () => { it('imported auth, create prod env, files should match', async () => { await initJSProjectWithProfile(projectRoot, projectSettings); - await importUserPoolOnly(projectRoot, ogSettings.userPoolName); + await importUserPoolOnly(projectRoot, ogSettings.userPoolName, { native: '_app_client ' }); await amplifyPushAuth(projectRoot); @@ -331,8 +340,83 @@ describe('auth import userpool only', () => { } as any); // The previously configured Cognito User Pool: '${userPoolName}' (${userPoolId}) cannot be found. - await expect(await importUserPoolOnly(projectRoot, ogSettings.userPoolName)).rejects.toThrowError( + await expect(await importUserPoolOnly(projectRoot, ogSettings.userPoolName, { native: '_app_client ' })).rejects.toThrowError( 'Process exited with non zero exit code 1', ); }); + + // Used for creating custom app clients. This should match with web app client setting for import to work + const customAppClientSettings: AppClientSettings = { + supportedIdentityProviders: ['COGNITO', 'Facebook', 'Google', 'LoginWithAmazon'], + allowedOAuthFlowsUserPoolClient: true, + callbackURLs: ['https://sin1/', 'https://sin2/'], + logoutURLs: ['https://sout1/', 'https://sout2/'], + allowedOAuthFlows: ['code'], + allowedScopes: ['aws.cognito.signin.user.admin', 'email', 'openid', 'phone', 'profile'], + }; + + it('should support importing AppClient with secret', async () => { + const nativeAppClientName = 'nativeClientWithSecret'; + let appClientId; + let appclientSecret; + try { + await initJSProjectWithProfile(projectRoot, projectSettings); + ({ appClientId, appclientSecret } = await addAppClientWithSecret( + profileName, + ogProjectRoot, + nativeAppClientName, + customAppClientSettings, + )); + await await importUserPoolOnly(projectRoot, ogSettings.userPoolName, { native: nativeAppClientName }); + await amplifyPushAuth(projectRoot); + expectLocalAndCloudMetaFilesMatching(projectRoot); + const projectDetails = getAuthProjectDetails(projectRoot); + expectAuthProjectDetailsMatch(projectDetails, { + ...ogProjectDetails, + meta: { ...ogProjectDetails.meta, AppClientID: appClientId, AppClientSecret: appclientSecret }, + team: { ...ogProjectDetails.team, nativeClientId: appClientId }, + }); + } finally { + // delete the app client + if (appClientId) { + deleteAppClient(profileName, ogProjectRoot, appClientId); + } + } + }); + + it('should support importing AppClient with out secret', async () => { + const nativeAppClientName = 'nativeClientWithOutSecret'; + let appClientId; + let appclientSecret; + + try { + await initJSProjectWithProfile(projectRoot, projectSettings); + + ({ appClientId, appclientSecret } = await addAppClientWithoutSecret( + profileName, + ogProjectRoot, + nativeAppClientName, + customAppClientSettings, + )); + + await await importUserPoolOnly(projectRoot, ogSettings.userPoolName, { native: nativeAppClientName, web: '_app_clientWeb' }); + + await amplifyPushAuth(projectRoot); + + expectLocalAndCloudMetaFilesMatching(projectRoot); + + const projectDetails = getAuthProjectDetails(projectRoot); + + expectAuthProjectDetailsMatch(projectDetails, { + ...ogProjectDetails, + meta: { ...ogProjectDetails.meta, AppClientID: appClientId, AppClientSecret: appclientSecret }, + team: { ...ogProjectDetails.team, nativeClientId: appClientId }, + }); + } finally { + // delete the app client + if (appClientId) { + deleteAppClient(profileName, ogProjectRoot, appClientId); + } + } + }); }); diff --git a/packages/amplify-e2e-tests/src/__tests__/import_auth_2.test.ts b/packages/amplify-e2e-tests/src/__tests__/import_auth_2.test.ts index 02aa5f3c346..81715b5854f 100644 --- a/packages/amplify-e2e-tests/src/__tests__/import_auth_2.test.ts +++ b/packages/amplify-e2e-tests/src/__tests__/import_auth_2.test.ts @@ -1,6 +1,6 @@ import { - addAuthIdentityPoolAndUserPoolWithOAuth, AddAuthIdentityPoolAndUserPoolWithOAuthSettings, + addAuthIdentityPoolAndUserPoolWithOAuth, addS3Storage, amplifyPull, amplifyPushAuth, @@ -12,18 +12,18 @@ import { initJSProjectWithProfile, } from 'amplify-e2e-core'; import { + AuthProjectDetails, createIDPAndUserPoolWithOAuthSettings, - expectLocalAndCloudMetaFilesMatching, expectAuthLocalAndOGMetaFilesOutputMatching, - expectLocalAndPulledBackendConfigMatching, expectAuthProjectDetailsMatch, - getOGAuthProjectDetails, + expectLocalAndCloudMetaFilesMatching, + expectLocalAndPulledBackendConfigMatching, getAuthProjectDetails, + getOGAuthProjectDetails, getShortId, headlessPull, headlessPullExpectError, importIdentityPoolAndUserPool, - AuthProjectDetails, } from '../import-helpers'; const profileName = 'amplify-integ-test-user'; @@ -107,7 +107,7 @@ describe('auth import identity pool and userpool', () => { it('auth import identitypool and userpool', async () => { await initJSProjectWithProfile(projectRoot, projectSettings); - await importIdentityPoolAndUserPool(projectRoot, ogSettings.userPoolName); + await importIdentityPoolAndUserPool(projectRoot, ogSettings.userPoolName, { native: '_app_client ' }); let projectDetails = getAuthProjectDetails(projectRoot); @@ -125,7 +125,7 @@ describe('auth import identity pool and userpool', () => { ...projectSettings, disableAmplifyAppCreation: false, }); - await importIdentityPoolAndUserPool(projectRoot, ogSettings.userPoolName); + await importIdentityPoolAndUserPool(projectRoot, ogSettings.userPoolName, { native: '_app_client ' }); await amplifyPushAuth(projectRoot); @@ -153,7 +153,7 @@ describe('auth import identity pool and userpool', () => { disableAmplifyAppCreation: false, }); - await importIdentityPoolAndUserPool(projectRoot, ogSettings.userPoolName); + await importIdentityPoolAndUserPool(projectRoot, ogSettings.userPoolName, { native: '_app_client ' }); await amplifyPushAuth(projectRoot); @@ -194,7 +194,7 @@ describe('auth import identity pool and userpool', () => { disableAmplifyAppCreation: false, }); - await importIdentityPoolAndUserPool(projectRoot, ogSettings.userPoolName); + await importIdentityPoolAndUserPool(projectRoot, ogSettings.userPoolName, { native: '_app_client ' }); await amplifyPushAuth(projectRoot); @@ -240,7 +240,7 @@ describe('auth import identity pool and userpool', () => { it('auth import, storage auth/guest access, push successful', async () => { await initJSProjectWithProfile(projectRoot, projectSettings); - await importIdentityPoolAndUserPool(projectRoot, ogSettings.userPoolName); + await importIdentityPoolAndUserPool(projectRoot, ogSettings.userPoolName, { native: '_app_client ' }); await addS3Storage(projectRoot); await amplifyPushAuth(projectRoot); diff --git a/packages/amplify-e2e-tests/src/import-helpers/types.ts b/packages/amplify-e2e-tests/src/import-helpers/types.ts index a23657298a6..4f1f9fbae29 100644 --- a/packages/amplify-e2e-tests/src/import-helpers/types.ts +++ b/packages/amplify-e2e-tests/src/import-helpers/types.ts @@ -1,3 +1,11 @@ +import { + BooleanType, + CallbackURLsListType, + OAuthFlowsType, + ScopeListType, + SupportedIdentityProvidersListType, +} from 'aws-sdk/clients/cognitoidentityserviceprovider'; + import { $TSObject } from 'amplify-cli-core'; export type AuthProjectDetails = { @@ -82,3 +90,12 @@ export type DynamoDBProjectDetails = { streamArn?: string; }; }; + +export type AppClientSettings = { + allowedOAuthFlows?: OAuthFlowsType; + callbackURLs?: CallbackURLsListType; + logoutURLs?: CallbackURLsListType; + allowedScopes?: ScopeListType; + supportedIdentityProviders?: SupportedIdentityProvidersListType; + allowedOAuthFlowsUserPoolClient?: BooleanType; +}; diff --git a/packages/amplify-e2e-tests/src/import-helpers/utilities.ts b/packages/amplify-e2e-tests/src/import-helpers/utilities.ts index 110b54160d1..0f7558f35d1 100644 --- a/packages/amplify-e2e-tests/src/import-helpers/utilities.ts +++ b/packages/amplify-e2e-tests/src/import-helpers/utilities.ts @@ -1,10 +1,13 @@ +import * as aws from 'aws-sdk'; import * as path from 'path'; -import { v4 as uuid } from 'uuid'; -import _ from 'lodash'; + import { $TSObject, JSONUtilities } from 'amplify-cli-core'; -import { getBackendAmplifyMeta, getTeamProviderInfo } from 'amplify-e2e-core'; +import { AppClientSettings, DynamoDBProjectDetails } from './types'; import { AuthProjectDetails, StorageProjectDetails } from '.'; -import { DynamoDBProjectDetails } from './types'; +import { getBackendAmplifyMeta, getProjectMeta, getTeamProviderInfo } from 'amplify-e2e-core'; + +import _ from 'lodash'; +import { v4 as uuid } from 'uuid'; export const getShortId = (): string => { const [shortId] = uuid().split('-'); @@ -263,3 +266,55 @@ export const getDynamoDBResourceName = (projectRoot: string): string => { }) as any; return dynamoDBResourceName; }; + +const addAppClient = async ( + profileName: string, + projectRoot: string, + clientName: string, + generateSecret: boolean, + settings: AppClientSettings, +) => { + const projectDetails = getProjectMeta(projectRoot); + const authDetails = getAuthProjectDetails(projectRoot); + const creds = new aws.SharedIniFileCredentials({ profile: profileName }); + aws.config.credentials = creds; + + const cognitoClient = new aws.CognitoIdentityServiceProvider({ region: projectDetails.providers.awscloudformation.Region }); + const response = await cognitoClient + .createUserPoolClient({ + ClientName: clientName, + UserPoolId: authDetails.meta.UserPoolId, + GenerateSecret: generateSecret, + AllowedOAuthFlows: settings.allowedOAuthFlows, + CallbackURLs: settings.callbackURLs, + LogoutURLs: settings.logoutURLs, + AllowedOAuthScopes: settings.allowedScopes, + SupportedIdentityProviders: settings.supportedIdentityProviders, + AllowedOAuthFlowsUserPoolClient: settings.allowedOAuthFlowsUserPoolClient, + }) + .promise(); + return { appClientId: response.UserPoolClient.ClientId, appclientSecret: response.UserPoolClient.ClientSecret }; +}; + +export const addAppClientWithSecret = async (profileName: string, projectRoot: string, clientName: string, settings: AppClientSettings) => { + return addAppClient(profileName, projectRoot, clientName, true, settings); +}; + +export const addAppClientWithoutSecret = async ( + profileName: string, + projectRoot: string, + clientName: string, + settings: AppClientSettings, +) => { + return addAppClient(profileName, projectRoot, clientName, false, settings); +}; + +export const deleteAppClient = async (profileName: string, projectRoot: string, clientId: string) => { + const authDetails = getAuthProjectDetails(projectRoot); + const projectDetails = getProjectMeta(projectRoot); + const creds = new aws.SharedIniFileCredentials({ profile: profileName }); + aws.config.credentials = creds; + + const cognitoClient = new aws.CognitoIdentityServiceProvider({ region: projectDetails.providers.awscloudformation.Region }); + await cognitoClient.deleteUserPoolClient({ ClientId: clientId, UserPoolId: authDetails.meta.UserPoolId }).promise(); +}; diff --git a/packages/amplify-e2e-tests/src/import-helpers/walkthroughs.ts b/packages/amplify-e2e-tests/src/import-helpers/walkthroughs.ts index c8dedabe650..0ee70a8dba1 100644 --- a/packages/amplify-e2e-tests/src/import-helpers/walkthroughs.ts +++ b/packages/amplify-e2e-tests/src/import-helpers/walkthroughs.ts @@ -1,20 +1,38 @@ -import { nspawn as spawn, getCLIPath } from 'amplify-e2e-core'; +import { getCLIPath, nspawn as spawn } from 'amplify-e2e-core'; -export const importUserPoolOnly = (cwd: string, autoCompletePrefix: string) => { +export const importUserPoolOnly = (cwd: string, autoCompletePrefix: string, clientNames?: { web?: string; native?: string }) => { return new Promise((resolve, reject) => { - spawn(getCLIPath(), ['auth', 'import'], { cwd, stripColors: true }) + const chain = spawn(getCLIPath(), ['auth', 'import'], { cwd, stripColors: true }) .wait('What type of auth resource do you want to import') .sendKeyDown() .sendCarriageReturn() .wait('Select the User Pool you want to import') .send(autoCompletePrefix) .delay(500) // Some delay required for autocomplete and terminal to catch up - .sendCarriageReturn() + .sendCarriageReturn(); + + if (clientNames?.web) { + chain + .wait('Select a Web client to import:') + .send(clientNames.web) + .delay(500) // Some delay required for autocomplete and terminal to catch up + .sendCarriageReturn(); + } + + if (clientNames?.native) { + chain.wait('Select a Native client to import:'); + chain + .send(clientNames.native) + .delay(500) // Some delay required for autocomplete and terminal to catch up + .sendCarriageReturn(); + } + + chain .wait('- JavaScript: https://docs.amplify.aws/lib/auth/getting-started/q/platform/js') .sendEof() .run((err: Error) => { if (!err) { - resolve(); + resolve(undefined); } else { reject(err); } @@ -22,20 +40,40 @@ export const importUserPoolOnly = (cwd: string, autoCompletePrefix: string) => { }); }; -export const importIdentityPoolAndUserPool = (cwd: string, autoCompletePrefix: string) => { +export const importIdentityPoolAndUserPool = (cwd: string, autoCompletePrefix: string, clientNames?: { web?: string; native?: string }) => { return new Promise((resolve, reject) => { - spawn(getCLIPath(), ['auth', 'import'], { cwd, stripColors: true }) + const chain = spawn(getCLIPath(), ['auth', 'import'], { cwd, stripColors: true }) .wait('What type of auth resource do you want to import') .sendCarriageReturn() .wait('Select the User Pool you want to import') .send(autoCompletePrefix) .delay(500) // Some delay required for autocomplete and terminal to catch up - .sendCarriageReturn() + .sendCarriageReturn(); + + if (clientNames?.web) { + chain + .wait('Select a Web client to import:') + .send(clientNames.web) + .delay(500) // Some delay required for autocomplete and terminal to catch up + .sendCarriageReturn(); + } + + if (clientNames?.native) { + chain.wait('Select a Native client to import:'); + chain + .send(clientNames.native) + .delay(500) // Some delay required for autocomplete and terminal to catch up + .sendCarriageReturn(); + } else { + chain.wait('Select a Native client to import:').sendCarriageReturn(); + } + + chain .wait('- JavaScript: https://docs.amplify.aws/lib/auth/getting-started/q/platform/js') .sendEof() .run((err: Error) => { if (!err) { - resolve(); + resolve(undefined); } else { reject(err); } @@ -53,7 +91,7 @@ export const removeImportedAuthWithDefault = (cwd: string) => { .sendEof() .run((err: Error) => { if (!err) { - resolve(); + resolve(undefined); } else { reject(err); } @@ -80,7 +118,7 @@ export const addS3WithAuthConfigurationMismatchErrorExit = (cwd: string, setting .sendEof() .run((err: Error) => { if (!err) { - resolve(); + resolve(undefined); } else { reject(err); }