Tear down the Authentication monolith (#206495)

* Tear down the Authentication monolith

Major changes:
* Turn the usage functions into a proper service `AuthenticationUsageService`
* Pull out the access data stuff into its own service `AuthenticationAccessService`
* Pull out things that make sense as actions `ManageTrustedExtensionsForAccount` `SignOutOfAccount`
* Pull out random registry stuff into a proper authentication contribution
* Pull out everything else that is extension specific into its own class (and eventually it should be in MainThreadAuthentication)
* Have the new `AuthenticationService` return a provider instead of having specific methods for getting the `label` or `supportsMultipleAccounts`

* fix tests

* fix tests
This commit is contained in:
Tyler James Leonhardt
2024-02-29 07:41:11 -06:00
committed by GitHub
parent 15a0f3c48f
commit 4e81df7ea9
19 changed files with 1485 additions and 977 deletions

View File

@@ -553,6 +553,10 @@
{
"name": "vs/workbench/contrib/accountEntitlements",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/authentication",
"project": "vscode-workbench"
}
]
}

View File

@@ -6,18 +6,18 @@
import { Disposable, DisposableMap } from 'vs/base/common/lifecycle';
import * as nls from 'vs/nls';
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { getAuthenticationProviderActivationEvent, addAccountUsage } from 'vs/workbench/services/authentication/browser/authenticationService';
import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication';
import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService } from 'vs/workbench/services/authentication/common/authentication';
import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import Severity from 'vs/base/common/severity';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import type { AuthenticationGetSessionOptions } from 'vscode';
import { Emitter, Event } from 'vs/base/common/event';
import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService';
import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService';
import { getAuthenticationProviderActivationEvent } from 'vs/workbench/services/authentication/browser/authenticationService';
export class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider {
@@ -58,8 +58,10 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
constructor(
extHostContext: IExtHostContext,
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
@IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService,
@IAuthenticationAccessService private readonly authenticationAccessService: IAuthenticationAccessService,
@IAuthenticationUsageService private readonly authenticationUsageService: IAuthenticationUsageService,
@IDialogService private readonly dialogService: IDialogService,
@IStorageService private readonly storageService: IStorageService,
@INotificationService private readonly notificationService: INotificationService,
@IExtensionService private readonly extensionService: IExtensionService,
@ITelemetryService private readonly telemetryService: ITelemetryService
@@ -116,7 +118,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise<AuthenticationSession | undefined> {
const sessions = await this.authenticationService.getSessions(providerId, scopes, true);
const supportsMultipleAccounts = this.authenticationService.supportsMultipleAccounts(providerId);
const provider = this.authenticationService.getProvider(providerId);
// Error cases
if (options.forceNewSession && options.createIfNone) {
@@ -131,22 +133,22 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
// Check if the sessions we have are valid
if (!options.forceNewSession && sessions.length) {
if (supportsMultipleAccounts) {
if (provider.supportsMultipleAccounts) {
if (options.clearSessionPreference) {
// Clearing the session preference is usually paired with createIfNone, so just remove the preference and
// defer to the rest of the logic in this function to choose the session.
this.authenticationService.removeSessionPreference(providerId, extensionId, scopes);
this.authenticationExtensionsService.removeSessionPreference(providerId, extensionId, scopes);
} else {
// If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function.
const existingSessionPreference = this.authenticationService.getSessionPreference(providerId, extensionId, scopes);
const existingSessionPreference = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes);
if (existingSessionPreference) {
const matchingSession = sessions.find(session => session.id === existingSessionPreference);
if (matchingSession && this.authenticationService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) {
if (matchingSession && this.authenticationAccessService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) {
return matchingSession;
}
}
}
} else if (this.authenticationService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) {
} else if (this.authenticationAccessService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) {
return sessions[0];
}
}
@@ -154,51 +156,41 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
// We may need to prompt because we don't have a valid session
// modal flows
if (options.createIfNone || options.forceNewSession) {
const providerName = this.authenticationService.getLabel(providerId);
const detail = (typeof options.forceNewSession === 'object') ? options.forceNewSession.detail : undefined;
// We only want to show the "recreating session" prompt if we are using forceNewSession & there are sessions
// that we will be "forcing through".
const recreatingSession = !!(options.forceNewSession && sessions.length);
const isAllowed = await this.loginPrompt(providerName, extensionName, recreatingSession, detail);
const isAllowed = await this.loginPrompt(provider.label, extensionName, recreatingSession, detail);
if (!isAllowed) {
throw new Error('User did not consent to login.');
}
let session;
if (sessions?.length && !options.forceNewSession) {
session = supportsMultipleAccounts
? await this.authenticationService.selectSession(providerId, extensionId, extensionName, scopes, sessions)
session = provider.supportsMultipleAccounts
? await this.authenticationExtensionsService.selectSession(providerId, extensionId, extensionName, scopes, sessions)
: sessions[0];
} else {
let sessionToRecreate: AuthenticationSession | undefined;
if (typeof options.forceNewSession === 'object' && options.forceNewSession.sessionToRecreate) {
sessionToRecreate = options.forceNewSession.sessionToRecreate as AuthenticationSession;
} else {
const sessionIdToRecreate = this.authenticationService.getSessionPreference(providerId, extensionId, scopes);
const sessionIdToRecreate = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes);
sessionToRecreate = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate) : undefined;
}
session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, sessionToRecreate });
}
this.authenticationService.updateAllowedExtension(providerId, session.account.label, extensionId, extensionName, true);
this.authenticationService.updateSessionPreference(providerId, extensionId, session);
this.authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]);
this.authenticationExtensionsService.updateSessionPreference(providerId, extensionId, session);
return session;
}
// For the silent flows, if we have a session, even though it may not be the user's preference, we'll return it anyway because it might be for a specific
// set of scopes.
const validSession = sessions.find(session => this.authenticationService.isAccessAllowed(providerId, session.account.label, extensionId));
const validSession = sessions.find(session => this.authenticationAccessService.isAccessAllowed(providerId, session.account.label, extensionId));
if (validSession) {
// Migration. If we have a valid session, but no preference, we'll set the preference to the valid session.
// TODO: Remove this after in a few releases.
if (!this.authenticationService.getSessionPreference(providerId, extensionId, scopes)) {
if (this.storageService.get(`${extensionName}-${providerId}`, StorageScope.APPLICATION)) {
this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.APPLICATION);
}
this.authenticationService.updateAllowedExtension(providerId, validSession.account.label, extensionId, extensionName, true);
this.authenticationService.updateSessionPreference(providerId, extensionId, validSession);
}
return validSession;
}
@@ -207,8 +199,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
// If there is a potential session, but the extension doesn't have access to it, use the "grant access" flow,
// otherwise request a new one.
sessions.length
? this.authenticationService.requestSessionAccess(providerId, extensionId, extensionName, scopes, sessions)
: await this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName);
? this.authenticationExtensionsService.requestSessionAccess(providerId, extensionId, extensionName, scopes, sessions)
: await this.authenticationExtensionsService.requestNewSession(providerId, scopes, extensionId, extensionName);
}
return undefined;
}
@@ -218,7 +210,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
if (session) {
this.sendProviderUsageTelemetry(extensionId, providerId);
addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName);
this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName);
}
return session;
@@ -226,11 +218,11 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
async $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise<AuthenticationSession[]> {
const sessions = await this.authenticationService.getSessions(providerId, [...scopes], true);
const accessibleSessions = sessions.filter(s => this.authenticationService.isAccessAllowed(providerId, s.account.label, extensionId));
const accessibleSessions = sessions.filter(s => this.authenticationAccessService.isAccessAllowed(providerId, s.account.label, extensionId));
if (accessibleSessions.length) {
this.sendProviderUsageTelemetry(extensionId, providerId);
for (const session of accessibleSessions) {
addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName);
this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName);
}
}
return accessibleSessions;

View File

@@ -14,6 +14,7 @@ import { IProgress, Progress } from 'vs/platform/progress/common/progress';
import { Registry } from 'vs/platform/registry/common/platform';
import { ExtHostLanguageModelsShape, ExtHostContext, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol';
import { ILanguageModelChatMetadata, IChatResponseFragment, ILanguageModelsService, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels';
import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService';
import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationProviderCreateSessionOptions, IAuthenticationService, INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication';
import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures';
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
@@ -33,6 +34,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape {
@IExtensionFeaturesManagementService private readonly _extensionFeaturesManagementService: IExtensionFeaturesManagementService,
@ILogService private readonly _logService: ILogService,
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
@IAuthenticationAccessService private readonly _authenticationAccessService: IAuthenticationAccessService,
@IExtensionService private readonly _extensionService: IExtensionService
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatProvider);
@@ -132,28 +134,8 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape {
disposables.add(toDisposable(() => {
this._authenticationService.unregisterAuthenticationProvider(authProviderId);
}));
disposables.add(this._authenticationService.onDidChangeSessions(async (e) => {
if (e.providerId === authProviderId) {
if (e.event.removed?.length) {
const allowedExtensions = this._authenticationService.readAllowedExtensions(authProviderId, accountLabel);
const extensionsToUpdateAccess = [];
for (const allowed of allowedExtensions) {
const from = await this._extensionService.getExtension(allowed.id);
this._authenticationService.updateAllowedExtension(authProviderId, authProviderId, allowed.id, allowed.name, false);
if (from) {
extensionsToUpdateAccess.push({
from: from.identifier,
to: extension,
enabled: false
});
}
}
this._proxy.$updateModelAccesslist(extensionsToUpdateAccess);
}
}
}));
disposables.add(this._authenticationService.onDidChangeExtensionSessionAccess(async (e) => {
const allowedExtensions = this._authenticationService.readAllowedExtensions(authProviderId, accountLabel);
disposables.add(this._authenticationAccessService.onDidChangeExtensionSessionAccess(async (e) => {
const allowedExtensions = this._authenticationAccessService.readAllowedExtensions(authProviderId, accountLabel);
const accessList = [];
for (const allowedExtension of allowedExtensions) {
const from = await this._extensionService.getExtension(allowedExtension.id);

View File

@@ -19,7 +19,7 @@ import { ExtHostContext, MainContext } from 'vs/workbench/api/common/extHost.pro
import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication';
import { IActivityService } from 'vs/workbench/services/activity/common/activity';
import { AuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService';
import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication';
import { IAuthenticationExtensionsService, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication';
import { IExtensionService, nullExtensionDescription as extensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol';
@@ -28,6 +28,9 @@ import { TestActivityService, TestExtensionService, TestProductService, TestStor
import type { AuthenticationProvider, AuthenticationSession } from 'vscode';
import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService';
import { IProductService } from 'vs/platform/product/common/productService';
import { AuthenticationAccessService, IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService';
import { AuthenticationUsageService, IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService';
import { AuthenticationExtensionsService } from 'vs/workbench/services/authentication/browser/authenticationExtensionsService';
class AuthQuickPick {
private listener: ((e: IQuickPickDidAcceptEvent) => any) | undefined;
@@ -113,9 +116,12 @@ suite('ExtHostAuthentication', () => {
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IBrowserWorkbenchEnvironmentService, TestEnvironmentService);
instantiationService.stub(IProductService, TestProductService);
instantiationService.stub(IAuthenticationAccessService, instantiationService.createInstance(AuthenticationAccessService));
instantiationService.stub(IAuthenticationUsageService, instantiationService.createInstance(AuthenticationUsageService));
const rpcProtocol = new TestRPCProtocol();
instantiationService.stub(IAuthenticationService, instantiationService.createInstance(AuthenticationService));
instantiationService.stub(IAuthenticationExtensionsService, instantiationService.createInstance(AuthenticationExtensionsService));
rpcProtocol.set(MainContext.MainThreadAuthentication, instantiationService.createInstance(MainThreadAuthentication, rpcProtocol));
extHostAuthentication = new ExtHostAuthentication(rpcProtocol);
rpcProtocol.set(ExtHostContext.ExtHostAuthentication, extHostAuthentication);

View File

@@ -43,6 +43,7 @@ import { isString } from 'vs/base/common/types';
import { KeyCode } from 'vs/base/common/keyCodes';
import { ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND } from 'vs/workbench/common/theme';
import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { ICommandService } from 'vs/platform/commands/common/commands';
export class GlobalCompositeBar extends Disposable {
@@ -309,6 +310,7 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction
@ILogService private readonly logService: ILogService,
@IActivityService activityService: IActivityService,
@IInstantiationService instantiationService: IInstantiationService,
@ICommandService private readonly commandService: ICommandService
) {
const action = instantiationService.createInstance(CompositeBarAction, {
id: ACCOUNTS_ACTIVITY_ID,
@@ -391,7 +393,7 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction
menus.push(noAccountsAvailableAction);
break;
}
const providerLabel = this.authenticationService.getLabel(providerId);
const providerLabel = this.authenticationService.getProvider(providerId).label;
const accounts = this.groupedAccounts.get(providerId);
if (!accounts) {
if (this.problematicProviders.has(providerId)) {
@@ -408,19 +410,22 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction
}
for (const account of accounts) {
const manageExtensionsAction = disposables.add(new Action(`configureSessions${account.label}`, localize('manageTrustedExtensions', "Manage Trusted Extensions"), undefined, true, () => {
return this.authenticationService.manageTrustedExtensionsForAccount(providerId, account.label);
}));
const manageExtensionsAction = toAction({
id: `configureSessions${account.label}`,
label: localize('manageTrustedExtensions', "Manage Trusted Extensions"),
enabled: true,
run: () => this.commandService.executeCommand('_manageTrustedExtensionsForAccount', { providerId, accountLabel: account.label })
});
const providerSubMenuActions: Action[] = [manageExtensionsAction];
const providerSubMenuActions: IAction[] = [manageExtensionsAction];
if (account.canSignOut) {
const signOutAction = disposables.add(new Action('signOut', localize('signOut', "Sign Out"), undefined, true, async () => {
const allSessions = await this.authenticationService.getSessions(providerId);
const sessionsForAccount = allSessions.filter(s => s.account.label === account.label);
return await this.authenticationService.removeAccountSessions(providerId, account.label, sessionsForAccount);
providerSubMenuActions.push(toAction({
id: 'signOut',
label: localize('signOut', "Sign Out"),
enabled: true,
run: () => this.commandService.executeCommand('_signOutOfAccount', { providerId, accountLabel: account.label })
}));
providerSubMenuActions.push(signOutAction);
}
const providerSubMenu = new SubmenuAction('activitybar.submenu', `${account.label} (${providerLabel})`, providerSubMenuActions);
@@ -628,7 +633,8 @@ export class SimpleAccountActivityActionViewItem extends AccountsActivityActionV
@ISecretStorageService secretStorageService: ISecretStorageService,
@ILogService logService: ILogService,
@IActivityService activityService: IActivityService,
@IInstantiationService instantiationService: IInstantiationService
@IInstantiationService instantiationService: IInstantiationService,
@ICommandService commandService: ICommandService
) {
super(() => [], {
...options,
@@ -638,7 +644,7 @@ export class SimpleAccountActivityActionViewItem extends AccountsActivityActionV
}),
hoverOptions,
compact: true,
}, () => undefined, actions => actions, themeService, lifecycleService, hoverService, contextMenuService, menuService, contextKeyService, authenticationService, environmentService, productService, configurationService, keybindingService, secretStorageService, logService, activityService, instantiationService);
}, () => undefined, actions => actions, themeService, lifecycleService, hoverService, contextMenuService, menuService, contextKeyService, authenticationService, environmentService, productService, configurationService, keybindingService, secretStorageService, logService, activityService, instantiationService, commandService);
}
}

View File

@@ -0,0 +1,168 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { fromNow } from 'vs/base/common/date';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls';
import { Action2 } from 'vs/platform/actions/common/actions';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IProductService } from 'vs/platform/product/common/productService';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService';
import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService';
import { AllowedExtension } from 'vs/workbench/services/authentication/common/authentication';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
export class ManageTrustedExtensionsForAccountAction extends Action2 {
constructor() {
super({
id: '_manageTrustedExtensionsForAccount',
title: localize('manageTrustedExtensionsForAccount', "Manage Trusted Extensions For Account"),
f1: false
});
}
override async run(accessor: ServicesAccessor, { providerId, accountLabel }: { providerId: string; accountLabel: string }): Promise<void> {
const productService = accessor.get(IProductService);
const extensionService = accessor.get(IExtensionService);
const dialogService = accessor.get(IDialogService);
const quickInputService = accessor.get(IQuickInputService);
const authenticationUsageService = accessor.get(IAuthenticationUsageService);
const authenticationAccessService = accessor.get(IAuthenticationAccessService);
if (!providerId || !accountLabel) {
throw new Error('Invalid arguments. Expected: { providerId: string; accountLabel: string }');
}
const allowedExtensions = authenticationAccessService.readAllowedExtensions(providerId, accountLabel);
const trustedExtensionAuthAccess = productService.trustedExtensionAuthAccess;
const trustedExtensionIds =
// Case 1: trustedExtensionAuthAccess is an array
Array.isArray(trustedExtensionAuthAccess)
? trustedExtensionAuthAccess
// Case 2: trustedExtensionAuthAccess is an object
: typeof trustedExtensionAuthAccess === 'object'
? trustedExtensionAuthAccess[providerId] ?? []
: [];
for (const extensionId of trustedExtensionIds) {
const allowedExtension = allowedExtensions.find(ext => ext.id === extensionId);
if (!allowedExtension) {
// Add the extension to the allowedExtensions list
const extension = await extensionService.getExtension(extensionId);
if (extension) {
allowedExtensions.push({
id: extensionId,
name: extension.displayName || extension.name,
allowed: true,
trusted: true
});
}
} else {
// Update the extension to be allowed
allowedExtension.allowed = true;
allowedExtension.trusted = true;
}
}
if (!allowedExtensions.length) {
dialogService.info(localize('noTrustedExtensions', "This account has not been used by any extensions."));
return;
}
interface TrustedExtensionsQuickPickItem extends IQuickPickItem {
extension: AllowedExtension;
lastUsed?: number;
}
const disposableStore = new DisposableStore();
const quickPick = disposableStore.add(quickInputService.createQuickPick<TrustedExtensionsQuickPickItem>());
quickPick.canSelectMany = true;
quickPick.customButton = true;
quickPick.customLabel = localize('manageTrustedExtensions.cancel', 'Cancel');
const usages = authenticationUsageService.readAccountUsages(providerId, accountLabel);
const trustedExtensions = [];
const otherExtensions = [];
for (const extension of allowedExtensions) {
const usage = usages.find(usage => extension.id === usage.extensionId);
extension.lastUsed = usage?.lastUsed;
if (extension.trusted) {
trustedExtensions.push(extension);
} else {
otherExtensions.push(extension);
}
}
const sortByLastUsed = (a: AllowedExtension, b: AllowedExtension) => (b.lastUsed || 0) - (a.lastUsed || 0);
const toQuickPickItem = function (extension: AllowedExtension) {
const lastUsed = extension.lastUsed;
const description = lastUsed
? localize({ key: 'accountLastUsedDate', comment: ['The placeholder {0} is a string with time information, such as "3 days ago"'] }, "Last used this account {0}", fromNow(lastUsed, true))
: localize('notUsed', "Has not used this account");
let tooltip: string | undefined;
if (extension.trusted) {
tooltip = localize('trustedExtensionTooltip', "This extension is trusted by Microsoft and\nalways has access to this account");
}
return {
label: extension.name,
extension,
description,
tooltip
};
};
const items: Array<TrustedExtensionsQuickPickItem | IQuickPickSeparator> = [
...otherExtensions.sort(sortByLastUsed).map(toQuickPickItem),
{ type: 'separator', label: localize('trustedExtensions', "Trusted by Microsoft") },
...trustedExtensions.sort(sortByLastUsed).map(toQuickPickItem)
];
quickPick.items = items;
quickPick.selectedItems = items.filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator' && (item.extension.allowed === undefined || item.extension.allowed));
quickPick.title = localize('manageTrustedExtensions', "Manage Trusted Extensions");
quickPick.placeholder = localize('manageExtensions', "Choose which extensions can access this account");
disposableStore.add(quickPick.onDidAccept(() => {
const updatedAllowedList = quickPick.items
.filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator')
.map(i => i.extension);
authenticationAccessService.updateAllowedExtensions(providerId, accountLabel, updatedAllowedList);
quickPick.hide();
}));
disposableStore.add(quickPick.onDidChangeSelection((changed) => {
const trustedItems = new Set<TrustedExtensionsQuickPickItem>();
quickPick.items.forEach(item => {
const trustItem = item as TrustedExtensionsQuickPickItem;
if (trustItem.extension) {
if (trustItem.extension.trusted) {
trustedItems.add(trustItem);
} else {
trustItem.extension.allowed = false;
}
}
});
changed.forEach((item) => {
item.extension.allowed = true;
trustedItems.delete(item);
});
// reselect trusted items if a user tried to unselect one since quick pick doesn't support forcing selection
if (trustedItems.size) {
quickPick.selectedItems = [...changed, ...trustedItems];
}
}));
disposableStore.add(quickPick.onDidHide(() => {
disposableStore.dispose();
}));
disposableStore.add(quickPick.onDidCustom(() => {
quickPick.hide();
}));
quickPick.show();
}
}

View File

@@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Severity from 'vs/base/common/severity';
import { localize } from 'vs/nls';
import { Action2 } from 'vs/platform/actions/common/actions';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService';
import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService';
import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication';
export class SignOutOfAccountAction extends Action2 {
constructor() {
super({
id: '_signOutOfAccount',
title: localize('signOutOfAccount', "Sign out of account"),
f1: false
});
}
override async run(accessor: ServicesAccessor, { providerId, accountLabel }: { providerId: string; accountLabel: string }): Promise<void> {
const authenticationService = accessor.get(IAuthenticationService);
const authenticationUsageService = accessor.get(IAuthenticationUsageService);
const authenticationAccessService = accessor.get(IAuthenticationAccessService);
const dialogService = accessor.get(IDialogService);
if (!providerId || !accountLabel) {
throw new Error('Invalid arguments. Expected: { providerId: string; accountLabel: string }');
}
const allSessions = await authenticationService.getSessions(providerId);
const sessions = allSessions.filter(s => s.account.label === accountLabel);
const accountUsages = authenticationUsageService.readAccountUsages(providerId, accountLabel);
const { confirmed } = await dialogService.confirm({
type: Severity.Info,
message: accountUsages.length
? localize('signOutMessage', "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountLabel, accountUsages.map(usage => usage.extensionName).join('\n'))
: localize('signOutMessageSimple', "Sign out of '{0}'?", accountLabel),
primaryButton: localize({ key: 'signOut', comment: ['&& denotes a mnemonic'] }, "&&Sign Out")
});
if (confirmed) {
const removeSessionPromises = sessions.map(session => authenticationService.removeSession(providerId, session.id));
await Promise.all(removeSessionPromises);
authenticationUsageService.removeAccountUsage(providerId, accountLabel);
authenticationAccessService.removeAllowedExtensions(providerId, accountLabel);
}
}
}

View File

@@ -0,0 +1,197 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
import { localize } from 'vs/nls';
import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions';
import { SignOutOfAccountAction } from 'vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction';
import { AuthenticationProviderInformation, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication';
import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService';
import { Extensions, IExtensionFeatureTableRenderer, IExtensionFeaturesRegistry, IRenderedData, IRowData, ITableData } from 'vs/workbench/services/extensionManagement/common/extensionFeatures';
import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { ManageTrustedExtensionsForAccountAction } from './actions/manageTrustedExtensionsForAccountAction';
const codeExchangeProxyCommand = CommandsRegistry.registerCommand('workbench.getCodeExchangeProxyEndpoints', function (accessor, _) {
const environmentService = accessor.get(IBrowserWorkbenchEnvironmentService);
return environmentService.options?.codeExchangeProxyEndpoints;
});
const authenticationDefinitionSchema: IJSONSchema = {
type: 'object',
additionalProperties: false,
properties: {
id: {
type: 'string',
description: localize('authentication.id', 'The id of the authentication provider.')
},
label: {
type: 'string',
description: localize('authentication.label', 'The human readable name of the authentication provider.'),
}
}
};
const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint<AuthenticationProviderInformation[]>({
extensionPoint: 'authentication',
jsonSchema: {
description: localize({ key: 'authenticationExtensionPoint', comment: [`'Contributes' means adds here`] }, 'Contributes authentication'),
type: 'array',
items: authenticationDefinitionSchema
},
activationEventsGenerator: (authenticationProviders, result) => {
for (const authenticationProvider of authenticationProviders) {
if (authenticationProvider.id) {
result.push(`onAuthenticationRequest:${authenticationProvider.id}`);
}
}
}
});
class AuthenticationDataRenderer extends Disposable implements IExtensionFeatureTableRenderer {
readonly type = 'table';
shouldRender(manifest: IExtensionManifest): boolean {
return !!manifest.contributes?.authentication;
}
render(manifest: IExtensionManifest): IRenderedData<ITableData> {
const authentication = manifest.contributes?.authentication || [];
if (!authentication.length) {
return { data: { headers: [], rows: [] }, dispose: () => { } };
}
const headers = [
localize('authenticationlabel', "Label"),
localize('authenticationid', "ID"),
];
const rows: IRowData[][] = authentication
.sort((a, b) => a.label.localeCompare(b.label))
.map(auth => {
return [
auth.label,
auth.id,
];
});
return {
data: {
headers,
rows
},
dispose: () => { }
};
}
}
const extensionFeature = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
id: 'authentication',
label: localize('authentication', "Authentication"),
access: {
canToggle: false
},
renderer: new SyncDescriptor(AuthenticationDataRenderer),
});
export class AuthenticationContribution extends Disposable implements IWorkbenchContribution {
static ID = 'workbench.contrib.authentication';
private _placeholderMenuItem: IDisposable | undefined = MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
command: {
id: 'noAuthenticationProviders',
title: localize('authentication.Placeholder', "No accounts requested yet..."),
precondition: ContextKeyExpr.false()
},
});
constructor(
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
@IBrowserWorkbenchEnvironmentService private readonly _environmentService: IBrowserWorkbenchEnvironmentService
) {
super();
this._register(codeExchangeProxyCommand);
this._register(extensionFeature);
this._registerHandlers();
this._registerAuthenticationExtentionPointHandler();
this._registerEnvContributedAuthenticationProviders();
this._registerActions();
}
private _registerAuthenticationExtentionPointHandler(): void {
authenticationExtPoint.setHandler((extensions, { added, removed }) => {
added.forEach(point => {
for (const provider of point.value) {
if (isFalsyOrWhitespace(provider.id)) {
point.collector.error(localize('authentication.missingId', 'An authentication contribution must specify an id.'));
continue;
}
if (isFalsyOrWhitespace(provider.label)) {
point.collector.error(localize('authentication.missingLabel', 'An authentication contribution must specify a label.'));
continue;
}
if (!this._authenticationService.declaredProviders.some(p => p.id === provider.id)) {
this._authenticationService.registerDeclaredAuthenticationProvider(provider);
} else {
point.collector.error(localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id));
}
}
});
const removedExtPoints = removed.flatMap(r => r.value);
removedExtPoints.forEach(point => {
const provider = this._authenticationService.declaredProviders.find(provider => provider.id === point.id);
if (provider) {
this._authenticationService.unregisterDeclaredAuthenticationProvider(provider.id);
}
});
});
}
private _registerEnvContributedAuthenticationProviders(): void {
if (!this._environmentService.options?.authenticationProviders?.length) {
return;
}
for (const provider of this._environmentService.options.authenticationProviders) {
this._authenticationService.registerAuthenticationProvider(provider.id, provider);
}
}
private _registerHandlers(): void {
this._register(this._authenticationService.onDidRegisterAuthenticationProvider(_e => {
this._placeholderMenuItem?.dispose();
this._placeholderMenuItem = undefined;
}));
this._register(this._authenticationService.onDidUnregisterAuthenticationProvider(_e => {
if (!this._authenticationService.getProviderIds().length) {
this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
command: {
id: 'noAuthenticationProviders',
title: localize('loading', "Loading..."),
precondition: ContextKeyExpr.false()
}
});
}
}));
}
private _registerActions(): void {
registerAction2(SignOutOfAccountAction);
registerAction2(ManageTrustedExtensionsForAccountAction);
}
}
registerWorkbenchContribution2(AuthenticationContribution.ID, AuthenticationContribution, WorkbenchPhase.AfterRestored);

View File

@@ -346,8 +346,8 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes
for (const authenticationProvider of (await this.getAuthenticationProviders())) {
const signedInForProvider = sessions.some(account => account.session.providerId === authenticationProvider.id);
if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) {
const providerName = this.authenticationService.getLabel(authenticationProvider.id);
if (!signedInForProvider || this.authenticationService.getProvider(authenticationProvider.id).supportsMultipleAccounts) {
const providerName = this.authenticationService.getProvider(authenticationProvider.id).label;
options.push({ label: localize('sign in using account', "Sign in with {0}", providerName), provider: authenticationProvider });
}
}
@@ -370,7 +370,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes
for (const session of sessions) {
const item = {
label: session.account.label,
description: this.authenticationService.getLabel(provider.id),
description: this.authenticationService.getProvider(provider.id).label,
session: { ...session, providerId: provider.id }
};
accounts.set(item.session.account.id, item);

View File

@@ -395,7 +395,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
private createExistingSessionItem(session: AuthenticationSession, providerId: string): ExistingSessionItem {
return {
label: session.account.label,
description: this.authenticationService.getLabel(providerId),
description: this.authenticationService.getProvider(providerId).label,
session,
providerId
};
@@ -412,9 +412,9 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
for (const authenticationProvider of (await this.getAuthenticationProviders())) {
const signedInForProvider = sessions.some(account => account.providerId === authenticationProvider.id);
if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) {
const providerName = this.authenticationService.getLabel(authenticationProvider.id);
options.push({ label: localize({ key: 'sign in using account', comment: ['{0} will be a auth provider (e.g. Github)'] }, "Sign in with {0}", providerName), provider: authenticationProvider });
const provider = this.authenticationService.getProvider(authenticationProvider.id);
if (!signedInForProvider || provider.supportsMultipleAccounts) {
options.push({ label: localize({ key: 'sign in using account', comment: ['{0} will be a auth provider (e.g. Github)'] }, "Sign in with {0}", provider.label), provider: authenticationProvider });
}
}

View File

@@ -918,7 +918,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
items.push({ id: syncNowCommand.id, label: `${SYNC_TITLE.value}: ${syncNowCommand.title.original}`, description: syncNowCommand.description(that.userDataSyncService) });
if (that.userDataSyncEnablementService.canToggleEnablement()) {
const account = that.userDataSyncWorkbenchService.current;
items.push({ id: turnOffSyncCommand.id, label: `${SYNC_TITLE.value}: ${turnOffSyncCommand.title.original}`, description: account ? `${account.accountName} (${that.authenticationService.getLabel(account.authenticationProviderId)})` : undefined });
items.push({ id: turnOffSyncCommand.id, label: `${SYNC_TITLE.value}: ${turnOffSyncCommand.title.original}`, description: account ? `${account.accountName} (${that.authenticationService.getProvider(account.authenticationProviderId).label})` : undefined });
}
quickPick.items = items;
disposables.add(quickPick.onDidAccept(() => {

View File

@@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IProductService } from 'vs/platform/product/common/productService';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { AllowedExtension } from 'vs/workbench/services/authentication/common/authentication';
export const IAuthenticationAccessService = createDecorator<IAuthenticationAccessService>('IAuthenticationAccessService');
export interface IAuthenticationAccessService {
readonly _serviceBrand: undefined;
readonly onDidChangeExtensionSessionAccess: Event<{ providerId: string; accountName: string }>;
/**
* Check extension access to an account
* @param providerId The id of the authentication provider
* @param accountName The account name that access is checked for
* @param extensionId The id of the extension requesting access
* @returns Returns true or false if the user has opted to permanently grant or disallow access, and undefined
* if they haven't made a choice yet
*/
isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined;
readAllowedExtensions(providerId: string, accountName: string): AllowedExtension[];
updateAllowedExtensions(providerId: string, accountName: string, extensions: AllowedExtension[]): void;
removeAllowedExtensions(providerId: string, accountName: string): void;
}
// TODO@TylerLeonhardt: Move this class to MainThreadAuthentication
export class AuthenticationAccessService extends Disposable implements IAuthenticationAccessService {
_serviceBrand: undefined;
private _onDidChangeExtensionSessionAccess: Emitter<{ providerId: string; accountName: string }> = this._register(new Emitter<{ providerId: string; accountName: string }>());
readonly onDidChangeExtensionSessionAccess: Event<{ providerId: string; accountName: string }> = this._onDidChangeExtensionSessionAccess.event;
constructor(
@IStorageService private readonly _storageService: IStorageService,
@IProductService private readonly _productService: IProductService
) {
super();
}
isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined {
const trustedExtensionAuthAccess = this._productService.trustedExtensionAuthAccess;
if (Array.isArray(trustedExtensionAuthAccess)) {
if (trustedExtensionAuthAccess.includes(extensionId)) {
return true;
}
} else if (trustedExtensionAuthAccess?.[providerId]?.includes(extensionId)) {
return true;
}
const allowList = this.readAllowedExtensions(providerId, accountName);
const extensionData = allowList.find(extension => extension.id === extensionId);
if (!extensionData) {
return undefined;
}
// This property didn't exist on this data previously, inclusion in the list at all indicates allowance
return extensionData.allowed !== undefined
? extensionData.allowed
: true;
}
readAllowedExtensions(providerId: string, accountName: string): AllowedExtension[] {
let trustedExtensions: AllowedExtension[] = [];
try {
const trustedExtensionSrc = this._storageService.get(`${providerId}-${accountName}`, StorageScope.APPLICATION);
if (trustedExtensionSrc) {
trustedExtensions = JSON.parse(trustedExtensionSrc);
}
} catch (err) { }
return trustedExtensions;
}
updateAllowedExtensions(providerId: string, accountName: string, extensions: AllowedExtension[]): void {
const allowList = this.readAllowedExtensions(providerId, accountName);
for (const extension of extensions) {
const index = allowList.findIndex(e => e.id === extension.id);
if (index === -1) {
allowList.push(extension);
} else {
allowList[index].allowed = extension.allowed;
}
}
this._storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.APPLICATION, StorageTarget.USER);
this._onDidChangeExtensionSessionAccess.fire({ providerId, accountName });
}
removeAllowedExtensions(providerId: string, accountName: string): void {
this._storageService.remove(`${providerId}-${accountName}`, StorageScope.APPLICATION);
this._onDidChangeExtensionSessionAccess.fire({ providerId, accountName });
}
}
registerSingleton(IAuthenticationAccessService, AuthenticationAccessService, InstantiationType.Delayed);

View File

@@ -0,0 +1,418 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, dispose, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import * as nls from 'vs/nls';
import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Severity } from 'vs/platform/notification/common/notification';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService';
import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService';
import { AuthenticationSession, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService } from 'vs/workbench/services/authentication/common/authentication';
// OAuth2 spec prohibits space in a scope, so use that to join them.
const SCOPESLIST_SEPARATOR = ' ';
interface SessionRequest {
disposables: IDisposable[];
requestingExtensionIds: string[];
}
interface SessionRequestInfo {
[scopesList: string]: SessionRequest;
}
// TODO@TylerLeonhardt: This should all go in MainThreadAuthentication
export class AuthenticationExtensionsService extends Disposable implements IAuthenticationExtensionsService {
declare readonly _serviceBrand: undefined;
private _signInRequestItems = new Map<string, SessionRequestInfo>();
private _sessionAccessRequestItems = new Map<string, { [extensionId: string]: { disposables: IDisposable[]; possibleSessions: AuthenticationSession[] } }>();
private _accountBadgeDisposable = this._register(new MutableDisposable());
constructor(
@IActivityService private readonly activityService: IActivityService,
@IStorageService private readonly storageService: IStorageService,
@IDialogService private readonly dialogService: IDialogService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
@IAuthenticationUsageService private readonly _authenticationUsageService: IAuthenticationUsageService,
@IAuthenticationAccessService private readonly _authenticationAccessService: IAuthenticationAccessService
) {
super();
this.registerListeners();
}
private registerListeners() {
this._register(this._authenticationService.onDidChangeSessions(async e => {
if (e.event.added?.length) {
await this.updateNewSessionRequests(e.providerId, e.event.added);
}
if (e.event.removed?.length) {
await this.updateAccessRequests(e.providerId, e.event.removed);
}
this.updateBadgeCount();
}));
this._register(this._authenticationService.onDidUnregisterAuthenticationProvider(e => {
const accessRequests = this._sessionAccessRequestItems.get(e.id) || {};
Object.keys(accessRequests).forEach(extensionId => {
this.removeAccessRequest(e.id, extensionId);
});
}));
}
private async updateNewSessionRequests(providerId: string, addedSessions: readonly AuthenticationSession[]): Promise<void> {
const existingRequestsForProvider = this._signInRequestItems.get(providerId);
if (!existingRequestsForProvider) {
return;
}
Object.keys(existingRequestsForProvider).forEach(requestedScopes => {
if (addedSessions.some(session => session.scopes.slice().join(SCOPESLIST_SEPARATOR) === requestedScopes)) {
const sessionRequest = existingRequestsForProvider[requestedScopes];
sessionRequest?.disposables.forEach(item => item.dispose());
delete existingRequestsForProvider[requestedScopes];
if (Object.keys(existingRequestsForProvider).length === 0) {
this._signInRequestItems.delete(providerId);
} else {
this._signInRequestItems.set(providerId, existingRequestsForProvider);
}
}
});
}
private async updateAccessRequests(providerId: string, removedSessions: readonly AuthenticationSession[]) {
const providerRequests = this._sessionAccessRequestItems.get(providerId);
if (providerRequests) {
Object.keys(providerRequests).forEach(extensionId => {
removedSessions.forEach(removed => {
const indexOfSession = providerRequests[extensionId].possibleSessions.findIndex(session => session.id === removed.id);
if (indexOfSession) {
providerRequests[extensionId].possibleSessions.splice(indexOfSession, 1);
}
});
if (!providerRequests[extensionId].possibleSessions.length) {
this.removeAccessRequest(providerId, extensionId);
}
});
}
}
private updateBadgeCount(): void {
this._accountBadgeDisposable.clear();
let numberOfRequests = 0;
this._signInRequestItems.forEach(providerRequests => {
Object.keys(providerRequests).forEach(request => {
numberOfRequests += providerRequests[request].requestingExtensionIds.length;
});
});
this._sessionAccessRequestItems.forEach(accessRequest => {
numberOfRequests += Object.keys(accessRequest).length;
});
if (numberOfRequests > 0) {
const badge = new NumberBadge(numberOfRequests, () => nls.localize('sign in', "Sign in requested"));
this._accountBadgeDisposable.value = this.activityService.showAccountsActivity({ badge });
}
}
private removeAccessRequest(providerId: string, extensionId: string): void {
const providerRequests = this._sessionAccessRequestItems.get(providerId) || {};
if (providerRequests[extensionId]) {
dispose(providerRequests[extensionId].disposables);
delete providerRequests[extensionId];
this.updateBadgeCount();
}
}
//#region Session Preference
updateSessionPreference(providerId: string, extensionId: string, session: AuthenticationSession): void {
// The 3 parts of this key are important:
// * Extension id: The extension that has a preference
// * Provider id: The provider that the preference is for
// * The scopes: The subset of sessions that the preference applies to
const key = `${extensionId}-${providerId}-${session.scopes.join(' ')}`;
// Store the preference in the workspace and application storage. This allows new workspaces to
// have a preference set already to limit the number of prompts that are shown... but also allows
// a specific workspace to override the global preference.
this.storageService.store(key, session.id, StorageScope.WORKSPACE, StorageTarget.MACHINE);
this.storageService.store(key, session.id, StorageScope.APPLICATION, StorageTarget.MACHINE);
}
getSessionPreference(providerId: string, extensionId: string, scopes: string[]): string | undefined {
// The 3 parts of this key are important:
// * Extension id: The extension that has a preference
// * Provider id: The provider that the preference is for
// * The scopes: The subset of sessions that the preference applies to
const key = `${extensionId}-${providerId}-${scopes.join(' ')}`;
// If a preference is set in the workspace, use that. Otherwise, use the global preference.
return this.storageService.get(key, StorageScope.WORKSPACE) ?? this.storageService.get(key, StorageScope.APPLICATION);
}
removeSessionPreference(providerId: string, extensionId: string, scopes: string[]): void {
// The 3 parts of this key are important:
// * Extension id: The extension that has a preference
// * Provider id: The provider that the preference is for
// * The scopes: The subset of sessions that the preference applies to
const key = `${extensionId}-${providerId}-${scopes.join(' ')}`;
// This won't affect any other workspaces that have a preference set, but it will remove the preference
// for this workspace and the global preference. This is only paired with a call to updateSessionPreference...
// so we really don't _need_ to remove them as they are about to be overridden anyway... but it's more correct
// to remove them first... and in case this gets called from somewhere else in the future.
this.storageService.remove(key, StorageScope.WORKSPACE);
this.storageService.remove(key, StorageScope.APPLICATION);
}
//#endregion
private async showGetSessionPrompt(provider: IAuthenticationProvider, accountName: string, extensionId: string, extensionName: string): Promise<boolean> {
enum SessionPromptChoice {
Allow = 0,
Deny = 1,
Cancel = 2
}
const { result } = await this.dialogService.prompt<SessionPromptChoice>({
type: Severity.Info,
message: nls.localize('confirmAuthenticationAccess', "The extension '{0}' wants to access the {1} account '{2}'.", extensionName, provider.label, accountName),
buttons: [
{
label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"),
run: () => SessionPromptChoice.Allow
},
{
label: nls.localize({ key: 'deny', comment: ['&& denotes a mnemonic'] }, "&&Deny"),
run: () => SessionPromptChoice.Deny
}
],
cancelButton: {
run: () => SessionPromptChoice.Cancel
}
});
if (result !== SessionPromptChoice.Cancel) {
this._authenticationAccessService.updateAllowedExtensions(provider.id, accountName, [{ id: extensionId, name: extensionName, allowed: result === SessionPromptChoice.Allow }]);
this.removeAccessRequest(provider.id, extensionId);
}
return result === SessionPromptChoice.Allow;
}
async selectSession(providerId: string, extensionId: string, extensionName: string, scopes: string[], availableSessions: AuthenticationSession[]): Promise<AuthenticationSession> {
return new Promise((resolve, reject) => {
// This function should be used only when there are sessions to disambiguate.
if (!availableSessions.length) {
reject('No available sessions');
return;
}
const quickPick = this.quickInputService.createQuickPick<{ label: string; session?: AuthenticationSession }>();
quickPick.ignoreFocusOut = true;
const items: { label: string; session?: AuthenticationSession }[] = availableSessions.map(session => {
return {
label: session.account.label,
session: session
};
});
items.push({
label: nls.localize('useOtherAccount', "Sign in to another account")
});
quickPick.items = items;
quickPick.title = nls.localize(
{
key: 'selectAccount',
comment: ['The placeholder {0} is the name of an extension. {1} is the name of the type of account, such as Microsoft or GitHub.']
},
"The extension '{0}' wants to access a {1} account",
extensionName,
this._authenticationService.getProvider(providerId).label);
quickPick.placeholder = nls.localize('getSessionPlateholder', "Select an account for '{0}' to use or Esc to cancel", extensionName);
quickPick.onDidAccept(async _ => {
const session = quickPick.selectedItems[0].session ?? await this._authenticationService.createSession(providerId, scopes);
const accountName = session.account.label;
this._authenticationAccessService.updateAllowedExtensions(providerId, accountName, [{ id: extensionId, name: extensionName, allowed: true }]);
this.updateSessionPreference(providerId, extensionId, session);
this.removeAccessRequest(providerId, extensionId);
quickPick.dispose();
resolve(session);
});
quickPick.onDidHide(_ => {
if (!quickPick.selectedItems[0]) {
reject('User did not consent to account access');
}
quickPick.dispose();
});
quickPick.show();
});
}
private async completeSessionAccessRequest(provider: IAuthenticationProvider, extensionId: string, extensionName: string, scopes: string[]): Promise<void> {
const providerRequests = this._sessionAccessRequestItems.get(provider.id) || {};
const existingRequest = providerRequests[extensionId];
if (!existingRequest) {
return;
}
if (!provider) {
return;
}
const possibleSessions = existingRequest.possibleSessions;
let session: AuthenticationSession | undefined;
if (provider.supportsMultipleAccounts) {
try {
session = await this.selectSession(provider.id, extensionId, extensionName, scopes, possibleSessions);
} catch (_) {
// ignore cancel
}
} else {
const approved = await this.showGetSessionPrompt(provider, possibleSessions[0].account.label, extensionId, extensionName);
if (approved) {
session = possibleSessions[0];
}
}
if (session) {
this._authenticationUsageService.addAccountUsage(provider.id, session.account.label, extensionId, extensionName);
}
}
requestSessionAccess(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: AuthenticationSession[]): void {
const providerRequests = this._sessionAccessRequestItems.get(providerId) || {};
const hasExistingRequest = providerRequests[extensionId];
if (hasExistingRequest) {
return;
}
const provider = this._authenticationService.getProvider(providerId);
const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
group: '3_accessRequests',
command: {
id: `${providerId}${extensionId}Access`,
title: nls.localize({
key: 'accessRequest',
comment: [`The placeholder {0} will be replaced with an authentication provider''s label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count`]
},
"Grant access to {0} for {1}... (1)",
provider.label,
extensionName)
}
});
const accessCommand = CommandsRegistry.registerCommand({
id: `${providerId}${extensionId}Access`,
handler: async (accessor) => {
this.completeSessionAccessRequest(provider, extensionId, extensionName, scopes);
}
});
providerRequests[extensionId] = { possibleSessions, disposables: [menuItem, accessCommand] };
this._sessionAccessRequestItems.set(providerId, providerRequests);
this.updateBadgeCount();
}
async requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise<void> {
if (!this._authenticationService.isAuthenticationProviderRegistered(providerId)) {
// Activate has already been called for the authentication provider, but it cannot block on registering itself
// since this is sync and returns a disposable. So, wait for registration event to fire that indicates the
// provider is now in the map.
await new Promise<void>((resolve, _) => {
const dispose = this._authenticationService.onDidRegisterAuthenticationProvider(e => {
if (e.id === providerId) {
dispose.dispose();
resolve();
}
});
});
}
let provider: IAuthenticationProvider;
try {
provider = this._authenticationService.getProvider(providerId);
} catch (_e) {
return;
}
const providerRequests = this._signInRequestItems.get(providerId);
const scopesList = scopes.join(SCOPESLIST_SEPARATOR);
const extensionHasExistingRequest = providerRequests
&& providerRequests[scopesList]
&& providerRequests[scopesList].requestingExtensionIds.includes(extensionId);
if (extensionHasExistingRequest) {
return;
}
// Construct a commandId that won't clash with others generated here, nor likely with an extension's command
const commandId = `${providerId}:${extensionId}:signIn${Object.keys(providerRequests || []).length}`;
const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
group: '2_signInRequests',
command: {
id: commandId,
title: nls.localize({
key: 'signInRequest',
comment: [`The placeholder {0} will be replaced with an authentication provider's label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count.`]
},
"Sign in with {0} to use {1} (1)",
provider.label,
extensionName)
}
});
const signInCommand = CommandsRegistry.registerCommand({
id: commandId,
handler: async (accessor) => {
const authenticationService = accessor.get(IAuthenticationService);
const session = await authenticationService.createSession(providerId, scopes);
this._authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]);
this.updateSessionPreference(providerId, extensionId, session);
}
});
if (providerRequests) {
const existingRequest = providerRequests[scopesList] || { disposables: [], requestingExtensionIds: [] };
providerRequests[scopesList] = {
disposables: [...existingRequest.disposables, menuItem, signInCommand],
requestingExtensionIds: [...existingRequest.requestingExtensionIds, extensionId]
};
this._signInRequestItems.set(providerId, providerRequests);
} else {
this._signInRequestItems.set(providerId, {
[scopesList]: {
disposables: [menuItem, signInCommand],
requestingExtensionIds: [extensionId]
}
});
}
this.updateBadgeCount();
}
}
registerSingleton(IAuthenticationExtensionsService, AuthenticationExtensionsService, InstantiationType.Delayed);

View File

@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
export interface IAccountUsage {
extensionId: string;
extensionName: string;
lastUsed: number;
}
export const IAuthenticationUsageService = createDecorator<IAuthenticationUsageService>('IAuthenticationUsageService');
export interface IAuthenticationUsageService {
readonly _serviceBrand: undefined;
readAccountUsages(providerId: string, accountName: string,): IAccountUsage[];
removeAccountUsage(providerId: string, accountName: string): void;
addAccountUsage(providerId: string, accountName: string, extensionId: string, extensionName: string): void;
}
export class AuthenticationUsageService implements IAuthenticationUsageService {
_serviceBrand: undefined;
constructor(@IStorageService private readonly _storageService: IStorageService) { }
readAccountUsages(providerId: string, accountName: string): IAccountUsage[] {
const accountKey = `${providerId}-${accountName}-usages`;
const storedUsages = this._storageService.get(accountKey, StorageScope.APPLICATION);
let usages: IAccountUsage[] = [];
if (storedUsages) {
try {
usages = JSON.parse(storedUsages);
} catch (e) {
// ignore
}
}
return usages;
}
removeAccountUsage(providerId: string, accountName: string): void {
const accountKey = `${providerId}-${accountName}-usages`;
this._storageService.remove(accountKey, StorageScope.APPLICATION);
}
addAccountUsage(providerId: string, accountName: string, extensionId: string, extensionName: string): void {
const accountKey = `${providerId}-${accountName}-usages`;
const usages = this.readAccountUsages(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()
});
}
this._storageService.store(accountKey, JSON.stringify(usages), StorageScope.APPLICATION, StorageTarget.MACHINE);
}
}
registerSingleton(IAuthenticationUsageService, AuthenticationUsageService, InstantiationType.Delayed);

View File

@@ -58,40 +58,108 @@ export const IAuthenticationService = createDecorator<IAuthenticationService>('I
export interface IAuthenticationService {
readonly _serviceBrand: undefined;
/**
* Fires when an authentication provider has been registered
*/
readonly onDidRegisterAuthenticationProvider: Event<AuthenticationProviderInformation>;
/**
* Fires when an authentication provider has been unregistered
*/
readonly onDidUnregisterAuthenticationProvider: Event<AuthenticationProviderInformation>;
/**
* Fires when the list of sessions for a provider has been added, removed or changed
*/
readonly onDidChangeSessions: Event<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }>;
/**
* Fires when the list of declaredProviders has changed
*/
readonly onDidChangeDeclaredProviders: Event<void>;
/**
* All providers that have been statically declared by extensions. These may not actually be registered or active yet.
*/
readonly declaredProviders: AuthenticationProviderInformation[];
/**
* Registers that an extension has declared an authentication provider in their package.json
* @param provider The provider information to register
*/
registerDeclaredAuthenticationProvider(provider: AuthenticationProviderInformation): void;
/**
* Unregisters a declared authentication provider
* @param id The id of the provider to unregister
*/
unregisterDeclaredAuthenticationProvider(id: string): void;
/**
* Checks if an authentication provider has been registered
* @param id The id of the provider to check
*/
isAuthenticationProviderRegistered(id: string): boolean;
getProviderIds(): string[];
/**
* Registers an authentication provider
* @param id The id of the provider
* @param provider The implementation of the provider
*/
registerAuthenticationProvider(id: string, provider: IAuthenticationProvider): void;
/**
* Unregisters an authentication provider
* @param id The id of the provider to unregister
*/
unregisterAuthenticationProvider(id: string): void;
isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined;
updateAllowedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string, isAllowed: boolean): void;
/**
* Gets the provider ids of all registered authentication providers
*/
getProviderIds(): string[];
/**
* Gets the provider with the given id.
* @param id The id of the provider to get
* @throws if the provider is not registered
*/
getProvider(id: string): IAuthenticationProvider;
/**
* Gets all sessions that satisfy the given scopes from the provider with the given id
* @param id The id of the provider to ask for a session
* @param scopes The scopes for the session
* @param activateImmediate If true, the provider should activate immediately if it is not already
*/
getSessions(id: string, scopes?: string[], activateImmediate?: boolean): Promise<ReadonlyArray<AuthenticationSession>>;
/**
* Creates an AuthenticationSession with the given provider and scopes
* @param providerId The id of the provider
* @param scopes The scopes to request
* @param options Additional options for creating the session
*/
createSession(providerId: string, scopes: string[], options?: IAuthenticationCreateSessionOptions): Promise<AuthenticationSession>;
/**
* Removes the session with the given id from the provider with the given id
* @param providerId The id of the provider
* @param sessionId The id of the session to remove
*/
removeSession(providerId: string, sessionId: string): Promise<void>;
}
// TODO: Move this into MainThreadAuthentication
export const IAuthenticationExtensionsService = createDecorator<IAuthenticationExtensionsService>('IAuthenticationExtensionsService');
export interface IAuthenticationExtensionsService {
readonly _serviceBrand: undefined;
updateSessionPreference(providerId: string, extensionId: string, session: AuthenticationSession): void;
getSessionPreference(providerId: string, extensionId: string, scopes: string[]): string | undefined;
removeSessionPreference(providerId: string, extensionId: string, scopes: string[]): void;
showGetSessionPrompt(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise<boolean>;
selectSession(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: readonly AuthenticationSession[]): Promise<AuthenticationSession>;
requestSessionAccess(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: readonly AuthenticationSession[]): void;
completeSessionAccessRequest(providerId: string, extensionId: string, extensionName: string, scopes: string[]): Promise<void>;
requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise<void>;
readonly onDidRegisterAuthenticationProvider: Event<AuthenticationProviderInformation>;
readonly onDidUnregisterAuthenticationProvider: Event<AuthenticationProviderInformation>;
readonly onDidChangeSessions: Event<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }>;
readonly onDidChangeExtensionSessionAccess: Event<{ providerId: string; accountName: string }>;
// TODO completely remove this property
declaredProviders: AuthenticationProviderInformation[];
readonly onDidChangeDeclaredProviders: Event<AuthenticationProviderInformation[]>;
getSessions(id: string, scopes?: string[], activateImmediate?: boolean): Promise<ReadonlyArray<AuthenticationSession>>;
getLabel(providerId: string): string;
supportsMultipleAccounts(providerId: string): boolean;
createSession(providerId: string, scopes: string[], options?: IAuthenticationCreateSessionOptions): Promise<AuthenticationSession>;
removeSession(providerId: string, sessionId: string): Promise<void>;
manageTrustedExtensionsForAccount(providerId: string, accountName: string): Promise<void>;
readAllowedExtensions(providerId: string, accountName: string): AllowedExtension[];
removeAccountSessions(providerId: string, accountName: string, sessions: AuthenticationSession[]): Promise<void>;
}
export interface IAuthenticationProviderCreateSessionOptions {

View File

@@ -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 * as assert from 'assert';
import { Emitter, Event } from 'vs/base/common/event';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { AuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService';
import { AuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService';
import { AuthenticationProviderInformation, AuthenticationSessionsChangeEvent, IAuthenticationProvider } from 'vs/workbench/services/authentication/common/authentication';
import { TestExtensionService, TestProductService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
function createSession() {
return { id: 'session1', accessToken: 'token1', account: { id: 'account', label: 'Account' }, scopes: ['test'] };
}
function createProvider(overrides: Partial<IAuthenticationProvider> = {}): IAuthenticationProvider {
return {
supportsMultipleAccounts: false,
onDidChangeSessions: new Emitter<AuthenticationSessionsChangeEvent>().event,
id: 'test',
label: 'Test',
getSessions: async () => [],
createSession: async () => createSession(),
removeSession: async () => { },
...overrides
};
}
suite('AuthenticationService', () => {
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
let authenticationService: AuthenticationService;
setup(() => {
const storageService = disposables.add(new TestStorageService());
const authenticationAccessService = disposables.add(new AuthenticationAccessService(storageService, TestProductService));
authenticationService = disposables.add(new AuthenticationService(new TestExtensionService(), authenticationAccessService));
});
teardown(() => {
// Dispose the authentication service after each test
authenticationService.dispose();
});
suite('declaredAuthenticationProviders', () => {
test('registerDeclaredAuthenticationProvider', async () => {
const changed = Event.toPromise(authenticationService.onDidChangeDeclaredProviders);
const provider: AuthenticationProviderInformation = {
id: 'github',
label: 'GitHub'
};
authenticationService.registerDeclaredAuthenticationProvider(provider);
// Assert that the provider is added to the declaredProviders array and the event fires
assert.equal(authenticationService.declaredProviders.length, 1);
assert.deepEqual(authenticationService.declaredProviders[0], provider);
await changed;
});
test('unregisterDeclaredAuthenticationProvider', async () => {
const provider: AuthenticationProviderInformation = {
id: 'github',
label: 'GitHub'
};
authenticationService.registerDeclaredAuthenticationProvider(provider);
const changed = Event.toPromise(authenticationService.onDidChangeDeclaredProviders);
authenticationService.unregisterDeclaredAuthenticationProvider(provider.id);
// Assert that the provider is removed from the declaredProviders array and the event fires
assert.equal(authenticationService.declaredProviders.length, 0);
await changed;
});
});
suite('authenticationProviders', () => {
test('isAuthenticationProviderRegistered', async () => {
const registered = Event.toPromise(authenticationService.onDidRegisterAuthenticationProvider);
const provider = createProvider();
assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), false);
authenticationService.registerAuthenticationProvider(provider.id, provider);
assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), true);
const result = await registered;
assert.deepEqual(result, { id: provider.id, label: provider.label });
});
test('unregisterAuthenticationProvider', async () => {
const unregistered = Event.toPromise(authenticationService.onDidUnregisterAuthenticationProvider);
const provider = createProvider();
authenticationService.registerAuthenticationProvider(provider.id, provider);
assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), true);
authenticationService.unregisterAuthenticationProvider(provider.id);
assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), false);
const result = await unregistered;
assert.deepEqual(result, { id: provider.id, label: provider.label });
});
test('getProviderIds', () => {
const provider1 = createProvider({
id: 'provider1',
label: 'Provider 1'
});
const provider2 = createProvider({
id: 'provider2',
label: 'Provider 2'
});
authenticationService.registerAuthenticationProvider(provider1.id, provider1);
authenticationService.registerAuthenticationProvider(provider2.id, provider2);
const providerIds = authenticationService.getProviderIds();
// Assert that the providerIds array contains the registered provider ids
assert.deepEqual(providerIds, [provider1.id, provider2.id]);
});
test('getProvider', () => {
const provider = createProvider();
authenticationService.registerAuthenticationProvider(provider.id, provider);
const retrievedProvider = authenticationService.getProvider(provider.id);
// Assert that the retrieved provider is the same as the registered provider
assert.deepEqual(retrievedProvider, provider);
});
});
suite('authenticationSessions', () => {
test('getSessions', async () => {
let isCalled = false;
const provider = createProvider({
getSessions: async () => {
isCalled = true;
return [createSession()];
},
});
authenticationService.registerAuthenticationProvider(provider.id, provider);
const sessions = await authenticationService.getSessions(provider.id);
assert.equal(sessions.length, 1);
assert.ok(isCalled);
});
test('createSession', async () => {
const emitter = new Emitter<AuthenticationSessionsChangeEvent>();
const provider = createProvider({
onDidChangeSessions: emitter.event,
createSession: async () => {
const session = createSession();
emitter.fire({ added: [session], removed: [], changed: [] });
return session;
},
});
const changed = Event.toPromise(authenticationService.onDidChangeSessions);
authenticationService.registerAuthenticationProvider(provider.id, provider);
const session = await authenticationService.createSession(provider.id, ['repo']);
// Assert that the created session matches the expected session and the event fires
assert.ok(session);
const result = await changed;
assert.deepEqual(result, {
providerId: provider.id,
label: provider.label,
event: { added: [session], removed: [], changed: [] }
});
});
test('removeSession', async () => {
const emitter = new Emitter<AuthenticationSessionsChangeEvent>();
const session = createSession();
const provider = createProvider({
onDidChangeSessions: emitter.event,
removeSession: async () => emitter.fire({ added: [], removed: [session], changed: [] })
});
const changed = Event.toPromise(authenticationService.onDidChangeSessions);
authenticationService.registerAuthenticationProvider(provider.id, provider);
await authenticationService.removeSession(provider.id, session.id);
const result = await changed;
assert.deepEqual(result, {
providerId: provider.id,
label: provider.label,
event: { added: [], removed: [session], changed: [] }
});
});
test('onDidChangeSessions', async () => {
const emitter = new Emitter<AuthenticationSessionsChangeEvent>();
const provider = createProvider({
onDidChangeSessions: emitter.event,
getSessions: async () => []
});
authenticationService.registerAuthenticationProvider(provider.id, provider);
const changed = Event.toPromise(authenticationService.onDidChangeSessions);
const session = createSession();
emitter.fire({ added: [], removed: [], changed: [session] });
const result = await changed;
assert.deepEqual(result, {
providerId: provider.id,
label: provider.label,
event: { added: [], removed: [], changed: [session] }
});
});
});
});

View File

@@ -641,7 +641,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
quickPickItems.push({ type: 'separator', label: localize('signed in', "Signed in") });
for (const authenticationProvider of authenticationProviders) {
const accounts = (allAccounts.get(authenticationProvider.id) || []).sort(({ sessionId }) => sessionId === this.currentSessionId ? -1 : 1);
const providerName = this.authenticationService.getLabel(authenticationProvider.id);
const providerName = this.authenticationService.getProvider(authenticationProvider.id).label;
for (const account of accounts) {
quickPickItems.push({
label: `${account.accountName} (${providerName})`,
@@ -656,8 +656,9 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
// Account Providers
for (const authenticationProvider of authenticationProviders) {
if (!allAccounts.has(authenticationProvider.id) || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) {
const providerName = this.authenticationService.getLabel(authenticationProvider.id);
const provider = this.authenticationService.getProvider(authenticationProvider.id);
if (!allAccounts.has(authenticationProvider.id) || provider.supportsMultipleAccounts) {
const providerName = provider.label;
quickPickItems.push({ label: localize('sign in using account', "Sign in with {0}", providerName), authenticationProvider });
}
}

View File

@@ -104,6 +104,9 @@ import 'vs/workbench/services/views/browser/viewsService';
import 'vs/workbench/services/quickinput/browser/quickInputService';
import 'vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService';
import 'vs/workbench/services/authentication/browser/authenticationService';
import 'vs/workbench/services/authentication/browser/authenticationExtensionsService';
import 'vs/workbench/services/authentication/browser/authenticationUsageService';
import 'vs/workbench/services/authentication/browser/authenticationAccessService';
import 'vs/editor/browser/services/hoverService/hoverService';
import 'vs/workbench/services/assignment/common/assignmentService';
import 'vs/workbench/services/outline/browser/outlineService';
@@ -341,6 +344,9 @@ import 'vs/workbench/contrib/languageDetection/browser/languageDetection.contrib
// Language Status
import 'vs/workbench/contrib/languageStatus/browser/languageStatus.contribution';
// Authentication
import 'vs/workbench/contrib/authentication/browser/authentication.contribution';
// User Data Sync
import 'vs/workbench/contrib/userDataSync/browser/userDataSync.contribution';