Remove the integration between auth and sync flow

This commit is contained in:
Sandeep Somavarapu
2020-04-15 15:38:22 +02:00
parent 090081d747
commit dbfb26bb63
3 changed files with 263 additions and 421 deletions
@@ -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<string>('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<string>('activeAccountStatus', SyncStatus.Uninitialized);
const enum ActiveAccountStatus {
Uninitialized = 'uninitialized',
Active = 'active',
Inactive = 'inactive'
}
export class UserDataSyncWorkbenchContribution extends Disposable implements IWorkbenchContribution {
private readonly syncEnablementContext: IContextKey<boolean>;
private readonly syncStatusContext: IContextKey<string>;
private readonly activeAccountStatusContext: IContextKey<string>;
private readonly conflictsSources: IContextKey<string>;
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<void> {
private async turnOn(): Promise<void> {
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<void> {
private async doTurnOn(): Promise<void> {
// 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<any> {
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),
@@ -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<void> {
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<void> {
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<void> {
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<void> {
if (this.userDataSyncEnablementService.isEnabled() && (this.lastUsedSessionId && this.lastUsedSessionId !== sessionId)) {
// accounts are switched while sync is enabled.
}
this.lastUsedSessionId = sessionId;
this.telemetryService.publicLog2<UserAccountEvent, UserAccountClassification>('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;
}
}
@@ -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<AuthStatus>('authTokenStatus', AuthStatus.Initializing);
const USER_DATA_SYNC_ACCOUNT_PREFERENCE_KEY = 'userDataSyncAccountPreference';
export interface IUserDataSyncAuthentication {
providerDisplayName: string | undefined;
activeAccountName: string | undefined;
authenticationState: AuthStatus;
initializeActiveAccount(): Promise<void>;
confirmActiveAccount(): Promise<void>;
login(): Promise<void>;
logout(): Promise<void>;
readonly onAccountsAvailable: Event<void>;
readonly onDidChangeActiveAccount: Event<void>;
}
export class UserDataSyncAuthentication extends Disposable implements IUserDataSyncAuthentication {
private readonly _authenticationState: IContextKey<AuthStatus>;
private _activeAccount: AuthenticationSession | undefined;
private loginInProgress: boolean = false;
private _onDidChangeActiveAccount: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeActiveAccount: Event<void> = this._onDidChangeActiveAccount.event;
private _onAccountsAvailable: Emitter<void> = this._register(new Emitter<void>());
readonly onAccountsAvailable: Event<void> = 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<void> {
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<void> {
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<void> {
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<void> {
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<UserAuthenticatedEvent, UserAuthenticatedClassification>('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<void> {
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<void> {
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<void> {
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();
}
}
}