Merge from vscode 79a1f5a5ca0c6c53db617aa1fa5a2396d2caebe2

This commit is contained in:
ADS Merger
2020-05-31 19:47:51 +00:00
parent 84492049e8
commit 28be33cfea
913 changed files with 28242 additions and 15549 deletions

View File

@@ -7,7 +7,7 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import * as modes from 'vs/editor/common/modes';
import * as nls from 'vs/nls';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService';
import { IAuthenticationService, AllowedExtension, readAllowedExtensions } from 'vs/workbench/services/authentication/browser/authenticationService';
import { ExtHostAuthenticationShape, ExtHostContext, IExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
@@ -18,43 +18,56 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
interface AllowedExtension {
id: string;
name: string;
}
const accountUsages = new Map<string, { [accountName: string]: string[] }>();
import { fromNow } from 'vs/base/common/date';
const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git'];
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);
}
interface IAccountUsage {
extensionId: string;
extensionName: string;
lastUsed: number;
}
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);
function readAccountUsages(storageService: IStorageService, providerId: string, accountName: string,): IAccountUsage[] {
const accountKey = `${providerId}-${accountName}-usages`;
const storedUsages = storageService.get(accountKey, StorageScope.GLOBAL);
let usages: IAccountUsage[] = [];
if (storedUsages) {
try {
usages = JSON.parse(storedUsages);
} catch (e) {
// ignore
}
} catch (err) { }
}
return trustedExtensions;
return usages;
}
function removeAccountUsage(storageService: IStorageService, providerId: string, accountName: string): void {
const accountKey = `${providerId}-${accountName}-usages`;
storageService.remove(accountKey, StorageScope.GLOBAL);
}
function addAccountUsage(storageService: IStorageService, providerId: string, accountName: string, extensionId: string, extensionName: string) {
const accountKey = `${providerId}-${accountName}-usages`;
const usages = readAccountUsages(storageService, providerId, accountName);
const existingUsageIndex = usages.findIndex(usage => usage.extensionId === extensionId);
if (existingUsageIndex > -1) {
usages.splice(existingUsageIndex, 1, {
extensionId,
extensionName,
lastUsed: Date.now()
});
} else {
usages.push({
extensionId,
extensionName,
lastUsed: Date.now()
});
}
storageService.store(accountKey, JSON.stringify(usages), StorageScope.GLOBAL);
}
export class MainThreadAuthenticationProvider extends Disposable {
@@ -66,8 +79,10 @@ export class MainThreadAuthenticationProvider extends Disposable {
private readonly _proxy: ExtHostAuthenticationShape,
public readonly id: string,
public readonly displayName: string,
public readonly supportsMultipleAccounts: boolean,
private readonly notificationService: INotificationService,
private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService
private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
private readonly storageService: IStorageService
) {
super();
}
@@ -81,12 +96,17 @@ export class MainThreadAuthenticationProvider extends Disposable {
}
private manageTrustedExtensions(quickInputService: IQuickInputService, storageService: IStorageService, accountName: string) {
const quickPick = quickInputService.createQuickPick<{ label: string, extension: AllowedExtension }>();
const quickPick = quickInputService.createQuickPick<{ label: string, description: string, extension: AllowedExtension }>();
quickPick.canSelectMany = true;
const allowedExtensions = readAllowedExtensions(storageService, this.id, accountName);
const usages = readAccountUsages(storageService, this.id, accountName);
const items = allowedExtensions.map(extension => {
const usage = usages.find(usage => extension.id === usage.extensionId);
return {
label: extension.name,
description: usage
? nls.localize('accountLastUsedDate', "Last used this account {0}", fromNow(usage.lastUsed, true))
: nls.localize('notUsed', "Has not used this account"),
extension
};
});
@@ -110,24 +130,6 @@ export class MainThreadAuthenticationProvider extends Disposable {
quickPick.show();
}
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();
}
private async registerCommandsAndContextMenuItems(): Promise<void> {
const sessions = await this._proxy.$getSessions(this.id);
sessions.forEach(session => this.registerSession(session));
@@ -163,10 +165,9 @@ export class MainThreadAuthenticationProvider extends Disposable {
const dialogService = accessor.get(IDialogService);
const quickPick = quickInputService.createQuickPick();
const showUsage = nls.localize('showUsage', "Show Extensions and Features Using This Account");
const manage = nls.localize('manageTrustedExtensions', "Manage Trusted Extensions");
const signOut = nls.localize('signOut', "Sign Out");
const items = ([{ label: showUsage }, { label: manage }, { label: signOut }]);
const items = ([{ label: manage }, { label: signOut }]);
quickPick.items = items;
@@ -180,10 +181,6 @@ export class MainThreadAuthenticationProvider extends Disposable {
this.manageTrustedExtensions(quickInputService, storageService, session.account.displayName);
}
if (selected.label === showUsage) {
this.showUsage(quickInputService, session.account.displayName);
}
quickPick.dispose();
});
@@ -199,39 +196,24 @@ export class MainThreadAuthenticationProvider extends Disposable {
}
async signOut(dialogService: IDialogService, session: modes.AuthenticationSession): Promise<void> {
const providerUsage = accountUsages.get(this.id);
const accountUsage = (providerUsage || {})[session.account.displayName] || [];
const accountUsages = readAccountUsages(this.storageService, this.id, session.account.displayName);
const sessionsForAccount = this._accounts.get(session.account.displayName);
// Skip dialog if nothing is using the account
if (!accountUsage.length) {
accountUsages.set(this.id, { [session.account.displayName]: [] });
sessionsForAccount?.forEach(sessionId => this.logout(sessionId));
return;
}
const result = await dialogService.confirm({
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'))
message: accountUsages.length
? nls.localize('signOutMessagve', "The account {0} has been used by: \n\n{1}\n\n Sign out of these features?", session.account.displayName, accountUsages.map(usage => usage.extensionName).join('\n'))
: nls.localize('signOutMessageSimple', "Sign out of {0}?", session.account.displayName)
});
if (result.confirmed) {
accountUsages.set(this.id, { [session.account.displayName]: [] });
sessionsForAccount?.forEach(sessionId => this.logout(sessionId));
removeAccountUsage(this.storageService, this.id, session.account.displayName);
}
}
async getSessions(): Promise<ReadonlyArray<modes.AuthenticationSession>> {
return (await this._proxy.$getSessions(this.id)).map(session => {
return {
id: session.id,
account: session.account,
getAccessToken: () => {
addAccountUsage(this.id, session.account.displayName, nls.localize('sync', "Preferences Sync"));
return this._proxy.$getSessionAccessToken(this.id, session.id);
}
};
});
return this._proxy.$getSessions(this.id);
}
async updateSessionItems(event: modes.AuthenticationSessionsChangeEvent): Promise<void> {
@@ -262,13 +244,7 @@ export class MainThreadAuthenticationProvider extends Disposable {
}
login(scopes: string[]): Promise<modes.AuthenticationSession> {
return this._proxy.$login(this.id, scopes).then(session => {
return {
id: session.id,
account: session.account,
getAccessToken: () => this._proxy.$getSessionAccessToken(this.id, session.id)
};
});
return this._proxy.$login(this.id, scopes);
}
async logout(sessionId: string): Promise<void> {
@@ -294,14 +270,31 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
@IStorageService private readonly storageService: IStorageService,
@INotificationService private readonly notificationService: INotificationService,
@IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
@IQuickInputService private readonly quickInputService: IQuickInputService
) {
super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication);
this._register(this.authenticationService.onDidChangeSessions(e => {
this._proxy.$onDidChangeAuthenticationSessions(e.providerId, e.event);
}));
this._register(this.authenticationService.onDidRegisterAuthenticationProvider(providerId => {
this._proxy.$onDidChangeAuthenticationProviders([providerId], []);
}));
this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(providerId => {
this._proxy.$onDidChangeAuthenticationProviders([], [providerId]);
}));
}
async $registerAuthenticationProvider(id: string, displayName: string): Promise<void> {
const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName, this.notificationService, this.storageKeysSyncRegistryService);
$getProviderIds(): Promise<string[]> {
return Promise.resolve(this.authenticationService.getProviderIds());
}
async $registerAuthenticationProvider(id: string, displayName: string, supportsMultipleAccounts: boolean): Promise<void> {
const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName, supportsMultipleAccounts, this.notificationService, this.storageKeysSyncRegistryService, this.storageService);
await provider.initialize();
this.authenticationService.registerAuthenticationProvider(id, provider);
}
@@ -310,21 +303,142 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
this.authenticationService.unregisterAuthenticationProvider(id);
}
$onDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void {
$sendDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void {
this.authenticationService.sessionsUpdate(id, event);
}
async $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean> {
addAccountUsage(providerId, accountName, extensionName);
$getSessions(id: string): Promise<ReadonlyArray<modes.AuthenticationSession>> {
return this.authenticationService.getSessions(id);
}
$login(providerId: string, scopes: string[]): Promise<modes.AuthenticationSession> {
return this.authenticationService.login(providerId, scopes);
}
$logout(providerId: string, sessionId: string): Promise<void> {
return this.authenticationService.logout(providerId, sessionId);
}
async $requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise<void> {
return this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName);
}
async $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: { createIfNone: boolean, clearSessionPreference: boolean }): Promise<modes.AuthenticationSession | undefined> {
const orderedScopes = scopes.sort().join(' ');
const sessions = (await this.$getSessions(providerId)).filter(session => session.scopes.sort().join(' ') === orderedScopes);
const displayName = this.authenticationService.getDisplayName(providerId);
if (sessions.length) {
if (!this.authenticationService.supportsMultipleAccounts(providerId)) {
const session = sessions[0];
const allowed = await this.$getSessionsPrompt(providerId, session.account.displayName, displayName, extensionId, extensionName);
if (allowed) {
return session;
} else {
throw new Error('User did not consent to login.');
}
}
// On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid
const selected = await this.$selectSession(providerId, displayName, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference);
return sessions.find(session => session.id === selected.id);
} else {
if (options.createIfNone) {
const isAllowed = await this.$loginPrompt(displayName, extensionName);
if (!isAllowed) {
throw new Error('User did not consent to login.');
}
const session = await this.authenticationService.login(providerId, scopes);
await this.$setTrustedExtension(providerId, session.account.displayName, extensionId, extensionName);
return session;
} else {
await this.$requestNewSession(providerId, scopes, extensionId, extensionName);
return undefined;
}
}
}
async $selectSession(providerId: string, providerName: string, extensionId: string, extensionName: string, potentialSessions: modes.AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise<modes.AuthenticationSession> {
if (!potentialSessions.length) {
throw new Error('No potential sessions found');
}
if (clearSessionPreference) {
this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.GLOBAL);
} else {
const existingSessionPreference = this.storageService.get(`${extensionName}-${providerId}`, StorageScope.GLOBAL);
if (existingSessionPreference) {
const matchingSession = potentialSessions.find(session => session.id === existingSessionPreference);
if (matchingSession) {
const allowed = await this.$getSessionsPrompt(providerId, matchingSession.account.displayName, providerName, extensionId, extensionName);
if (allowed) {
return matchingSession;
}
}
}
}
return new Promise((resolve, reject) => {
const quickPick = this.quickInputService.createQuickPick<{ label: string, session?: modes.AuthenticationSession }>();
quickPick.ignoreFocusOut = true;
const items: { label: string, session?: modes.AuthenticationSession }[] = potentialSessions.map(session => {
return {
label: session.account.displayName,
session
};
});
items.push({
label: nls.localize('useOtherAccount', "Sign in to another account")
});
quickPick.items = items;
quickPick.title = nls.localize('selectAccount', "The extension '{0}' wants to access a {1} account", extensionName, providerName);
quickPick.placeholder = nls.localize('getSessionPlateholder', "Select an account for '{0}' to use or Esc to cancel", extensionName);
quickPick.onDidAccept(async _ => {
const selected = quickPick.selectedItems[0];
const session = selected.session ?? await this.authenticationService.login(providerId, scopes);
const accountName = session.account.displayName;
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);
}
this.storageService.store(`${extensionName}-${providerId}`, session.id, StorageScope.GLOBAL);
quickPick.dispose();
resolve(session);
});
quickPick.onDidHide(_ => {
if (!quickPick.selectedItems[0]) {
reject('User did not consent to account access');
}
quickPick.dispose();
});
quickPick.show();
});
}
async $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean> {
const allowList = readAllowedExtensions(this.storageService, providerId, accountName);
const extensionData = allowList.find(extension => extension.id === extensionId);
if (extensionData) {
addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName);
return true;
}
const remoteConnection = this.remoteAgentService.getConnection();
if (remoteConnection && remoteConnection.remoteAuthority && remoteConnection.remoteAuthority.startsWith('vsonline') && VSO_ALLOWED_EXTENSIONS.includes(extensionId)) {
addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName);
return true;
}
@@ -339,6 +453,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
const allow = choice === 0;
if (allow) {
addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName);
allowList.push({ id: extensionId, name: extensionName });
this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL);
}