mainThreadAuthentication.ts 13.0 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
7
import * as modes from 'vs/editor/common/modes';
8
import * as nls from 'vs/nls';
9 10 11
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService';
import { ExtHostAuthenticationShape, ExtHostContext, IExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol';
12 13 14
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import Severity from 'vs/base/common/severity';
15
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
16
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
17
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
18
import { INotificationService } from 'vs/platform/notification/common/notification';
19

20 21 22 23 24
interface AllowedExtension {
	id: string;
	name: string;
}

25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
const accountUsages = new Map<string, { [accountName: string]: string[] }>();

function addAccountUsage(providerId: string, accountName: string, extensionOrFeatureName: string) {
	const providerAccountUsage = accountUsages.get(providerId);
	if (!providerAccountUsage) {
		accountUsages.set(providerId, { [accountName]: [extensionOrFeatureName] });
	} else {
		if (providerAccountUsage[accountName]) {
			if (!providerAccountUsage[accountName].includes(extensionOrFeatureName)) {
				providerAccountUsage[accountName].push(extensionOrFeatureName);
			}
		} else {
			providerAccountUsage[accountName] = [extensionOrFeatureName];
		}

		accountUsages.set(providerId, providerAccountUsage);
	}
}

44 45 46 47 48 49 50 51 52 53 54 55
function readAllowedExtensions(storageService: IStorageService, providerId: string, accountName: string): AllowedExtension[] {
	let trustedExtensions: AllowedExtension[] = [];
	try {
		const trustedExtensionSrc = storageService.get(`${providerId}-${accountName}`, StorageScope.GLOBAL);
		if (trustedExtensionSrc) {
			trustedExtensions = JSON.parse(trustedExtensionSrc);
		}
	} catch (err) { }

	return trustedExtensions;
}

56 57
export class MainThreadAuthenticationProvider extends Disposable {
	private _sessionMenuItems = new Map<string, IDisposable[]>();
58 59
	private _accounts = new Map<string, string[]>(); // Map account name to session ids
	private _sessions = new Map<string, string>(); // Map account id to name
60 61 62

	constructor(
		private readonly _proxy: ExtHostAuthenticationShape,
63
		public readonly id: string,
64
		public readonly displayName: string,
65
		private readonly notificationService: INotificationService
66 67
	) {
		super();
68 69 70 71 72
	}

	public async initialize(): Promise<void> {
		return this.registerCommandsAndContextMenuItems();
	}
73

74 75
	public hasSessions(): boolean {
		return !!this._sessions.size;
76 77
	}

78 79
	private manageTrustedExtensions(quickInputService: IQuickInputService, storageService: IStorageService, accountName: string) {
		const quickPick = quickInputService.createQuickPick<{ label: string, extension: AllowedExtension }>();
80
		quickPick.canSelectMany = true;
81 82
		const allowedExtensions = readAllowedExtensions(storageService, this.id, accountName);
		const items = allowedExtensions.map(extension => {
83
			return {
84 85
				label: extension.name,
				extension
86 87 88 89 90
			};
		});

		quickPick.items = items;
		quickPick.selectedItems = items;
91 92
		quickPick.title = nls.localize('manageTrustedExtensions', "Manage Trusted Extensions");
		quickPick.placeholder = nls.localize('manageExensions', "Choose which extensions can access this account");
93 94

		quickPick.onDidAccept(() => {
95 96
			const updatedAllowedList = quickPick.selectedItems.map(item => item.extension);
			storageService.store(`${this.id}-${accountName}`, JSON.stringify(updatedAllowedList), StorageScope.GLOBAL);
97

98 99 100 101 102 103 104 105 106 107
			quickPick.dispose();
		});

		quickPick.onDidHide(() => {
			quickPick.dispose();
		});

		quickPick.show();
	}

108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
	private showUsage(quickInputService: IQuickInputService, accountName: string) {
		const quickPick = quickInputService.createQuickPick();
		const providerUsage = accountUsages.get(this.id);
		const accountUsage = (providerUsage || {})[accountName] || [];

		quickPick.items = accountUsage.map(extensionOrFeature => {
			return {
				label: extensionOrFeature
			};
		});

		quickPick.onDidHide(() => {
			quickPick.dispose();
		});

		quickPick.show();
	}

126 127
	private async registerCommandsAndContextMenuItems(): Promise<void> {
		const sessions = await this._proxy.$getSessions(this.id);
128
		sessions.forEach(session => this.registerSession(session));
129 130 131
	}

	private registerSession(session: modes.AuthenticationSession) {
132
		this._sessions.set(session.id, session.account.displayName);
133

134
		const existingSessionsForAccount = this._accounts.get(session.account.displayName);
135
		if (existingSessionsForAccount) {
136
			this._accounts.set(session.account.displayName, existingSessionsForAccount.concat(session.id));
137 138
			return;
		} else {
139
			this._accounts.set(session.account.displayName, [session.id]);
140 141
		}

142 143 144 145
		const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
			group: '1_accounts',
			command: {
				id: `configureSessions${session.id}`,
146
				title: `${session.account.displayName} (${this.displayName})`
147 148 149 150 151 152 153 154
			},
			order: 3
		});

		const manageCommand = CommandsRegistry.registerCommand({
			id: `configureSessions${session.id}`,
			handler: (accessor, args) => {
				const quickInputService = accessor.get(IQuickInputService);
155
				const storageService = accessor.get(IStorageService);
156
				const dialogService = accessor.get(IDialogService);
157 158

				const quickPick = quickInputService.createQuickPick();
159
				const showUsage = nls.localize('showUsage', "Show Extensions and Features Using This Account");
160 161
				const manage = nls.localize('manageTrustedExtensions', "Manage Trusted Extensions");
				const signOut = nls.localize('signOut', "Sign Out");
162
				const items = ([{ label: showUsage }, { label: manage }, { label: signOut }]);
163 164 165

				quickPick.items = items;

166
				quickPick.onDidAccept(e => {
167
					const selected = quickPick.selectedItems[0];
168
					if (selected.label === signOut) {
169
						this.signOut(dialogService, session);
170 171
					}

172
					if (selected.label === manage) {
173
						this.manageTrustedExtensions(quickInputService, storageService, session.account.displayName);
174 175
					}

176
					if (selected.label === showUsage) {
177
						this.showUsage(quickInputService, session.account.displayName);
178 179
					}

180 181 182 183 184 185 186 187 188 189 190
					quickPick.dispose();
				});

				quickPick.onDidHide(_ => {
					quickPick.dispose();
				});

				quickPick.show();
			},
		});

191
		this._sessionMenuItems.set(session.account.displayName, [menuItem, manageCommand]);
192
	}
193

194 195
	async signOut(dialogService: IDialogService, session: modes.AuthenticationSession): Promise<void> {
		const providerUsage = accountUsages.get(this.id);
196 197
		const accountUsage = (providerUsage || {})[session.account.displayName] || [];
		const sessionsForAccount = this._accounts.get(session.account.displayName);
198 199 200

		// Skip dialog if nothing is using the account
		if (!accountUsage.length) {
201
			accountUsages.set(this.id, { [session.account.displayName]: [] });
202 203 204 205 206
			sessionsForAccount?.forEach(sessionId => this.logout(sessionId));
			return;
		}

		const result = await dialogService.confirm({
207 208
			title: nls.localize('signOutConfirm', "Sign out of {0}", session.account.displayName),
			message: nls.localize('signOutMessage', "The account {0} is currently used by: \n\n{1}\n\n Sign out of these features?", session.account.displayName, accountUsage.join('\n'))
209 210 211
		});

		if (result.confirmed) {
212
			accountUsages.set(this.id, { [session.account.displayName]: [] });
213 214 215 216
			sessionsForAccount?.forEach(sessionId => this.logout(sessionId));
		}
	}

217 218 219 220
	async getSessions(): Promise<ReadonlyArray<modes.AuthenticationSession>> {
		return (await this._proxy.$getSessions(this.id)).map(session => {
			return {
				id: session.id,
221
				account: session.account,
222
				getAccessToken: () => {
223
					addAccountUsage(this.id, session.account.displayName, nls.localize('sync', "Preferences Sync"));
224 225
					return this._proxy.$getSessionAccessToken(this.id, session.id);
				}
226 227
			};
		});
228 229
	}

230 231 232 233 234 235 236 237
	async updateSessionItems(event: modes.AuthenticationSessionsChangeEvent): Promise<void> {
		const { added, removed } = event;
		const session = await this._proxy.$getSessions(this.id);
		const addedSessions = session.filter(session => added.some(id => id === session.id));

		removed.forEach(sessionId => {
			const accountName = this._sessions.get(sessionId);
			if (accountName) {
238
				this._sessions.delete(sessionId);
239 240 241 242 243 244 245 246 247 248
				let sessionsForAccount = this._accounts.get(accountName) || [];
				const sessionIndex = sessionsForAccount.indexOf(sessionId);
				sessionsForAccount.splice(sessionIndex);

				if (!sessionsForAccount.length) {
					const disposeables = this._sessionMenuItems.get(accountName);
					if (disposeables) {
						disposeables.forEach(disposeable => disposeable.dispose());
						this._sessionMenuItems.delete(accountName);
					}
249
					this._accounts.delete(accountName);
250
				}
251 252 253 254 255 256
			}
		});

		addedSessions.forEach(session => this.registerSession(session));
	}

257
	login(scopes: string[]): Promise<modes.AuthenticationSession> {
258 259 260
		return this._proxy.$login(this.id, scopes).then(session => {
			return {
				id: session.id,
261
				account: session.account,
262
				getAccessToken: () => this._proxy.$getSessionAccessToken(this.id, session.id)
263 264
			};
		});
265 266
	}

267 268
	async logout(sessionId: string): Promise<void> {
		await this._proxy.$logout(this.id, sessionId);
269
		this.notificationService.info(nls.localize('signedOut', "Successfully signed out."));
270 271 272 273 274 275
	}

	dispose(): void {
		super.dispose();
		this._sessionMenuItems.forEach(item => item.forEach(d => d.dispose()));
		this._sessionMenuItems.clear();
276 277 278 279 280 281 282 283 284 285
	}
}

@extHostNamedCustomer(MainContext.MainThreadAuthentication)
export class MainThreadAuthentication extends Disposable implements MainThreadAuthenticationShape {
	private readonly _proxy: ExtHostAuthenticationShape;

	constructor(
		extHostContext: IExtHostContext,
		@IAuthenticationService private readonly authenticationService: IAuthenticationService,
286
		@IDialogService private readonly dialogService: IDialogService,
287
		@IStorageService private readonly storageService: IStorageService,
288
		@INotificationService private readonly notificationService: INotificationService
289 290 291 292 293
	) {
		super();
		this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication);
	}

294 295
	async $registerAuthenticationProvider(id: string, displayName: string): Promise<void> {
		const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName, this.notificationService);
296
		await provider.initialize();
297 298 299
		this.authenticationService.registerAuthenticationProvider(id, provider);
	}

300
	$unregisterAuthenticationProvider(id: string): void {
301 302 303
		this.authenticationService.unregisterAuthenticationProvider(id);
	}

304 305
	$onDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void {
		this.authenticationService.sessionsUpdate(id, event);
306
	}
307

308
	async $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean> {
309 310
		addAccountUsage(providerId, accountName, extensionName);

311 312 313
		const allowList = readAllowedExtensions(this.storageService, providerId, accountName);
		const extensionData = allowList.find(extension => extension.id === extensionId);
		if (extensionData) {
314
			return true;
315 316
		}

317
		const { choice } = await this.dialogService.show(
318
			Severity.Info,
319
			nls.localize('confirmAuthenticationAccess', "The extension '{0}' is trying to access authentication information for the {1} account '{2}'.", extensionName, providerName, accountName),
320 321
			[nls.localize('cancel', "Cancel"), nls.localize('allow', "Allow")],
			{
322
				cancelId: 0
323
			}
324 325
		);

326
		const allow = choice === 1;
327
		if (allow) {
328
			allowList.push({ id: extensionId, name: extensionName });
329
			this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL);
330
		}
331 332

		return allow;
333 334
	}

335 336
	async $loginPrompt(providerName: string, extensionName: string): Promise<boolean> {
		const { choice } = await this.dialogService.show(
337 338
			Severity.Info,
			nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, providerName),
339
			[nls.localize('cancel', "Cancel"), nls.localize('allow', "Allow")],
340
			{
341
				cancelId: 0
342
			}
343 344
		);

345 346
		return choice === 1;
	}
347 348 349 350 351 352 353 354

	async $setTrustedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise<void> {
		const allowList = readAllowedExtensions(this.storageService, providerId, accountName);
		if (!allowList.find(allowed => allowed.id === extensionId)) {
			allowList.push({ id: extensionId, name: extensionName });
			this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL);
		}
	}
355
}