diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index c046e3c83a9c1..fc027ddf329cd 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -281,17 +281,32 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu const session = await this.doGetSession(providerId, scopes, extensionId, extensionName, options); if (session) { - type AuthProviderUsageClassification = { - owner: 'TylerLeonhardt'; - comment: 'Used to see which extensions are using which providers'; - extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id.' }; - providerId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider id.' }; - }; - this.telemetryService.publicLog2<{ extensionId: string; providerId: string }, AuthProviderUsageClassification>('authentication.providerUsage', { providerId, extensionId }); - + this.sendProviderUsageTelemetry(extensionId, providerId); addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName); } return session; } + + async $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise { + const sessions = await this.authenticationService.getSessions(providerId, [...scopes], true); + const accessibleSessions = sessions.filter(s => this.authenticationService.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); + } + } + return accessibleSessions; + } + + private sendProviderUsageTelemetry(extensionId: string, providerId: string): void { + type AuthProviderUsageClassification = { + owner: 'TylerLeonhardt'; + comment: 'Used to see which extensions are using which providers'; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id.' }; + providerId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider id.' }; + }; + this.telemetryService.publicLog2<{ extensionId: string; providerId: string }, AuthProviderUsageClassification>('authentication.providerUsage', { providerId, extensionId }); + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index a8ad831840c2b..5c7056e1f18b3 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -247,6 +247,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I getSession(providerId: string, scopes: readonly string[], options?: vscode.AuthenticationGetSessionOptions) { return extHostAuthentication.getSession(extension, providerId, scopes, options as any); }, + getSessions(providerId: string, scopes: readonly string[]) { + checkProposedApiEnabled(extension, 'getSessions'); + return extHostAuthentication.getSessions(extension, providerId, scopes); + }, // TODO: remove this after GHPR and Codespaces move off of it async hasSession(providerId: string, scopes: readonly string[]) { checkProposedApiEnabled(extension, 'authSession'); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index eaeefd7588d06..2e4a46474ea10 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -150,6 +150,7 @@ export interface MainThreadAuthenticationShape extends IDisposable { $ensureProvider(id: string): Promise; $sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): void; $getSession(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string, options: { createIfNone?: boolean; forceNewSession?: boolean | { detail: string }; clearSessionPreference?: boolean }): Promise; + $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise; $removeSession(providerId: string, sessionId: string): Promise; } diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index ad9749b474e5e..ac8e9b29c11d7 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -9,12 +9,6 @@ import { IMainContext, MainContext, MainThreadAuthenticationShape, ExtHostAuthen import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import { IExtensionDescription, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -interface GetSessionsRequest { - scopes: string; - providerId: string; - result: Promise; -} - interface ProviderWithMetadata { label: string; provider: vscode.AuthenticationProvider; @@ -30,7 +24,8 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { private _onDidChangeSessions = new Emitter(); readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; - private _inFlightRequests = new Map(); + private _getSessionTaskSingler = new TaskSingler(); + private _getSessionsTaskSingler = new TaskSingler>(); constructor(mainContext: IMainContext) { this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication); @@ -47,41 +42,22 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: readonly string[], options: vscode.AuthenticationGetSessionOptions): Promise; async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: readonly string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); - const inFlightRequests = this._inFlightRequests.get(extensionId) || []; const sortedScopes = [...scopes].sort().join(' '); - let inFlightRequest: GetSessionsRequest | undefined = inFlightRequests.find(request => request.providerId === providerId && request.scopes === sortedScopes); - - if (inFlightRequest) { - return inFlightRequest.result; - } else { - const session = this._getSession(requestingExtension, extensionId, providerId, scopes, options); - inFlightRequest = { - providerId, - scopes: sortedScopes, - result: session - }; - - inFlightRequests.push(inFlightRequest); - this._inFlightRequests.set(extensionId, inFlightRequests); - - try { - await session; - } finally { - const requestIndex = inFlightRequests.findIndex(request => request.providerId === providerId && request.scopes === sortedScopes); - if (requestIndex > -1) { - inFlightRequests.splice(requestIndex); - this._inFlightRequests.set(extensionId, inFlightRequests); - } - } - - return session; - } + return await this._getSessionTaskSingler.getOrCreate(`${extensionId} ${sortedScopes}`, async () => { + await this._proxy.$ensureProvider(providerId); + const extensionName = requestingExtension.displayName || requestingExtension.name; + return this._proxy.$getSession(providerId, scopes, extensionId, extensionName, options); + }); } - private async _getSession(requestingExtension: IExtensionDescription, extensionId: string, providerId: string, scopes: readonly string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise { - await this._proxy.$ensureProvider(providerId); - const extensionName = requestingExtension.displayName || requestingExtension.name; - return this._proxy.$getSession(providerId, scopes, extensionId, extensionName, options); + async getSessions(requestingExtension: IExtensionDescription, providerId: string, scopes: readonly string[]): Promise> { + const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); + const sortedScopes = [...scopes].sort().join(' '); + return await this._getSessionsTaskSingler.getOrCreate(`${extensionId} ${sortedScopes}`, async () => { + await this._proxy.$ensureProvider(providerId); + const extensionName = requestingExtension.displayName || requestingExtension.name; + return this._proxy.$getSessions(providerId, scopes, extensionId, extensionName); + }); } async removeSession(providerId: string, sessionId: string): Promise { @@ -162,3 +138,18 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { return Promise.resolve(); } } + +class TaskSingler { + private _inFlightPromises = new Map>(); + getOrCreate(key: string, promiseFactory: () => Promise) { + const inFlight = this._inFlightPromises.get(key); + if (inFlight) { + return inFlight; + } + + const promise = promiseFactory().finally(() => this._inFlightPromises.delete(key)); + this._inFlightPromises.set(key, promise); + + return promise; + } +} diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 9c9f813fd44aa..7d7b31d03f456 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -36,6 +36,7 @@ export const allApiProposals = Object.freeze({ fileSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts', findTextInFiles: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findTextInFiles.d.ts', fsChunks: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fsChunks.d.ts', + getSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.getSessions.d.ts', idToken: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.idToken.d.ts', indentSize: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.indentSize.d.ts', inlineCompletionsAdditions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts', diff --git a/src/vscode-dts/vscode.proposed.getSessions.d.ts b/src/vscode-dts/vscode.proposed.getSessions.d.ts new file mode 100644 index 0000000000000..f94ab74d821e8 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.getSessions.d.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/152399 + + export namespace authentication { + /** + * Get all authentication sessions matching the desired scopes that this extension has access to. In order to request access, + * use {@link getSession}. To request an additional account, specify {@link AuthenticationGetSessionOptions.clearSessionPreference} + * and {@link AuthenticationGetSessionOptions.createIfNone} together. + * + * Currently, there are only two authentication providers that are contributed from built in extensions + * to the editor that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'. + * + * @param providerId The id of the provider to use + * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider + * @returns A thenable that resolves to a readonly array of authentication sessions. + */ + export function getSessions(providerId: string, scopes: readonly string[]): Thenable; + } +}