mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 12:04:04 +01:00
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:
committed by
GitHub
parent
15a0f3c48f
commit
4e81df7ea9
@@ -553,6 +553,10 @@
|
||||
{
|
||||
"name": "vs/workbench/contrib/accountEntitlements",
|
||||
"project": "vscode-workbench"
|
||||
},
|
||||
{
|
||||
"name": "vs/workbench/contrib/authentication",
|
||||
"project": "vscode-workbench"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
@@ -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 {
|
||||
|
||||
@@ -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] }
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user