userDataSyncAccount.ts 13.5 KB
Newer Older
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  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';
7
import { IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
8 9
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
import { IProductService } from 'vs/platform/product/common/productService';
10
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
11 12
import { localize } from 'vs/nls';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
13
import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes';
14
import { Event, Emitter } from 'vs/base/common/event';
15
import { getUserDataSyncStore, IUserDataSyncEnablementService, IAuthenticationProvider, isAuthenticationProvider } from 'vs/platform/userDataSync/common/userDataSync';
16 17
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
18 19
import { values } from 'vs/base/common/map';
import { ILogService } from 'vs/platform/log/common/log';
20 21
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { flatten } from 'vs/base/common/arrays';
S
Sandeep Somavarapu 已提交
22
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
23 24 25 26 27 28 29 30 31

type UserAccountClassification = {
	id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' };
};

type UserAccountEvent = {
	id: string;
};

32 33
type AccountQuickPickItem = { label: string, authenticationProvider: IAuthenticationProvider, account?: IUserDataSyncAccount, detail?: string };

34
export interface IUserDataSyncAccount {
35
	readonly authenticationProviderId: string;
36 37
	readonly sessionId: string;
	readonly accountName: string;
38 39
}

40 41 42
export const enum AccountStatus {
	Uninitialized = 'uninitialized',
	Unavailable = 'unavailable',
S
Sandeep Somavarapu 已提交
43
	Available = 'available',
44 45 46
}

export class UserDataSyncAccounts extends Disposable {
47

48
	private static DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY = 'userDataSyncAccount.donotUseWorkbenchSession';
49
	private static CACHED_SESSION_STORAGE_KEY = 'userDataSyncAccountPreference';
50 51 52

	_serviceBrand: any;

53
	readonly authenticationProviders: IAuthenticationProvider[];
54 55 56 57 58

	private _status: AccountStatus = AccountStatus.Uninitialized;
	get status(): AccountStatus { return this._status; }
	private readonly _onDidChangeStatus = this._register(new Emitter<AccountStatus>());
	readonly onDidChangeStatus = this._onDidChangeStatus.event;
59

60 61 62
	private readonly _onDidSignOut = this._register(new Emitter<void>());
	readonly onDidSignOut = this._onDidSignOut.event;

63 64
	private _all: Map<string, IUserDataSyncAccount[]> = new Map<string, IUserDataSyncAccount[]>();
	get all(): IUserDataSyncAccount[] { return flatten(values(this._all)); }
65

66
	get current(): IUserDataSyncAccount | undefined { return this.all.filter(account => this.isCurrentAccount(account))[0]; }
67 68 69 70 71 72 73 74

	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,
75
		@ILogService private readonly logService: ILogService,
76 77
		@IProductService productService: IProductService,
		@IConfigurationService configurationService: IConfigurationService,
78
		@IExtensionService extensionService: IExtensionService,
79
		@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
80 81
	) {
		super();
82 83 84
		this.authenticationProviders = getUserDataSyncStore(productService, configurationService)?.authenticationProviders || [];
		if (this.authenticationProviders.length) {
			extensionService.whenInstalledExtensionsRegistered().then(() => {
85
				if (this.authenticationProviders.every(({ id }) => authenticationService.isAuthenticationProviderRegistered(id))) {
86 87
					this.initialize();
				} else {
88 89 90 91 92 93
					const disposable = this.authenticationService.onDidRegisterAuthenticationProvider(() => {
						if (this.authenticationProviders.every(({ id }) => authenticationService.isAuthenticationProviderRegistered(id))) {
							disposable.dispose();
							this.initialize();
						}
					});
94 95
				}
			});
96 97 98
		}
	}

99
	private async initialize(): Promise<void> {
S
Sandeep Somavarapu 已提交
100 101
		if (this.currentSessionId === undefined && this.useWorkbenchSessionId && this.environmentService.options?.authenticationSessionId) {
			this.currentSessionId = this.environmentService.options.authenticationSessionId;
102 103 104
			this.useWorkbenchSessionId = false;
		}

105
		await this.update();
106

107 108 109 110 111 112
		this._register(
			Event.any(
				Event.filter(
					Event.any(
						this.authenticationService.onDidRegisterAuthenticationProvider,
						this.authenticationService.onDidUnregisterAuthenticationProvider,
113
					), authenticationProviderId => this.isSupportedAuthenticationProviderId(authenticationProviderId)),
114 115
				this.authenticationTokenService.onTokenFailed)
				(() => this.update()));
116

117
		this._register(Event.filter(this.authenticationService.onDidChangeSessions, e => this.isSupportedAuthenticationProviderId(e.providerId))(({ event }) => this.onDidChangeSessions(event)));
118
		this._register(this.storageService.onDidChangeStorage(e => this.onDidChangeStorage(e)));
119 120
	}

121 122 123
	private isSupportedAuthenticationProviderId(authenticationProviderId: string): boolean {
		return this.authenticationProviders.some(({ id }) => id === authenticationProviderId);
	}
124

125 126 127 128 129
	private async update(): Promise<void> {
		const allAccounts: Map<string, IUserDataSyncAccount[]> = new Map<string, IUserDataSyncAccount[]>();
		for (const { id } of this.authenticationProviders) {
			const accounts = await this.getAccounts(id);
			allAccounts.set(id, accounts);
130 131
		}

132 133
		this._all = allAccounts;
		const status = this.current ? AccountStatus.Available : AccountStatus.Unavailable;
134

S
Sandeep Somavarapu 已提交
135 136 137 138
		if (this._status === AccountStatus.Unavailable) {
			await this.authenticationTokenService.setToken(undefined);
		}

139
		if (this._status !== status) {
140 141 142 143 144 145 146
			const previous = this._status;
			this.logService.debug('Sync account status changed', previous, status);

			if (previous === AccountStatus.Available && status === AccountStatus.Unavailable) {
				this._onDidSignOut.fire();
			}

147 148
			this._status = status;
			this._onDidChangeStatus.fire(status);
149 150 151
		}
	}

152 153 154 155 156 157 158 159
	private async getAccounts(authenticationProviderId: string): Promise<IUserDataSyncAccount[]> {

		let accounts: Map<string, IUserDataSyncAccount> = new Map<string, IUserDataSyncAccount>();
		let currentAccount: IUserDataSyncAccount | null = null;
		let currentSession: AuthenticationSession | undefined = undefined;

		const sessions = await this.authenticationService.getSessions(authenticationProviderId) || [];
		for (const session of sessions) {
160
			const account: IUserDataSyncAccount = { authenticationProviderId, sessionId: session.id, accountName: session.account.displayName };
161
			accounts.set(account.accountName, account);
162
			if (this.isCurrentAccount(account)) {
163
				currentAccount = account;
164
				currentSession = session;
165 166 167 168 169 170 171 172 173 174 175 176
			}
		}

		if (currentAccount) {
			// Always use current account if available
			accounts.set(currentAccount.accountName, currentAccount);
		}

		// update access token
		if (currentSession) {
			try {
				const token = await currentSession.getAccessToken();
177
				await this.authenticationTokenService.setToken({ token, authenticationProviderId });
178 179 180
			} catch (e) {
				this.logService.error(e);
			}
181
		}
182 183

		return values(accounts);
184 185
	}

186
	private isCurrentAccount(account: IUserDataSyncAccount): boolean {
187
		return account.sessionId === this.currentSessionId;
188 189
	}

190 191 192 193
	async pick(): Promise<boolean> {
		const result = await this.doPick();
		if (!result) {
			return false;
194
		}
195 196 197 198
		let sessionId: string, accountName: string;
		if (isAuthenticationProvider(result)) {
			const session = await this.authenticationService.login(result.id, result.scopes);
			sessionId = session.id;
199
			accountName = session.account.displayName;
200 201 202 203 204 205 206 207 208 209 210
		} else {
			sessionId = result.sessionId;
			accountName = result.accountName;
		}
		await this.switch(sessionId, accountName);
		return true;
	}

	private async doPick(): Promise<IUserDataSyncAccount | IAuthenticationProvider | undefined> {
		if (this.authenticationProviders.length === 0) {
			return undefined;
211
		}
212

213
		await this.update();
214 215 216 217

		// Single auth provider and no accounts available
		if (this.authenticationProviders.length === 1 && !this.all.length) {
			return this.authenticationProviders[0];
218
		}
219 220 221

		return new Promise<IUserDataSyncAccount | IAuthenticationProvider | undefined>(async (c, e) => {
			let result: IUserDataSyncAccount | IAuthenticationProvider | undefined;
222
			const disposables: DisposableStore = new DisposableStore();
223
			const quickPick = this.quickInputService.createQuickPick<AccountQuickPickItem>();
224 225
			disposables.add(quickPick);

S
Sandeep Somavarapu 已提交
226
			quickPick.title = localize('pick an account', "Preferences Sync");
227
			quickPick.ok = false;
228
			quickPick.placeholder = localize('choose account placeholder', "Select an account");
229
			quickPick.ignoreFocusOut = true;
230
			quickPick.items = this.createQuickpickItems();
231

232 233 234
			disposables.add(quickPick.onDidAccept(() => {
				result = quickPick.selectedItems[0]?.account ? quickPick.selectedItems[0]?.account : quickPick.selectedItems[0]?.authenticationProvider;
				quickPick.hide();
235
			}));
S
Sandeep Somavarapu 已提交
236 237
			disposables.add(quickPick.onDidHide(() => {
				disposables.dispose();
238
				c(result);
S
Sandeep Somavarapu 已提交
239
			}));
240 241 242 243
			quickPick.show();
		});
	}

244 245
	private createQuickpickItems(): (AccountQuickPickItem | IQuickPickSeparator)[] {
		const quickPickItems: (AccountQuickPickItem | IQuickPickSeparator)[] = [];
246
		const authenticationProviders = [...this.authenticationProviders].sort(({ id }) => id === this.current?.authenticationProviderId ? -1 : 1);
247 248
		for (const authenticationProvider of authenticationProviders) {
			const providerName = this.authenticationService.getDisplayName(authenticationProvider.id);
S
Sandeep Somavarapu 已提交
249 250 251 252 253 254 255 256 257 258 259
			if (this.all.length) {
				quickPickItems.push({ type: 'separator', label: providerName });
				const accounts = this._all.get(authenticationProvider.id) || [];
				for (const account of accounts) {
					quickPickItems.push({
						label: account.accountName,
						detail: account.sessionId === this.current?.sessionId ? localize('last used', "Last Used") : undefined,
						account,
						authenticationProvider,
					});
				}
260
				quickPickItems.push({
S
Sandeep Somavarapu 已提交
261
					label: accounts.length ? localize('use another', "Use another {0} Account", providerName) : localize('use provider account', "Use {0} Account", providerName),
262 263
					authenticationProvider,
				});
S
Sandeep Somavarapu 已提交
264 265
			} else {
				quickPickItems.push({ label: providerName, authenticationProvider });
266 267 268 269 270
			}
		}
		return quickPickItems;
	}

271 272 273
	private async switch(sessionId: string, accountName: string): Promise<void> {
		const currentAccount = this.current;
		if (this.userDataSyncEnablementService.isEnabled() && (currentAccount && currentAccount.accountName !== accountName)) {
274 275
			// accounts are switched while sync is enabled.
		}
276
		this.currentSessionId = sessionId;
277 278 279 280
		this.telemetryService.publicLog2<UserAccountEvent, UserAccountClassification>('sync.userAccount', { id: sessionId.split('/')[1] });
		await this.update();
	}

281 282 283
	private onDidChangeSessions(e: AuthenticationSessionsChangeEvent): void {
		if (this.currentSessionId && e.removed.includes(this.currentSessionId)) {
			this.currentSessionId = undefined;
284
		}
285
		this.update();
286 287
	}

288 289 290 291 292 293
	private onDidChangeStorage(e: IWorkspaceStorageChangeEvent): void {
		if (e.key === UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY && e.scope === StorageScope.GLOBAL
			&& this.currentSessionId !== this.getStoredCachedSessionId() /* This checks if current window changed the value or not */) {
			this._cachedCurrentSessionId = null;
			this.update();
		}
294 295
	}

296 297 298 299
	private _cachedCurrentSessionId: string | undefined | null = null;
	private get currentSessionId(): string | undefined {
		if (this._cachedCurrentSessionId === null) {
			this._cachedCurrentSessionId = this.getStoredCachedSessionId();
300
		}
301
		return this._cachedCurrentSessionId;
302 303
	}

304
	private set currentSessionId(cachedSessionId: string | undefined) {
305
		if (this._cachedCurrentSessionId !== cachedSessionId) {
306 307 308 309 310 311
			this._cachedCurrentSessionId = cachedSessionId;
			if (cachedSessionId === undefined) {
				this.storageService.remove(UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY, StorageScope.GLOBAL);
			} else {
				this.storageService.store(UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY, cachedSessionId, StorageScope.GLOBAL);
			}
312
		}
313 314 315 316
	}

	private getStoredCachedSessionId(): string | undefined {
		return this.storageService.get(UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY, StorageScope.GLOBAL);
317
	}
318

319 320
	private get useWorkbenchSessionId(): boolean {
		return !this.storageService.getBoolean(UserDataSyncAccounts.DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY, StorageScope.GLOBAL, false);
321 322
	}

323 324
	private set useWorkbenchSessionId(useWorkbenchSession: boolean) {
		this.storageService.store(UserDataSyncAccounts.DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY, !useWorkbenchSession, StorageScope.GLOBAL);
325
	}
326
}