From dbfb26bb63ce28d7a4ef608ed66717815cd44556 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 15 Apr 2020 15:38:22 +0200 Subject: [PATCH] Remove the integration between auth and sync flow --- .../userDataSync/browser/userDataSync.ts | 125 +++---- .../browser/userDataSyncAccount.ts | 209 +++++++++++ .../browser/userDataSyncAuthentication.ts | 350 ------------------ 3 files changed, 263 insertions(+), 421 deletions(-) create mode 100644 src/vs/workbench/contrib/userDataSync/browser/userDataSyncAccount.ts delete mode 100644 src/vs/workbench/contrib/userDataSync/browser/userDataSyncAuthentication.ts diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index b79b651d6ee..9bd5d190f9f 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -29,7 +29,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { - CONTEXT_SYNC_STATE, getUserDataSyncStore, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, registerConfiguration, + CONTEXT_SYNC_STATE, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, registerConfiguration, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, CONTEXT_SYNC_ENABLEMENT, SyncResourceConflicts, Conflict, getSyncResourceFromLocalPreview } from 'vs/platform/userDataSync/common/userDataSync'; @@ -42,7 +42,6 @@ import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; import { IOutputService } from 'vs/workbench/contrib/output/common/output'; import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; import { IActivityService, IBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity'; -import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; @@ -51,7 +50,8 @@ import { fromNow } from 'vs/base/common/date'; import { IProductService } from 'vs/platform/product/common/productService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { UserDataSyncAuthentication, IUserDataSyncAuthentication, AuthStatus, CONTEXT_AUTH_TOKEN_STATE } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncAuthentication'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { UserDataSyncAccountManager } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncAccount'; const CONTEXT_CONFLICTS_SOURCES = new RawContextKey('conflictsSources', ''); @@ -85,12 +85,13 @@ const getActivityTitle = (label: string, userDataSyncService: IUserDataSyncServi } return label; }; -const getIdentityTitle = (label: string, providerDisplayName?: string, accountName?: string) => { - return accountName ? `${label} (${providerDisplayName}:${accountName})` : label; +const getIdentityTitle = (label: string, userDataSyncAccountService: UserDataSyncAccountManager, authenticationService: IAuthenticationService) => { + const activeAccount = userDataSyncAccountService.activeAccount; + return activeAccount ? `${label} (${authenticationService.getDisplayName(activeAccount.providerId)}:${activeAccount.accountName})` : label; }; const turnOnSyncCommand = { id: 'workbench.userData.actions.syncStart', title: localize('turn on sync with category', "Preferences Sync: Turn on...") }; const signInCommand = { id: 'workbench.userData.actions.signin', title: localize('sign in', "Preferences Sync: Sign in to sync") }; -const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title(providerName: string | undefined, accountName: string | undefined) { return getIdentityTitle(localize('stop sync', "Preferences Sync: Turn Off"), providerName, accountName); } }; +const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title(userDataSyncAccountService: UserDataSyncAccountManager, authenticationService: IAuthenticationService) { return getIdentityTitle(localize('stop sync', "Preferences Sync: Turn Off"), userDataSyncAccountService, authenticationService); } }; const resolveSettingsConflictsCommand = { id: 'workbench.userData.actions.resolveSettingsConflicts', title: localize('showConflicts', "Preferences Sync: Show Settings Conflicts") }; const resolveKeybindingsConflictsCommand = { id: 'workbench.userData.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "Preferences Sync: Show Keybindings Conflicts") }; const resolveSnippetsConflictsCommand = { id: 'workbench.userData.actions.resolveSnippetsConflicts', title: localize('showSnippetsConflicts', "Preferences Sync: Show User Snippets Conflicts") }; @@ -102,26 +103,29 @@ const showSyncActivityCommand = { }; const showSyncSettingsCommand = { id: 'workbench.userData.actions.syncSettings', title: localize('sync settings', "Preferences Sync: Show Settings"), }; +const CONTEXT_ACTIVE_ACCOUNT_STATE = new RawContextKey('activeAccountStatus', SyncStatus.Uninitialized); +const enum ActiveAccountStatus { + Uninitialized = 'uninitialized', + Active = 'active', + Inactive = 'inactive' +} + export class UserDataSyncWorkbenchContribution extends Disposable implements IWorkbenchContribution { private readonly syncEnablementContext: IContextKey; private readonly syncStatusContext: IContextKey; - + private readonly activeAccountStatusContext: IContextKey; private readonly conflictsSources: IContextKey; + private readonly userDataSyncAccountManager: UserDataSyncAccountManager; private readonly badgeDisposable = this._register(new MutableDisposable()); - private readonly signInNotificationDisposable = this._register(new MutableDisposable()); - - private readonly userDataSyncAuthentication: IUserDataSyncAuthentication | undefined; constructor( @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, - @IAuthenticationService authenticationService: IAuthenticationService, @IContextKeyService contextKeyService: IContextKeyService, @IActivityService private readonly activityService: IActivityService, @INotificationService private readonly notificationService: INotificationService, - @IConfigurationService configurationService: IConfigurationService, @IEditorService private readonly editorService: IEditorService, @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, @IDialogService private readonly dialogService: IDialogService, @@ -136,48 +140,49 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo @IProductService private readonly productService: IProductService, @IStorageService private readonly storageService: IStorageService, @IOpenerService private readonly openerService: IOpenerService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, ) { super(); - const userDataSyncStore = getUserDataSyncStore(productService, configurationService); + this.syncEnablementContext = CONTEXT_SYNC_ENABLEMENT.bindTo(contextKeyService); this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService); - + this.activeAccountStatusContext = CONTEXT_ACTIVE_ACCOUNT_STATE.bindTo(contextKeyService); this.conflictsSources = CONTEXT_CONFLICTS_SOURCES.bindTo(contextKeyService); - if (userDataSyncStore) { + + this.userDataSyncAccountManager = instantiationService.createInstance(UserDataSyncAccountManager); + + if (this.userDataSyncAccountManager.userDataSyncAccountProvider) { registerConfiguration(); - this.userDataSyncAuthentication = new UserDataSyncAuthentication(userDataSyncStore.authenticationProviderId, - authenticationService, - contextKeyService, - this.notificationService, - quickInputService, - authTokenService, - telemetryService, - storageService, - productService, - userDataSyncEnablementService); this.onDidChangeSyncStatus(this.userDataSyncService.status); this.onDidChangeConflicts(this.userDataSyncService.conflicts); this.onDidChangeEnablement(this.userDataSyncEnablementService.isEnabled()); + this.onDidChangeActiveAccount(); + this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeSyncStatus(this.userDataSyncService.status))); this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflicts))); this._register(userDataSyncService.onSyncErrors(errors => this.onSyncErrors(errors))); this._register(this.userDataSyncEnablementService.onDidChangeEnablement(enabled => this.onDidChangeEnablement(enabled))); this._register(userDataAutoSyncService.onError(error => this.onAutoSyncError(error))); - this._register(this.userDataSyncAuthentication.onDidChangeActiveAccount(_ => this.updateBadge())); - this._register(this.userDataSyncAuthentication.onAccountsAvailable(_ => this.turnOn(true))); + this._register(this.userDataSyncAccountManager.onDidChangeActiveAccount(() => this.onDidChangeActiveAccount())); this.registerActions(); - this.userDataSyncAuthentication.initializeActiveAccount().then(_ => { - if (!isWeb) { - this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => userDataAutoSyncService.triggerAutoSync([source]))); - } - }); textModelResolverService.registerTextModelContentProvider(USER_DATA_SYNC_SCHEME, instantiationService.createInstance(UserDataRemoteContentProvider)); registerEditorContribution(AcceptChangesContribution.ID, AcceptChangesContribution); + + if (!isWeb) { + this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => userDataAutoSyncService.triggerAutoSync([source]))); + } } } + private onDidChangeActiveAccount(): void { + const activeAccount = this.userDataSyncAccountManager.activeAccount; + this.activeAccountStatusContext.set(activeAccount === undefined ? ActiveAccountStatus.Uninitialized + : activeAccount === null ? ActiveAccountStatus.Inactive : ActiveAccountStatus.Active); + this.updateBadge(); + } + private onDidChangeSyncStatus(status: SyncStatus) { this.syncStatusContext.set(status); this.updateBadge(); @@ -294,21 +299,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private onDidChangeEnablement(enabled: boolean) { this.syncEnablementContext.set(enabled); this.updateBadge(); - if (enabled) { - if (this.userDataSyncAuthentication?.authenticationState === AuthStatus.SignedOut) { - const handle = this.notificationService.prompt(Severity.Info, localize('sign in message', "Please sign in with your {0} account to continue sync", this.userDataSyncAuthentication?.providerDisplayName), - [ - { - label: localize('Sign in', "Sign in"), - run: () => this.userDataSyncAuthentication!.login() - } - ]); - this.signInNotificationDisposable.value = toDisposable(() => handle.close()); - handle.onDidClose(() => this.signInNotificationDisposable.clear()); - } - } else { - this.signInNotificationDisposable.clear(); - } } private onAutoSyncError(error: UserDataSyncError): void { @@ -404,7 +394,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo let clazz: string | undefined; let priority: number | undefined = undefined; - if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.userDataSyncEnablementService.isEnabled() && this.userDataSyncAuthentication?.authenticationState === AuthStatus.SignedOut) { + if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.userDataSyncEnablementService.isEnabled() && this.userDataSyncAccountManager.activeAccount === null) { badge = new NumberBadge(1, () => localize('sign in to sync', "Sign in to Sync")); } else if (this.userDataSyncService.conflicts.length) { badge = new NumberBadge(this.userDataSyncService.conflicts.reduce((result, syncResourceConflict) => { return result + syncResourceConflict.conflicts.length; }, 0), () => localize('has conflicts', "Preferences Sync: Conflicts Detected")); @@ -415,7 +405,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private async turnOn(skipAccountPick?: boolean): Promise { + private async turnOn(): Promise { if (!this.storageService.getBoolean('sync.donotAskPreviewConfirmation', StorageScope.GLOBAL, false)) { const result = await this.dialogService.show( Severity.Info, @@ -433,15 +423,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo case 0: this.openerService.open(URI.parse('https://aka.ms/vscode-settings-sync-help')); return; case 2: return; } - } else if (skipAccountPick) { - const result = await this.dialogService.confirm({ - type: 'info', - message: localize('turn on sync confirmation', "Do you want to turn on preferences sync?"), - primaryButton: localize('turn on', "Turn On") - }); - if (!result.confirmed) { - return; - } } return new Promise((c, e) => { @@ -451,10 +432,10 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo quickPick.title = localize('turn on title', "Preferences Sync: Turn On"); quickPick.ok = false; quickPick.customButton = true; - if (this.userDataSyncAuthentication?.authenticationState === AuthStatus.SignedIn) { + if (this.userDataSyncAccountManager.activeAccount) { quickPick.customLabel = localize('turn on', "Turn On"); } else { - const displayName = this.userDataSyncAuthentication?.providerDisplayName; + const displayName = this.authenticationService.getDisplayName(this.userDataSyncAccountManager.userDataSyncAccountProvider!); quickPick.description = localize('sign in and turn on sync detail', "Sign in with your {0} account to synchronize your data across devices.", displayName); quickPick.customLabel = localize('sign in and turn on sync', "Sign in & Turn on"); } @@ -467,7 +448,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo disposables.add(Event.any(quickPick.onDidAccept, quickPick.onDidCustom)(async () => { if (quickPick.selectedItems.length) { this.updateConfiguration(items, quickPick.selectedItems); - this.doTurnOn(skipAccountPick).then(c, e); + this.doTurnOn().then(c, e); quickPick.hide(); } })); @@ -476,14 +457,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); } - private async doTurnOn(skipAccountPick?: boolean): Promise { + private async doTurnOn(): Promise { // If this was not triggered by signing in from the accounts menu, show accounts list - if (!skipAccountPick) { - await this.userDataSyncAuthentication!.confirmActiveAccount(); + if (this.userDataSyncAccountManager.activeAccount) { + await this.userDataSyncAccountManager.select(); + } else { + await this.userDataSyncAccountManager.login(); } // User did not pick an account or login failed, no need to continue - if (this.userDataSyncAuthentication!.authenticationState !== AuthStatus.SignedIn) { + if (!this.userDataSyncAccountManager.activeAccount) { return; } @@ -681,7 +664,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private registerTurnOnSyncAction(): void { - const turnOnSyncWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT.toNegated(), CONTEXT_AUTH_TOKEN_STATE.notEqualsTo(AuthStatus.Initializing)); + const turnOnSyncWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT.toNegated(), CONTEXT_ACTIVE_ACCOUNT_STATE.notEqualsTo(ActiveAccountStatus.Uninitialized)); CommandsRegistry.registerCommand(turnOnSyncCommand.id, async () => { try { await this.turnOn(); @@ -724,14 +707,14 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo menu: { group: '5_sync', id: MenuId.GlobalActivity, - when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT, CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthStatus.SignedOut)), + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACTIVE_ACCOUNT_STATE.isEqualTo(ActiveAccountStatus.Inactive)), order: 2 }, }); } async run(): Promise { try { - await that.userDataSyncAuthentication?.login(); + await that.userDataSyncAccountManager.login(); } catch (e) { that.notificationService.error(e); } @@ -825,7 +808,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private registerSyncStatusAction(): void { const that = this; - const when = ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthStatus.SignedIn), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)); + const when = ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACTIVE_ACCOUNT_STATE.isEqualTo(ActiveAccountStatus.Active), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)); this._register(registerAction2(class SyncStatusAction extends Action2 { constructor() { super({ @@ -875,7 +858,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo items.push({ id: showSyncSettingsCommand.id, label: showSyncSettingsCommand.title }); items.push({ id: showSyncActivityCommand.id, label: showSyncActivityCommand.title(that.userDataSyncService) }); items.push({ type: 'separator' }); - items.push({ id: stopSyncCommand.id, label: stopSyncCommand.title(that.userDataSyncAuthentication!.providerDisplayName, that.userDataSyncAuthentication!.activeAccountName) }); + items.push({ id: stopSyncCommand.id, label: stopSyncCommand.title(that.userDataSyncAccountManager, that.authenticationService) }); quickPick.items = items; disposables.add(quickPick.onDidAccept(() => { if (quickPick.selectedItems[0] && quickPick.selectedItems[0].id) { @@ -899,7 +882,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo constructor() { super({ id: stopSyncCommand.id, - title: stopSyncCommand.title(that.userDataSyncAuthentication!.providerDisplayName, that.userDataSyncAuthentication!.activeAccountName), + title: stopSyncCommand.title(that.userDataSyncAccountManager, that.authenticationService), menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT), diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncAccount.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncAccount.ts new file mode 100644 index 00000000000..0541a68d9af --- /dev/null +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncAccount.ts @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { localize } from 'vs/nls'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { AuthenticationSession } from 'vs/editor/common/modes'; +import { Event, Emitter } from 'vs/base/common/event'; +import { getUserDataSyncStore, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { distinct } from 'vs/base/common/arrays'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; + +type UserAccountClassification = { + id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' }; +}; + +type UserAccountEvent = { + id: string; +}; + +export interface IUserDataSyncAccount { + providerId: string; + sessionId: string; + accountName: string; +} + +export class UserDataSyncAccountManager extends Disposable { + + private static LAST_USED_SESSION_STORAGE_KEY = 'userDataSyncAccountPreference'; + + _serviceBrand: any; + + readonly userDataSyncAccountProvider: string | undefined; + + private _activeAccount: IUserDataSyncAccount | undefined | null; + get activeAccount(): IUserDataSyncAccount | undefined | null { return this._activeAccount; } + private readonly _onDidChangeActiveAccount = this._register(new Emitter<{ previous: IUserDataSyncAccount | undefined | null, current: IUserDataSyncAccount | null }>()); + readonly onDidChangeActiveAccount = this._onDidChangeActiveAccount.event; + + constructor( + @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IAuthenticationTokenService private readonly authenticationTokenService: IAuthenticationTokenService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IStorageService private readonly storageService: IStorageService, + @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(); + this.userDataSyncAccountProvider = getUserDataSyncStore(productService, configurationService)?.authenticationProviderId; + if (this.userDataSyncAccountProvider) { + this.update(); + this._register( + Event.any( + Event.filter( + Event.any( + this.authenticationService.onDidRegisterAuthenticationProvider, + this.authenticationService.onDidUnregisterAuthenticationProvider, + Event.map(this.authenticationService.onDidChangeSessions, e => e.providerId) + ), providerId => providerId === this.userDataSyncAccountProvider), + authenticationTokenService.onTokenFailed) + (() => this.update())); + } + } + + private async update(): Promise { + if (!this.userDataSyncAccountProvider) { + return; + } + let activeSession: AuthenticationSession | undefined = undefined; + if (this.lastUsedSessionId) { + const sessions = await this.authenticationService.getSessions(this.userDataSyncAccountProvider); + if (sessions?.length) { + activeSession = sessions.find(session => session.id === this.lastUsedSessionId); + } + } + + let activeAccount: IUserDataSyncAccount | null = null; + if (activeSession) { + try { + const token = await activeSession.getAccessToken(); + await this.authenticationTokenService.setToken(token); + activeAccount = { + providerId: this.userDataSyncAccountProvider, + sessionId: activeSession.id, + accountName: activeSession.accountName + }; + } catch (e) { + // Ignore and log error + } + } + + if (!this.areSameAccounts(activeAccount, this._activeAccount)) { + const previous = this._activeAccount; + this._activeAccount = activeAccount; + this._onDidChangeActiveAccount.fire({ previous, current: this._activeAccount }); + } + } + + async login(): Promise { + if (this.userDataSyncAccountProvider) { + const session = await this.authenticationService.login(this.userDataSyncAccountProvider, ['https://management.core.windows.net/.default', 'offline_access']); + await this.switch(session.id); + } + } + + async select(): Promise { + if (!this.activeAccount) { + throw new Error('Requires Login'); + } + await this.update(); + if (!this.activeAccount) { + throw new Error('Requires Login'); + } + const { providerId, sessionId } = this.activeAccount; + await new Promise(async (c, e) => { + const disposables: DisposableStore = new DisposableStore(); + const quickPick = this.quickInputService.createQuickPick<{ label: string, session?: AuthenticationSession, detail?: string }>(); + disposables.add(quickPick); + + quickPick.title = localize('pick account', "{0}: Pick an account", this.authenticationService.getDisplayName(providerId)); + quickPick.ok = false; + quickPick.placeholder = localize('choose account placeholder', "Pick an account for syncing"); + quickPick.ignoreFocusOut = true; + disposables.add(quickPick.onDidAccept(async () => { + const selected = quickPick.selectedItems[0]; + if (selected) { + if (selected.session) { + await this.switch(selected.session.id); + } else { + await this.login(); + } + quickPick.hide(); + c(); + } + })); + disposables.add(quickPick.onDidHide(() => disposables.dispose())); + quickPick.show(); + + quickPick.busy = true; + quickPick.items = await this.getSessionQuickPickItems(providerId, sessionId); + quickPick.busy = false; + + }); + } + + async switch(sessionId: string): Promise { + if (this.userDataSyncEnablementService.isEnabled() && (this.lastUsedSessionId && this.lastUsedSessionId !== sessionId)) { + // accounts are switched while sync is enabled. + } + this.lastUsedSessionId = sessionId; + this.telemetryService.publicLog2('sync.userAccount', { id: sessionId.split('/')[1] }); + await this.update(); + } + + private async getSessionQuickPickItems(providerId: string, sessionId: string): Promise<{ label: string, session?: AuthenticationSession, detail?: string }[]> { + const quickPickItems: { label: string, session?: AuthenticationSession, detail?: string }[] = []; + + let sessions = await this.authenticationService.getSessions(providerId) || []; + const lastUsedSession = sessions.filter(session => session.id === sessionId)[0]; + + if (lastUsedSession) { + sessions = sessions.filter(session => session.accountName !== lastUsedSession.accountName); + quickPickItems.push({ + label: lastUsedSession.accountName, + session: lastUsedSession, + detail: localize('previously used', "Last used") + }); + } + + quickPickItems.push(...distinct(sessions, session => session.accountName).map(session => ({ label: session.accountName, session }))); + quickPickItems.push({ label: localize('choose another', "Use another account") }); + return quickPickItems; + } + + private get lastUsedSessionId(): string | undefined { + return this.storageService.get(UserDataSyncAccountManager.LAST_USED_SESSION_STORAGE_KEY, StorageScope.GLOBAL); + } + + private set lastUsedSessionId(lastUserSessionId: string | undefined) { + if (lastUserSessionId === undefined) { + this.storageService.remove(UserDataSyncAccountManager.LAST_USED_SESSION_STORAGE_KEY, StorageScope.GLOBAL); + } else { + this.storageService.store(UserDataSyncAccountManager.LAST_USED_SESSION_STORAGE_KEY, lastUserSessionId, StorageScope.GLOBAL); + } + } + + private areSameAccounts(a: IUserDataSyncAccount | undefined | null, b: IUserDataSyncAccount | undefined | null): boolean { + if (a === b) { + return true; + } + if (a && b + && a.providerId === b.providerId + && a.sessionId === b.sessionId + ) { + return true; + } + return false; + } + +} diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncAuthentication.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncAuthentication.ts deleted file mode 100644 index eb907922658..00000000000 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncAuthentication.ts +++ /dev/null @@ -1,350 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; -import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { localize } from 'vs/nls'; -import { distinct } from 'vs/base/common/arrays'; -import Severity from 'vs/base/common/severity'; -import { Action } from 'vs/base/common/actions'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes'; -import { Emitter, Event } from 'vs/base/common/event'; -import { IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; - -export interface IUserDataSyncAuthenticationService { - _serviceBrand: undefined; -} - -export const enum AuthStatus { - Initializing = 'Initializing', - SignedIn = 'SignedIn', // Signed in indicates that there is an active account - SignedOut = 'SignedOut', - Unavailable = 'Unavailable' -} - -export const CONTEXT_AUTH_TOKEN_STATE = new RawContextKey('authTokenStatus', AuthStatus.Initializing); -const USER_DATA_SYNC_ACCOUNT_PREFERENCE_KEY = 'userDataSyncAccountPreference'; - -export interface IUserDataSyncAuthentication { - providerDisplayName: string | undefined; - activeAccountName: string | undefined; - authenticationState: AuthStatus; - - initializeActiveAccount(): Promise; - confirmActiveAccount(): Promise; - - login(): Promise; - logout(): Promise; - - readonly onAccountsAvailable: Event; - readonly onDidChangeActiveAccount: Event; -} -export class UserDataSyncAuthentication extends Disposable implements IUserDataSyncAuthentication { - private readonly _authenticationState: IContextKey; - private _activeAccount: AuthenticationSession | undefined; - private loginInProgress: boolean = false; - - private _onDidChangeActiveAccount: Emitter = this._register(new Emitter()); - readonly onDidChangeActiveAccount: Event = this._onDidChangeActiveAccount.event; - - private _onAccountsAvailable: Emitter = this._register(new Emitter()); - readonly onAccountsAvailable: Event = this._onAccountsAvailable.event; - - constructor( - private readonly _authenticationProviderId: string, - @IAuthenticationService private readonly authenticationService: IAuthenticationService, - @IContextKeyService contextKeyService: IContextKeyService, - @INotificationService private readonly notificationService: INotificationService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @IAuthenticationTokenService private readonly authTokenService: IAuthenticationTokenService, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @IStorageService private readonly storageService: IStorageService, - @IProductService readonly productService: IProductService, - @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService - ) { - super(); - this._authenticationState = CONTEXT_AUTH_TOKEN_STATE.bindTo(contextKeyService); - this._register(this.authTokenService.onTokenFailed(_ => this.onTokenFailed())); - this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => this.onDidRegisterAuthenticationProvider(e))); - this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => this.onDidUnregisterAuthenticationProvider(e))); - this._register(this.authenticationService.onDidChangeSessions(e => this.onDidChangeSessions(e))); - } - - get providerDisplayName(): string | undefined { - try { - return this.authenticationService.getDisplayName(this._authenticationProviderId); - } catch (e) { - // Ignore, provider is not yet registered - return undefined; - } - } - - get authenticationState(): AuthStatus { - return this._authenticationState.get()!; - } - - get activeAccountName(): string | undefined { - return this.activeAccount?.accountName; - } - - async initializeActiveAccount(): Promise { - const sessions = await this.authenticationService.getSessions(this._authenticationProviderId); - // Auth provider has not yet been registered - if (!sessions) { - return; - } - - if (!this.userDataSyncEnablementService.isEnabled()) { - this.setActiveAccount(undefined); - } - - if (sessions.length === 0) { - await this.setActiveAccount(undefined); - return; - } - - if (sessions.length === 1) { - this.logAuthenticatedEvent(sessions[0]); - await this.setActiveAccount(sessions[0]); - return; - } - - const accountPreference = this.storageService.get(USER_DATA_SYNC_ACCOUNT_PREFERENCE_KEY, StorageScope.GLOBAL); - if (accountPreference) { - const matchingSession = sessions.find(session => session.id === accountPreference); - if (matchingSession) { - this.setActiveAccount(matchingSession); - return; - } - } - - await this.showSwitchAccountPicker(sessions); - } - - async confirmActiveAccount(): Promise { - const sessions = await this.authenticationService.getSessions(this._authenticationProviderId) || []; - if (sessions.length) { - await new Promise((resolve, _) => { - const disposables: DisposableStore = new DisposableStore(); - const quickPick = this.quickInputService.createQuickPick<{ id: string, label: string, session?: AuthenticationSession, detail?: string }>(); - disposables.add(quickPick); - - quickPick.title = localize('pick account', "{0}: Pick an account", this.providerDisplayName); - quickPick.ok = false; - quickPick.placeholder = localize('choose account placeholder', "Pick an account for syncing"); - quickPick.ignoreFocusOut = true; - - const chooseAnotherItemId = 'chooseAnother'; - const accountPreference = this.storageService.get(USER_DATA_SYNC_ACCOUNT_PREFERENCE_KEY, StorageScope.GLOBAL); - - // Move previously used account to first item - const orderedSessions = sessions.slice().sort(session => { - if (session.id === accountPreference) { - return -1; - } else { - return 0; - } - }); - - quickPick.items = orderedSessions.map(session => { - return { - id: session.id, - label: session.accountName, - session: session, - detail: session.id === accountPreference ? localize('previously used', "Previously used") : '' - }; - }).concat([{ - id: chooseAnotherItemId, - label: localize('choose another', "Use another account") - } as any]); - - disposables.add(quickPick.onDidAccept(async () => { - const selected = quickPick.selectedItems[0]; - if (selected) { - if (selected.id === chooseAnotherItemId) { - this.login(); - } else { - this.setActiveAccount(selected.session); - } - - quickPick.hide(); - resolve(); - } - })); - - disposables.add(quickPick.onDidHide(() => disposables.dispose())); - quickPick.show(); - }); - } else { - await this.login(); - } - } - - async login(): Promise { - try { - this.loginInProgress = true; - await this.setActiveAccount(await this.authenticationService.login(this._authenticationProviderId, ['https://management.core.windows.net/.default', 'offline_access'])); - this.loginInProgress = false; - } catch (e) { - this.notificationService.error(localize('loginFailed', "Logging in failed: {0}", e.message)); - } - } - - async logout(): Promise { - if (this.activeAccount) { - await this.authenticationService.logout(this._authenticationProviderId, this.activeAccount.id); - } - } - - private logAuthenticatedEvent(session: AuthenticationSession): void { - type UserAuthenticatedClassification = { - id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' }; - }; - - type UserAuthenticatedEvent = { - id: string; - }; - - const id = session.id.split('/')[1]; - this.telemetryService.publicLog2('user.authenticated', { id }); - } - - get activeAccount(): AuthenticationSession | undefined { - return this._activeAccount; - } - - async setActiveAccount(account: AuthenticationSession | undefined) { - this._activeAccount = account; - - if (account) { - try { - const token = await account.getAccessToken(); - this.authTokenService.setToken(token); - this.storageService.store(USER_DATA_SYNC_ACCOUNT_PREFERENCE_KEY, account.id, StorageScope.GLOBAL); - this._authenticationState.set(AuthStatus.SignedIn); - } catch (e) { - this.authTokenService.setToken(undefined); - this._authenticationState.set(AuthStatus.Unavailable); - } - } else { - this.authTokenService.setToken(undefined); - this._authenticationState.set(AuthStatus.SignedOut); - } - - this._onDidChangeActiveAccount.fire(); - } - - private async showSwitchAccountPicker(sessions: readonly AuthenticationSession[]): Promise { - return new Promise((resolve, _) => { - const quickPick = this.quickInputService.createQuickPick<{ label: string, session: AuthenticationSession }>(); - quickPick.title = localize('chooseAccountTitle', "Preferences Sync: Choose Account"); - quickPick.placeholder = localize('chooseAccount', "Choose an account you would like to use for preferences sync"); - const dedupedSessions = distinct(sessions, (session) => session.accountName); - quickPick.items = dedupedSessions.map(session => { - return { - label: session.accountName, - session: session - }; - }); - - quickPick.onDidHide(() => { - quickPick.dispose(); - resolve(); - }); - - quickPick.onDidAccept(() => { - const selected = quickPick.selectedItems[0]; - this.setActiveAccount(selected.session); - quickPick.dispose(); - resolve(); - }); - - quickPick.show(); - }); - } - - private async onDidChangeSessions(e: { providerId: string, event: AuthenticationSessionsChangeEvent }): Promise { - const { providerId, event } = e; - if (!this.userDataSyncEnablementService.isEnabled()) { - if (event.added) { - this._onAccountsAvailable.fire(); - } - return; - } - - if (providerId === this._authenticationProviderId) { - if (this.loginInProgress) { - return; - } - - if (this.activeAccount) { - if (event.removed.length) { - const activeWasRemoved = !!event.removed.find(removed => removed === this.activeAccount!.id); - if (activeWasRemoved) { - this.setActiveAccount(undefined); - this.notificationService.notify({ - severity: Severity.Info, - message: localize('turned off on logout', "Sync has stopped because you are no longer signed in."), - actions: { - primary: [new Action('sign in', localize('sign in', "Sign in"), undefined, true, () => this.login())] - } - }); - return; - } - } - - if (event.added.length) { - // Offer to switch accounts - const accounts = (await this.authenticationService.getSessions(this._authenticationProviderId) || []); - await this.showSwitchAccountPicker(accounts); - return; - } - - if (event.changed.length) { - const activeWasChanged = !!event.changed.find(changed => changed === this.activeAccount!.id); - if (activeWasChanged) { - // Try to update existing account, case where access token has been refreshed - const accounts = (await this.authenticationService.getSessions(this._authenticationProviderId) || []); - const matchingAccount = accounts.filter(a => a.id === this.activeAccount?.id)[0]; - this.setActiveAccount(matchingAccount); - } - } - } else { - this.initializeActiveAccount(); - } - } - } - - private async onTokenFailed(): Promise { - if (this.activeAccount) { - const accounts = (await this.authenticationService.getSessions(this._authenticationProviderId) || []); - const matchingAccount = accounts.filter(a => a.id === this.activeAccount?.id)[0]; - this.setActiveAccount(matchingAccount); - } else { - this.setActiveAccount(undefined); - } - } - - private async onDidRegisterAuthenticationProvider(providerId: string) { - if (providerId === this._authenticationProviderId) { - await this.initializeActiveAccount(); - } - } - - private onDidUnregisterAuthenticationProvider(providerId: string) { - if (providerId === this._authenticationProviderId) { - this.setActiveAccount(undefined); - this._authenticationState.reset(); - } - } -} -