diff --git a/.circleci/config.base.yml b/.circleci/config.base.yml index 0a298c1446a..06e5f3be9ac 100644 --- a/.circleci/config.base.yml +++ b/.circleci/config.base.yml @@ -264,6 +264,30 @@ jobs: path: ~/repo/packages/amplify-migration-tests/amplify-migration-reports working_directory: ~/repo + amplify_migration_tests_v4_30_0: + <<: *defaults + steps: + - attach_workspace: + at: ./ + - restore_cache: + key: amplify-cli-yarn-deps-{{ .Branch }}-{{ checksum "yarn.lock" }} + - run: + name: Update OS Packages + command: sudo apt-get update + - run: + name: Run tests migrating from CLI v4.30.0 + command: | + source .circleci/local_publish_helpers.sh + changeNpmGlobalPath + cd packages/amplify-migration-tests + yarn run migration_v4.30.0_auth --maxWorkers=3 $TEST_SUITE + no_output_timeout: 5m + - store_test_results: + path: packages/amplify-migration-tests/ + - store_artifacts: + path: ~/repo/packages/amplify-migration-tests/amplify-migration-reports + working_directory: ~/repo + amplify_migration_tests_latest: <<: *defaults steps: @@ -748,6 +772,16 @@ workflows: - feat-import requires: - build + - amplify_migration_tests_v4_30_0: + filters: + branches: + only: + - master + - graphqlschemae2e + - /fix\/team-provider/ + - feat-import + requires: + - build - amplify_console_integration_tests: filters: branches: diff --git a/.circleci/config.yml b/.circleci/config.yml index b848f9979b7..fe9aef5d66a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -322,6 +322,30 @@ jobs: path: packages/amplify-migration-tests/ - store_artifacts: path: ~/repo/packages/amplify-migration-tests/amplify-migration-reports + amplify_migration_tests_v4_30_0: + working_directory: ~/repo + docker: *ref_0 + resource_class: large + steps: + - attach_workspace: + at: ./ + - restore_cache: + key: 'amplify-cli-yarn-deps-{{ .Branch }}-{{ checksum "yarn.lock" }}' + - run: + name: Update OS Packages + command: sudo apt-get update + - run: + name: Run tests migrating from CLI v4.30.0 + command: | + source .circleci/local_publish_helpers.sh + changeNpmGlobalPath + cd packages/amplify-migration-tests + yarn run migration_v4.30.0_auth --maxWorkers=3 $TEST_SUITE + no_output_timeout: 5m + - store_test_results: + path: packages/amplify-migration-tests/ + - store_artifacts: + path: ~/repo/packages/amplify-migration-tests/amplify-migration-reports amplify_migration_tests_latest: working_directory: ~/repo docker: *ref_0 @@ -1304,6 +1328,16 @@ workflows: - feat-import requires: - build + - amplify_migration_tests_v4_30_0: + filters: + branches: + only: + - master + - graphqlschemae2e + - /fix\/team-provider/ + - feat-import + requires: + - build - amplify_console_integration_tests: filters: branches: diff --git a/packages/amplify-category-auth/resources/cloudformation-templates/auth-template.yml.ejs b/packages/amplify-category-auth/resources/cloudformation-templates/auth-template.yml.ejs index 15ae4a2c8bd..c8ff2b5c708 100644 --- a/packages/amplify-category-auth/resources/cloudformation-templates/auth-template.yml.ejs +++ b/packages/amplify-category-auth/resources/cloudformation-templates/auth-template.yml.ejs @@ -43,6 +43,7 @@ Parameters: Conditions: ShouldNotCreateEnvResources: !Equals [ !Ref env, NONE ] + HasHostedUIProviderCreds: !Not [ !Equals [ !Ref hostedUIProviderCreds, NONE ]] Resources: <%if (props.verificationBucketName) { %> @@ -305,6 +306,7 @@ Resources: <% } %> RefreshTokenValidity: !Ref userpoolClientRefreshTokenValidity UserPoolId: !Ref UserPool + PreventUserExistenceErrors: ENABLED DependsOn: UserPool UserPoolClient: # Created provide application access to user pool @@ -319,6 +321,7 @@ Resources: GenerateSecret: !Ref userpoolClientGenerateSecret RefreshTokenValidity: !Ref userpoolClientRefreshTokenValidity UserPoolId: !Ref UserPool + PreventUserExistenceErrors: ENABLED DependsOn: UserPool # BEGIN USER POOL LAMBDA RESOURCES UserPoolClientRole: @@ -367,7 +370,7 @@ Resources: - ' }' - '};' Handler: index.handler - Runtime: nodejs10.x + Runtime: nodejs12.x Timeout: '300' Role: !GetAtt - UserPoolClientRole @@ -493,7 +496,7 @@ Resources: Handler: index.handler - Runtime: nodejs10.x + Runtime: nodejs12.x Timeout: '300' Role: !GetAtt - UserPoolClientRole @@ -562,6 +565,9 @@ Resources: - ' const userPoolId = event.ResourceProperties.userPoolId;' - ' let hostedUIProviderMeta = JSON.parse(event.ResourceProperties.hostedUIProviderMeta);' - ' let hostedUIProviderCreds = JSON.parse(event.ResourceProperties.hostedUIProviderCreds);' + - ' if(Object.keys(hostedUIProviderCreds).length === 0) {' + - ' response.send(event, context, response.SUCCESS, {});' + - ' }' - ' if (event.RequestType == ''Delete'') {' - ' response.send(event, context, response.SUCCESS, {});' - ' }' @@ -622,7 +628,7 @@ Resources: - '} ' Handler: index.handler - Runtime: nodejs10.x + Runtime: nodejs12.x Timeout: '300' Role: !GetAtt - UserPoolClientRole @@ -672,7 +678,7 @@ Resources: ServiceToken: !GetAtt HostedUIProvidersCustomResource.Arn userPoolId: !Ref UserPool hostedUIProviderMeta: !Ref hostedUIProviderMeta - hostedUIProviderCreds: !Ref hostedUIProviderCreds + hostedUIProviderCreds: !If [HasHostedUIProviderCreds, !Ref hostedUIProviderCreds, "{}" ] DependsOn: HostedUIProvidersCustomResourceLogPolicy <% } %> <%if (props.oAuthMetadata) { %> @@ -721,7 +727,7 @@ Resources: - '}' Handler: index.handler - Runtime: nodejs10.x + Runtime: nodejs12.x Timeout: '300' Role: !GetAtt - UserPoolClientRole @@ -842,7 +848,7 @@ Resources: - ' }' - '};' Handler: index.handler - Runtime: nodejs10.x + Runtime: nodejs12.x Timeout: '300' Role: !GetAtt - MFALambdaRole @@ -1002,7 +1008,7 @@ Resources: - ' }' - '};' Handler: index.handler - Runtime: nodejs10.x + Runtime: nodejs12.x Timeout: '300' Role: !GetAtt - OpenIdLambdaRole diff --git a/packages/amplify-cli-core/src/__tests__/moveSecretsToDeployment.test.ts b/packages/amplify-cli-core/src/__tests__/moveSecretsToDeployment.test.ts new file mode 100644 index 00000000000..d16ec53935a --- /dev/null +++ b/packages/amplify-cli-core/src/__tests__/moveSecretsToDeployment.test.ts @@ -0,0 +1,111 @@ +import { StateManager } from '../../lib'; + +const teamProviderInfoSecrets = { + dev: { + awscloudformation: { + AuthRoleName: 'amplify-teamprovider-dev-134909-authRole', + UnauthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-dev-134909-unauthRole', + AuthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-dev-134909-authRole', + Region: 'us-east-1', + DeploymentBucketName: 'amplify-teamprovider-dev-134909-deployment', + UnauthRoleName: 'amplify-teamprovider-dev-134909-unauthRole', + StackName: 'amplify-teamprovider-dev-134909', + StackId: 'arn:aws:cloudformation:us-east-1:1234567891011:stack/amplify-teamprovider-dev-134909/df33f4d0-1895-11eb-a8b4-0e706f74ed45', + AmplifyAppId: 'd1gmlw7l76gj9', + }, + categories: { + auth: { + teamprovider1819bdce: { + hostedUIProviderCreds: '[{"ProviderName":"Facebook","client_id":"asdasdasdasd","client_secret":"asdasdasd"}]', + }, + }, + }, + }, + prod: { + awscloudformation: { + AuthRoleName: 'amplify-teamprovider-prod-164239-authRole', + UnauthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-prod-164239-unauthRole', + AuthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-prod-164239-authRole', + Region: 'us-east-1', + DeploymentBucketName: 'amplify-teamprovider-prod-164239-deployment', + UnauthRoleName: 'amplify-teamprovider-prod-164239-unauthRole', + StackName: 'amplify-teamprovider-prod-164239', + StackId: 'arn:aws:cloudformation:us-east-1:1234567891011:stack/amplify-teamprovider-prod-164239/1b625f60-18ae-11eb-9e65-0ab042f700a7', + AmplifyAppId: 'd1gmlw7l76gj9', + }, + categories: { + auth: { + teamprovider1819bdce: { + hostedUIProviderCreds: '[{"ProviderName":"Facebook","client_id":"abc","client_secret":"adb"}]', + }, + }, + }, + }, +}; + +const teamProviderInfoWithoutSecrets = { + dev: { + awscloudformation: { + AuthRoleName: 'amplify-teamprovider-dev-134909-authRole', + UnauthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-dev-134909-unauthRole', + AuthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-dev-134909-authRole', + Region: 'us-east-1', + DeploymentBucketName: 'amplify-teamprovider-dev-134909-deployment', + UnauthRoleName: 'amplify-teamprovider-dev-134909-unauthRole', + StackName: 'amplify-teamprovider-dev-134909', + StackId: 'arn:aws:cloudformation:us-east-1:1234567891011:stack/amplify-teamprovider-dev-134909/df33f4d0-1895-11eb-a8b4-0e706f74ed45', + AmplifyAppId: 'd1gmlw7l76gj9', + }, + categories: { + auth: { + teamprovider1819bdce: {}, + }, + }, + }, + prod: { + awscloudformation: { + AuthRoleName: 'amplify-teamprovider-prod-164239-authRole', + UnauthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-prod-164239-unauthRole', + AuthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-prod-164239-authRole', + Region: 'us-east-1', + DeploymentBucketName: 'amplify-teamprovider-prod-164239-deployment', + UnauthRoleName: 'amplify-teamprovider-prod-164239-unauthRole', + StackName: 'amplify-teamprovider-prod-164239', + StackId: 'arn:aws:cloudformation:us-east-1:1234567891011:stack/amplify-teamprovider-prod-164239/1b625f60-18ae-11eb-9e65-0ab042f700a7', + AmplifyAppId: 'd1gmlw7l76gj9', + }, + categories: { + auth: { + teamprovider1819bdce: { + hostedUIProviderCreds: '[{"ProviderName":"Facebook","client_id":"abc","client_secret":"adb"}]', + }, + }, + }, + }, +}; +const secrets = { + d1gmlw7l76gj9: { + dev: { + auth: { + teamprovider1819bdce: { + hostedUIProviderCreds: '[{"ProviderName":"Facebook","client_id":"asdasdasdasd","client_secret":"asdasdasd"}]', + }, + }, + }, + }, +}; + +describe('test move secrests to deployment', () => { + const stateManager = new StateManager(); + const mockGetLocalEnvInfo = jest.spyOn(stateManager, 'getLocalEnvInfo').mockReturnValue({ envName: 'dev' }); + const mockGetTeamProviderInfo = jest.spyOn(stateManager, 'getTeamProviderInfo').mockReturnValue(teamProviderInfoSecrets); + const setTeamProviderInfo = jest.spyOn(stateManager, 'setTeamProviderInfo').mockImplementation(); + const setDeploymentSecrets = jest.spyOn(stateManager, 'setDeploymentSecrets').mockImplementation(); + it('test with migrate', () => { + stateManager.moveSecretsFromDeploymentToTeamProvider(); + expect(setTeamProviderInfo).toBeCalledWith(undefined, teamProviderInfoWithoutSecrets); + expect(setDeploymentSecrets).toBeCalledWith(secrets); + expect(mockGetLocalEnvInfo).toBeCalled(); + expect(mockGetLocalEnvInfo).toBeCalled(); + }); +}); diff --git a/packages/amplify-cli-core/src/errors/index.ts b/packages/amplify-cli-core/src/errors/index.ts index 2a3992bf3ac..ffbaff59a71 100644 --- a/packages/amplify-cli-core/src/errors/index.ts +++ b/packages/amplify-cli-core/src/errors/index.ts @@ -10,3 +10,4 @@ export class NonEmptyDirectoryError extends Error {} export class InvalidEnvironmentNameError extends Error {} export class InvalidSubCommandError extends Error {} export class FrontendBuildError extends Error {} +export class MigrateError extends Error {} diff --git a/packages/amplify-cli-core/src/jsonUtilities.ts b/packages/amplify-cli-core/src/jsonUtilities.ts index 663887307c7..4427d0d209b 100644 --- a/packages/amplify-cli-core/src/jsonUtilities.ts +++ b/packages/amplify-cli-core/src/jsonUtilities.ts @@ -43,6 +43,7 @@ export class JSONUtilities { options?: { minify?: boolean; keepComments?: boolean; + mode?: number; }, ): void => { if (!fileName) { @@ -68,7 +69,10 @@ export class JSONUtilities { const dirPath = path.dirname(fileName); fs.ensureDirSync(dirPath); - fs.writeFileSync(fileName, jsonString, 'utf8'); + fs.writeFileSync(fileName, jsonString, { + encoding: 'utf8', + mode: options?.mode, + }); }; public static parse = ( diff --git a/packages/amplify-cli-core/src/state-manager/pathManager.ts b/packages/amplify-cli-core/src/state-manager/pathManager.ts index fdb97382853..4c4f6518f6d 100644 --- a/packages/amplify-cli-core/src/state-manager/pathManager.ts +++ b/packages/amplify-cli-core/src/state-manager/pathManager.ts @@ -7,6 +7,7 @@ export const PathConstants = { DotAWSDir: '.aws', AWSCredentials: 'credentials', AWSConfig: 'config', + DeploymentSecrets: 'deployment-secrets.json', // in project root AmplifyDirName: 'amplify', @@ -130,6 +131,10 @@ export class PathManager { return this.constructPath(projectPath, [PathConstants.AmplifyDirName, fileName]); }; + getDotAWSAmplifyDirPath = (): string => path.normalize(path.join(homedir(), PathConstants.DotAWSDir, PathConstants.AmplifyDirName)); + + getDeploymentSecrets = (): string => path.normalize(path.join(this.getDotAWSAmplifyDirPath(), PathConstants.DeploymentSecrets)); + private constructPath = (projectPath?: string, segments: string[] = []): string => { if (!projectPath) { projectPath = this.findProjectRoot(); diff --git a/packages/amplify-cli-core/src/state-manager/stateManager.ts b/packages/amplify-cli-core/src/state-manager/stateManager.ts index b5756392a9e..169cb4ff6a8 100644 --- a/packages/amplify-cli-core/src/state-manager/stateManager.ts +++ b/packages/amplify-cli-core/src/state-manager/stateManager.ts @@ -3,12 +3,13 @@ import { pathManager } from './pathManager'; import { $TSMeta, $TSTeamProviderInfo, $TSAny } from '..'; import { JSONUtilities } from '../jsonUtilities'; import { Tag, ReadValidateTags } from '../tags'; - +import _ from 'lodash'; export type GetOptions = { throwIfNotExist?: boolean; preserveComments?: boolean; default?: T; }; +const hostedUIProviderCredsField = 'hostedUIProviderCreds'; export class StateManager { metaFileExists = (projectPath?: string): boolean => fs.existsSync(pathManager.getAmplifyMetaFilePath(projectPath)); @@ -27,6 +28,11 @@ export class StateManager { currentMetaFileExists = (projectPath?: string): boolean => fs.existsSync(pathManager.getCurrentAmplifyMetaFilePath(projectPath)); + setDeploymentSecrets = (deploymentSecrets: $TSAny): void => { + const path = pathManager.getDeploymentSecrets(); + JSONUtilities.writeJson(path, deploymentSecrets, { mode: 0o600 }); //set deployment secret file permissions to -rw------- + }; + getCurrentMeta = (projectPath?: string, options?: GetOptions<$TSMeta>): $TSMeta => { const filePath = pathManager.getCurrentAmplifyMetaFilePath(projectPath); const mergedOptions = { @@ -39,12 +45,55 @@ export class StateManager { return data; }; + getDeploymentSecrets = (): $TSAny => { + return ( + JSONUtilities.readJson<$TSAny>(pathManager.getDeploymentSecrets(), { + throwIfNotExist: false, + }) || {} + ); + }; + getProjectTags = (projectPath?: string): Tag[] => ReadValidateTags(pathManager.getTagFilePath(projectPath)); getCurrentProjectTags = (projectPath?: string): Tag[] => ReadValidateTags(pathManager.getCurrentTagFilePath(projectPath)); teamProviderInfoExists = (projectPath?: string): boolean => fs.existsSync(pathManager.getTeamProviderInfoFilePath(projectPath)); + teamProviderInfoHasAuthSecrets = (projectPath?: string): any => { + if (this.teamProviderInfoExists(projectPath)) { + const teamProviderInfo = this.getTeamProviderInfo(projectPath); + const { envName } = this.getLocalEnvInfo(); + const envTeamProvider = teamProviderInfo[envName]; + if (envTeamProvider && envTeamProvider.categories && envTeamProvider.categories.auth) { + return _.some(Object.keys(envTeamProvider.categories.auth), resource => { + return envTeamProvider.categories.auth[resource][hostedUIProviderCredsField]; + }); + } + } + return false; + }; + + moveSecretsFromDeploymentToTeamProvider = (projectPath?: string): void => { + const { envName } = this.getLocalEnvInfo(projectPath); + let teamProviderInfo = this.getTeamProviderInfo(); + const envTeamProvider = teamProviderInfo[envName]; + const amplifyAppId = envTeamProvider.awscloudformation.AmplifyAppId; + let secrets = {}; + Object.keys(envTeamProvider.categories).forEach(category => { + if (category === 'auth') { + Object.keys(envTeamProvider.categories.auth).forEach(resourceName => { + if (envTeamProvider.categories.auth[resourceName][hostedUIProviderCredsField]) { + const teamProviderSecrets = envTeamProvider.categories.auth[resourceName][hostedUIProviderCredsField]; + delete envTeamProvider.categories.auth[resourceName][hostedUIProviderCredsField]; + secrets = _.set(secrets, [amplifyAppId, envName, 'auth', resourceName, hostedUIProviderCredsField], teamProviderSecrets); + } + }); + } + }); + this.setTeamProviderInfo(undefined, teamProviderInfo); + this.setDeploymentSecrets(secrets); + }; + getTeamProviderInfo = (projectPath?: string, options?: GetOptions<$TSTeamProviderInfo>): $TSTeamProviderInfo => { const filePath = pathManager.getTeamProviderInfoFilePath(projectPath); const mergedOptions = { diff --git a/packages/amplify-cli/src/__tests__/team-provider-migrate.test.ts b/packages/amplify-cli/src/__tests__/team-provider-migrate.test.ts new file mode 100644 index 00000000000..756529a6fbf --- /dev/null +++ b/packages/amplify-cli/src/__tests__/team-provider-migrate.test.ts @@ -0,0 +1,121 @@ +import { Context } from '../domain/context'; + +const teamProviderInfoSecrets = { + dev: { + awscloudformation: { + AuthRoleName: 'amplify-teamprovider-dev-134909-authRole', + UnauthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-dev-134909-unauthRole', + AuthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-dev-134909-authRole', + Region: 'us-east-1', + DeploymentBucketName: 'amplify-teamprovider-dev-134909-deployment', + UnauthRoleName: 'amplify-teamprovider-dev-134909-unauthRole', + StackName: 'amplify-teamprovider-dev-134909', + StackId: 'arn:aws:cloudformation:us-east-1:1234567891011:stack/amplify-teamprovider-dev-134909/df33f4d0-1895-11eb-a8b4-0e706f74ed45', + AmplifyAppId: 'd1gmlw7l76gj9', + }, + categories: { + auth: { + teamprovider1819bdce: { + hostedUIProviderCreds: '[{"ProviderName":"Facebook","client_id":"asdasdasdasd","client_secret":"asdasdasd"}]', + }, + }, + }, + }, + prod: { + awscloudformation: { + AuthRoleName: 'amplify-teamprovider-prod-164239-authRole', + UnauthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-prod-164239-unauthRole', + AuthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-prod-164239-authRole', + Region: 'us-east-1', + DeploymentBucketName: 'amplify-teamprovider-prod-164239-deployment', + UnauthRoleName: 'amplify-teamprovider-prod-164239-unauthRole', + StackName: 'amplify-teamprovider-prod-164239', + StackId: 'arn:aws:cloudformation:us-east-1:1234567891011:stack/amplify-teamprovider-prod-164239/1b625f60-18ae-11eb-9e65-0ab042f700a7', + AmplifyAppId: 'd1gmlw7l76gj9', + }, + categories: { + auth: { + teamprovider1819bdce: { + hostedUIProviderCreds: '[{"ProviderName":"Facebook","client_id":"abc","client_secret":"adb"}]', + }, + }, + }, + }, +}; + +const teamProviderInfoWithoutSecrets = { + dev: { + awscloudformation: { + AuthRoleName: 'amplify-teamprovider-dev-134909-authRole', + UnauthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-dev-134909-unauthRole', + AuthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-dev-134909-authRole', + Region: 'us-east-1', + DeploymentBucketName: 'amplify-teamprovider-dev-134909-deployment', + UnauthRoleName: 'amplify-teamprovider-dev-134909-unauthRole', + StackName: 'amplify-teamprovider-dev-134909', + StackId: 'arn:aws:cloudformation:us-east-1:1234567891011:stack/amplify-teamprovider-dev-134909/df33f4d0-1895-11eb-a8b4-0e706f74ed45', + AmplifyAppId: 'd1gmlw7l76gj9', + }, + categories: { + auth: { + teamprovider1819bdce: {}, + }, + }, + }, + prod: { + awscloudformation: { + AuthRoleName: 'amplify-teamprovider-prod-164239-authRole', + UnauthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-prod-164239-unauthRole', + AuthRoleArn: 'arn:aws:iam::1234567891011:role/amplify-teamprovider-prod-164239-authRole', + Region: 'us-east-1', + DeploymentBucketName: 'amplify-teamprovider-prod-164239-deployment', + UnauthRoleName: 'amplify-teamprovider-prod-164239-unauthRole', + StackName: 'amplify-teamprovider-prod-164239', + StackId: 'arn:aws:cloudformation:us-east-1:1234567891011:stack/amplify-teamprovider-prod-164239/1b625f60-18ae-11eb-9e65-0ab042f700a7', + AmplifyAppId: 'd1gmlw7l76gj9', + }, + categories: { + auth: { + teamprovider1819bdce: { + hostedUIProviderCreds: '[{"ProviderName":"Facebook","client_id":"abc","client_secret":"adb"}]', + }, + }, + }, + }, +}; +const secrets = { + d1gmlw7l76gj9: { + dev: { + auth: { + teamprovider1819bdce: { + hostedUIProviderCreds: '[{"ProviderName":"Facebook","client_id":"asdasdasdasd","client_secret":"asdasdasd"}]', + }, + }, + }, + }, +}; + +describe('test migration code', () => { + it('case: test for teamprovide with secrets', async () => { + const promptConfirm = jest.fn().mockReturnValue(true); + const mockMoveSecrets = jest.fn(); + const mockteamProviderInfoHasAuthSecrets = jest.fn().mockReturnValue(true); + jest.setMock('amplify-cli-core', { + stateManager: { + moveSecretsFromDeploymentToTeamProvider: mockMoveSecrets, + teamProviderInfoHasAuthSecrets: mockteamProviderInfoHasAuthSecrets, + }, + pathManager: { + findProjectRoot: jest.fn().mockReturnValue(true), + }, + }); + const mockContext: Context = jest.genMockFromModule('../domain/context'); + mockContext.prompt = { + confirm: promptConfirm, + }; + const migrated = await require('../utils/team-provider-migrate').MigrateTeamProvider(mockContext); + expect(migrated).toEqual(true); + expect(mockMoveSecrets).toBeCalled(); + expect(mockteamProviderInfoHasAuthSecrets).toBeCalled(); + }); +}); diff --git a/packages/amplify-cli/src/domain/amplify-toolkit.ts b/packages/amplify-cli/src/domain/amplify-toolkit.ts index b14cd59291e..5db5250faef 100644 --- a/packages/amplify-cli/src/domain/amplify-toolkit.ts +++ b/packages/amplify-cli/src/domain/amplify-toolkit.ts @@ -58,6 +58,7 @@ export class AmplifyToolkit { private _loadEnvResourceParameters: any; private _saveEnvResourceParameters: any; private _removeResourceParameters: any; + private _removeDeploymentSecrets: any; private _triggerFlow: any; private _addTrigger: any; private _updateTrigger: any; @@ -345,6 +346,12 @@ export class AmplifyToolkit { return this._removeResourceParameters; } + get removeDeploymentSecrets(): any { + this._removeDeploymentSecrets = + this._removeDeploymentSecrets || require(path.join(this._amplifyHelpersDirPath, 'envResourceParams')).removeDeploymentSecrets; + return this._removeDeploymentSecrets; + } + get triggerFlow(): any { this._triggerFlow = this._triggerFlow || require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).triggerFlow; return this._triggerFlow; @@ -451,9 +458,9 @@ export class AmplifyToolkit { addCleanUpTask = (task: (context: Context) => void) => { this._cleanUpTasks.push(task); - } + }; runCleanUpTasks = async (context: Context) => { await Promise.all(this._cleanUpTasks.map(task => task(context))); - } + }; } diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/envResourceParams.ts b/packages/amplify-cli/src/extensions/amplify-helpers/envResourceParams.ts index 7d1b9146c27..b35ebf89651 100644 --- a/packages/amplify-cli/src/extensions/amplify-helpers/envResourceParams.ts +++ b/packages/amplify-cli/src/extensions/amplify-helpers/envResourceParams.ts @@ -1,8 +1,10 @@ import _ from 'lodash'; import { getEnvInfo } from './get-env-info'; import { $TSContext, $TSObject, stateManager } from 'amplify-cli-core'; +import { getAmplifyAppId } from './get-amplify-appId'; const CATEGORIES = 'categories'; +const hostedUIProviderCredsField = 'hostedUIProviderCreds'; function isMigrationContext(context: $TSContext) { return 'migrationInfo' in context; @@ -62,20 +64,45 @@ export function saveEnvResourceParameters(context: $TSContext, category: string, const teamProviderInfo = getApplicableTeamProviderInfo(context); const currentEnv = getCurrentEnvName(context); const resources = getOrCreateSubObject(teamProviderInfo, [currentEnv, CATEGORIES, category]); - - resources[resource] = _.assign(resources[resource], parameters); + const { hostedUIProviderCreds, ...otherParameters } = parameters; + resources[resource] = _.assign(resources[resource], otherParameters); if (!isMigrationContext(context)) { stateManager.setTeamProviderInfo(undefined, teamProviderInfo); + // write hostedUIProviderCreds to deploymentSecrets + if (hostedUIProviderCreds) { + const deploymentSecrets = stateManager.getDeploymentSecrets(); + const appId = getAmplifyAppId(); + const newDeploymentSecrets = getOrCreateSubObject(deploymentSecrets, [appId, currentEnv, category, resource]); + newDeploymentSecrets[hostedUIProviderCredsField] = hostedUIProviderCreds; + stateManager.setDeploymentSecrets(deploymentSecrets); + } } } export function loadEnvResourceParameters(context: $TSContext, category: string, resource: string) { - const teamProviderInfo = getApplicableTeamProviderInfo(context); + const envParameters = { + ...loadEnvResourceParametersFromDeploymentSecrets(context, category, resource), + ...loadEnvResourceParametersFromTeamprovider(context, category, resource), + }; + return envParameters; +} +function loadEnvResourceParametersFromDeploymentSecrets(context: $TSContext, category: string, resource: string) { try { const currentEnv = getCurrentEnvName(context); - + const deploymentSecrets = stateManager.getDeploymentSecrets(); + const appId = getAmplifyAppId(); + const val = getOrCreateSubObject(deploymentSecrets, [appId, currentEnv, category, resource]); + return val; + } catch (e) { + return {}; + } +} +function loadEnvResourceParametersFromTeamprovider(context: $TSContext, category: string, resource: string) { + try { + const teamProviderInfo = getApplicableTeamProviderInfo(context); + const currentEnv = getCurrentEnvName(context); return getOrCreateSubObject(teamProviderInfo, [currentEnv, CATEGORIES, category, resource]); } catch (e) { return {}; @@ -85,10 +112,21 @@ export function loadEnvResourceParameters(context: $TSContext, category: string, export function removeResourceParameters(context: $TSContext, category: string, resource: string) { const teamProviderInfo = getApplicableTeamProviderInfo(context); const currentEnv = getCurrentEnvName(context); - removeObjectRecursively(teamProviderInfo, [currentEnv, CATEGORIES, category, resource]); if (!isMigrationContext(context)) { stateManager.setTeamProviderInfo(undefined, teamProviderInfo); + removeDeploymentSecrets(context, category, resource); + } +} +// removes deployment secrets +// called after remove and push +export function removeDeploymentSecrets(context: $TSContext, category: string, resource: string) { + const currentEnv = getCurrentEnvName(context); + const deploymentSecrets = stateManager.getDeploymentSecrets(); + const appId = getAmplifyAppId(); + removeObjectRecursively(deploymentSecrets, [appId, currentEnv, category, resource, hostedUIProviderCredsField]); + if (!isMigrationContext(context)) { + stateManager.setDeploymentSecrets(deploymentSecrets); } } diff --git a/packages/amplify-cli/src/index.ts b/packages/amplify-cli/src/index.ts index d105556981c..4876ec8486f 100644 --- a/packages/amplify-cli/src/index.ts +++ b/packages/amplify-cli/src/index.ts @@ -1,6 +1,14 @@ import * as fs from 'fs-extra'; import * as path from 'path'; -import { $TSContext, CLIContextEnvironmentProvider, FeatureFlags, pathManager, stateManager, exitOnNextTick } from 'amplify-cli-core'; +import { + $TSContext, + CLIContextEnvironmentProvider, + FeatureFlags, + pathManager, + stateManager, + exitOnNextTick, + MigrateError, +} from 'amplify-cli-core'; import { Input } from './domain/input'; import { getPluginPlatform, scan } from './plugin-manager'; import { getCommandLineInput, verifyInput } from './input-manager'; @@ -17,7 +25,7 @@ import { notify } from './version-notifier'; import { EventEmitter } from 'events'; import { rewireDeprecatedCommands } from './rewireDeprecatedCommands'; import { ensureMobileHubCommandCompatibility } from './utils/mobilehub-support'; -import { postInstallInitialization } from './utils/post-install-initialization'; +import { MigrateTeamProvider } from './utils/team-provider-migrate'; EventEmitter.defaultMaxListeners = 1000; // entry from commandline @@ -71,6 +79,10 @@ export async function run() { await attachUsageData(context); + if (!(await MigrateTeamProvider(context))) { + context.usageData.emitError(new MigrateError()); + return 1; + } errorHandler = boundErrorHandler.bind(context); process.on('SIGINT', sigIntHandler.bind(context)); diff --git a/packages/amplify-cli/src/utils/team-provider-migrate.ts b/packages/amplify-cli/src/utils/team-provider-migrate.ts new file mode 100644 index 00000000000..9afde2ca636 --- /dev/null +++ b/packages/amplify-cli/src/utils/team-provider-migrate.ts @@ -0,0 +1,18 @@ +import { Context } from '../domain/context'; +import { stateManager, pathManager } from 'amplify-cli-core'; +import _ from 'lodash'; +import { externalAuthEnable } from 'amplify-category-auth'; +const message = `Amplify auth will be modified to mangage secrets from deployment-secrets.json. Would you like to proceed?`; + +export async function MigrateTeamProvider(context: Context): Promise { + // check if command executed in proj root and team provider has secrets + if (pathManager.findProjectRoot() && stateManager.teamProviderInfoHasAuthSecrets()) { + if (await context.prompt.confirm(message)) { + stateManager.moveSecretsFromDeploymentToTeamProvider(); + externalAuthEnable(context, undefined, undefined, { authSelections: 'identityPoolAndUserPool' }); + } else { + return false; + } + } + return true; +} diff --git a/packages/amplify-e2e-core/src/index.ts b/packages/amplify-e2e-core/src/index.ts index cee83b380c1..7cabf27ffa0 100644 --- a/packages/amplify-e2e-core/src/index.ts +++ b/packages/amplify-e2e-core/src/index.ts @@ -21,7 +21,10 @@ declare global { const amplifyTestsDir = 'amplify-e2e-tests'; export function getCLIPath(testingWithLatestCodebase = false) { + console.log(`isCI(): ${isCI()}`); + console.log(`testingWithLatestCodebase: ${testingWithLatestCodebase}`); if (isCI() && !testingWithLatestCodebase) { + console.log('running as amplify'); return 'amplify'; } return path.join(__dirname, '..', '..', 'amplify-cli', 'bin', 'amplify'); diff --git a/packages/amplify-e2e-core/src/init/initProjectHelper.ts b/packages/amplify-e2e-core/src/init/initProjectHelper.ts index 2440681027d..d6fb5e5c3f0 100644 --- a/packages/amplify-e2e-core/src/init/initProjectHelper.ts +++ b/packages/amplify-e2e-core/src/init/initProjectHelper.ts @@ -266,10 +266,44 @@ export function initNewEnvWithProfile(cwd: string, s: { envName: string }) { }); } -export function amplifyStatus(cwd: string, expectedStatus: string) { +export function amplifyVersion(cwd: string, expectedVersion: string, testingWithLatestCodebase = false) { + console.log(`CLI path: ${getCLIPath(testingWithLatestCodebase)}`); + return new Promise((resolve, reject) => { + spawn(getCLIPath(testingWithLatestCodebase), ['--version'], { cwd, stripColors: true }) + .wait(expectedVersion) + .run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} + +//Can be called only if detects teamprovider change +export function amplifyStatusWithMigrate(cwd: string, expectedStatus: string, testingWithLatestCodebase = false) { + return new Promise((resolve, reject) => { + let regex = new RegExp(`.*${expectedStatus}*`); + spawn(getCLIPath(testingWithLatestCodebase), ['status'], { cwd, stripColors: true }) + .wait('Amplify auth will be modified to mangage secrets from deployment-secret') + .sendLine('y') + .wait(regex) + .sendLine('\r') + .run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} + +export function amplifyStatus(cwd: string, expectedStatus: string, testingWithLatestCodebase = false) { return new Promise((resolve, reject) => { let regex = new RegExp(`.*${expectedStatus}*`); - spawn(getCLIPath(), ['status'], { cwd, stripColors: true }) + spawn(getCLIPath(testingWithLatestCodebase), ['status'], { cwd, stripColors: true }) .wait(regex) .sendLine('\r') .run((err: Error) => { diff --git a/packages/amplify-e2e-core/src/utils/projectMeta.ts b/packages/amplify-e2e-core/src/utils/projectMeta.ts index 8d69f207771..8508b25d7cf 100644 --- a/packages/amplify-e2e-core/src/utils/projectMeta.ts +++ b/packages/amplify-e2e-core/src/utils/projectMeta.ts @@ -1,5 +1,7 @@ import * as path from 'path'; +import * as os from 'os'; import * as fs from 'fs-extra'; +import _ from 'lodash'; function getAWSConfigAndroidPath(projRoot: string): string { return path.join(projRoot, 'app', 'src', 'main', 'res', 'raw', 'awsconfiguration.json'); @@ -69,6 +71,20 @@ function getAwsIOSConfig(projectRoot: string) { return JSON.parse(fs.readFileSync(configPath, 'utf8')); } +function getDeploymentSecrets(): any { + const deploymentSecretsPath = path.join(os.homedir(), '.aws', 'amplify', 'deployment-secrets.json'); + if (!fs.existsSync(deploymentSecretsPath)) return {}; + return JSON.parse(fs.readFileSync(deploymentSecretsPath, 'utf8')); +} + +function isDeploymentSecretForEnvExists(projRoot: string, envName: string) { + const teamproviderInfo = getTeamProviderInfo(projRoot); + const ampilfyAppId = teamproviderInfo[envName].awscloudformation.AmplifyAppId; + const resource = _.first(Object.keys(teamproviderInfo[envName].categories.auth)); + const path = [ampilfyAppId, envName, 'auth', resource, 'hostedUIProviderCreds']; + return _.get(getDeploymentSecrets(), path); +} + export { getProjectMeta, getProjectTags, @@ -79,6 +95,8 @@ export { getAmplifyConfigAndroidPath, getAmplifyConfigIOSPath, getAWSConfigIOSPath, + getDeploymentSecrets, + isDeploymentSecretForEnvExists, getS3StorageBucketName, getAmplifyDirPath, getBackendConfig, diff --git a/packages/amplify-e2e-tests/src/__tests__/auth_1.test.ts b/packages/amplify-e2e-tests/src/__tests__/auth_1.test.ts index aec1db0fa16..0505ae8f0d3 100644 --- a/packages/amplify-e2e-tests/src/__tests__/auth_1.test.ts +++ b/packages/amplify-e2e-tests/src/__tests__/auth_1.test.ts @@ -1,21 +1,6 @@ import { initJSProjectWithProfile, deleteProject, amplifyPushAuth, amplifyPush } from 'amplify-e2e-core'; -import { - addAuthWithDefault, - runAmplifyAuthConsole, - removeAuthWithDefault, - addAuthWithDefaultSocial, - addAuthWithGroupTrigger, - addAuthWithRecaptchaTrigger, - addAuthViaAPIWithTrigger, -} from 'amplify-e2e-core'; -import { - createNewProjectDir, - deleteProjectDir, - getProjectMeta, - getUserPool, - getUserPoolClients, - getLambdaFunction, -} from 'amplify-e2e-core'; +import { addAuthWithDefault, runAmplifyAuthConsole, removeAuthWithDefault } from 'amplify-e2e-core'; +import { createNewProjectDir, deleteProjectDir, getProjectMeta, getUserPool } from 'amplify-e2e-core'; const defaultsSettings = { name: 'authTest', diff --git a/packages/amplify-e2e-tests/src/__tests__/auth_2.test.ts b/packages/amplify-e2e-tests/src/__tests__/auth_2.test.ts index 57a3e978414..be6227e7a89 100644 --- a/packages/amplify-e2e-tests/src/__tests__/auth_2.test.ts +++ b/packages/amplify-e2e-tests/src/__tests__/auth_2.test.ts @@ -1,18 +1,12 @@ import { initJSProjectWithProfile, deleteProject, amplifyPushAuth, amplifyPush } from 'amplify-e2e-core'; -import { - addAuthWithDefault, - removeAuthWithDefault, - addAuthWithDefaultSocial, - addAuthWithGroupTrigger, - addAuthWithRecaptchaTrigger, - addAuthViaAPIWithTrigger, -} from 'amplify-e2e-core'; +import { addAuthWithDefaultSocial, addAuthWithGroupTrigger, addAuthWithRecaptchaTrigger, addAuthViaAPIWithTrigger } from 'amplify-e2e-core'; import { createNewProjectDir, deleteProjectDir, getProjectMeta, getUserPool, getUserPoolClients, + isDeploymentSecretForEnvExists, getLambdaFunction, } from 'amplify-e2e-core'; @@ -34,9 +28,10 @@ describe('amplify add auth...', () => { it('...should init a project and add auth with defaultSocial', async () => { await initJSProjectWithProfile(projRoot, defaultsSettings); await addAuthWithDefaultSocial(projRoot, {}); + expect(isDeploymentSecretForEnvExists(projRoot, 'integtest')).toBeDefined(); await amplifyPushAuth(projRoot); const meta = getProjectMeta(projRoot); - + expect(isDeploymentSecretForEnvExists(projRoot, 'integtest')).toBeUndefined(); const authMeta = Object.keys(meta.auth).map(key => meta.auth[key])[0]; const id = authMeta.output.UserPoolId; const userPool = await getUserPool(id, meta.providers.awscloudformation.Region); diff --git a/packages/amplify-migration-tests/package.json b/packages/amplify-migration-tests/package.json index 89d08a65c9b..7f1628e5b44 100644 --- a/packages/amplify-migration-tests/package.json +++ b/packages/amplify-migration-tests/package.json @@ -18,6 +18,7 @@ "private": true, "scripts": { "migration_v4.0.0": "npm run setup-profile 4.0.0 && jest --verbose", + "migration_v4.30.0_auth": "npm run setup-profile 4.30.0 && jest auth.deployment.secrets.test.ts -t 'amplify auth add with social' --verbose", "migration": "npm run setup-profile latest && jest --verbose", "setup-profile": "ts-node ./src/configure_tests.ts" }, diff --git a/packages/amplify-migration-tests/src/__tests__/migration_tests/auth-deployment-migration/auth.deployment.secrets.test.ts b/packages/amplify-migration-tests/src/__tests__/migration_tests/auth-deployment-migration/auth.deployment.secrets.test.ts new file mode 100644 index 00000000000..88d13079495 --- /dev/null +++ b/packages/amplify-migration-tests/src/__tests__/migration_tests/auth-deployment-migration/auth.deployment.secrets.test.ts @@ -0,0 +1,39 @@ +import { + initJSProjectWithProfile, + deleteProject, + amplifyPush, + addAuthWithDefaultSocial, + isDeploymentSecretForEnvExists, + amplifyStatus, + amplifyStatusWithMigrate, + amplifyVersion, +} from 'amplify-e2e-core'; +import { createNewProjectDir, deleteProjectDir } from 'amplify-e2e-core'; + +describe('amplify auth add with social', () => { + let projRoot: string; + beforeEach(async () => { + projRoot = await createNewProjectDir('api-key-cli-migration'); + }); + + afterEach(async () => { + await deleteProject(projRoot); + deleteProjectDir(projRoot); + }); + + it('init project, add social migrate and push', async () => { + // init, add api and push with installed cli + const envName = 'integtest'; + await amplifyVersion(projRoot, '4.30.0', false); + await initJSProjectWithProfile(projRoot, {}); + await addAuthWithDefaultSocial(projRoot, {}); + expect(isDeploymentSecretForEnvExists(projRoot, envName)).toBeUndefined(); + await amplifyPush(projRoot); + expect(isDeploymentSecretForEnvExists(projRoot, envName)).toBeUndefined(); + await amplifyStatusWithMigrate(projRoot, 'No Change', true); + expect(isDeploymentSecretForEnvExists(projRoot, envName)).toBeDefined(); + await amplifyStatus(projRoot, 'Update', true); + await amplifyPush(projRoot, true); + expect(isDeploymentSecretForEnvExists(projRoot, envName)).toBeUndefined(); + }); +}); diff --git a/packages/amplify-provider-awscloudformation/src/push-resources.js b/packages/amplify-provider-awscloudformation/src/push-resources.js index b683de6c15a..9fbde8bb7ea 100644 --- a/packages/amplify-provider-awscloudformation/src/push-resources.js +++ b/packages/amplify-provider-awscloudformation/src/push-resources.js @@ -156,7 +156,14 @@ async function run(context, resourceDefinition) { // Store current cloud backend in S3 deployment bcuket await storeCurrentCloudBackend(context); await amplifyServiceManager.storeArtifactsForAmplifyService(context); - + //check for auth resources and remove deployment secret for push + const authResources = resources.filter(resource => resource.category === 'auth'); + if (authResources.length > 0) { + for (let i = 0; i < authResources.length; i++) { + const authResource = authResources[i]; + context.amplify.removeDeploymentSecrets(context, authResource.category, authResource.resourceName); + } + } spinner.succeed('All resources are updated in the cloud'); displayHelpfulURLs(context, resources);