Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial getSessions API #177036

Merged
merged 2 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions src/vs/workbench/api/browser/mainThreadAuthentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AuthenticationSession[]> {
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 });
}
}
4 changes: 4 additions & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export interface MainThreadAuthenticationShape extends IDisposable {
$ensureProvider(id: string): Promise<void>;
$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<AuthenticationSession | undefined>;
$getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise<AuthenticationSession[]>;
$removeSession(providerId: string, sessionId: string): Promise<void>;
}

Expand Down
69 changes: 30 additions & 39 deletions src/vs/workbench/api/common/extHostAuthentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<vscode.AuthenticationSession | undefined>;
}

interface ProviderWithMetadata {
label: string;
provider: vscode.AuthenticationProvider;
Expand All @@ -30,7 +24,8 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
private _onDidChangeSessions = new Emitter<vscode.AuthenticationSessionsChangeEvent>();
readonly onDidChangeSessions: Event<vscode.AuthenticationSessionsChangeEvent> = this._onDidChangeSessions.event;

private _inFlightRequests = new Map<string, GetSessionsRequest[]>();
private _getSessionTaskSingler = new TaskSingler<vscode.AuthenticationSession | undefined>();
private _getSessionsTaskSingler = new TaskSingler<ReadonlyArray<vscode.AuthenticationSession>>();

constructor(mainContext: IMainContext) {
this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication);
Expand All @@ -47,41 +42,22 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: readonly string[], options: vscode.AuthenticationGetSessionOptions): Promise<vscode.AuthenticationSession | undefined>;
async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: readonly string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise<vscode.AuthenticationSession | undefined> {
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} ${providerId} ${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<vscode.AuthenticationSession | undefined> {
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<ReadonlyArray<vscode.AuthenticationSession>> {
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<void> {
Expand Down Expand Up @@ -162,3 +138,18 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
return Promise.resolve();
}
}

class TaskSingler<T> {
private _inFlightPromises = new Map<string, Promise<T>>();
getOrCreate(key: string, promiseFactory: () => Promise<T>) {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
25 changes: 25 additions & 0 deletions src/vscode-dts/vscode.proposed.getSessions.d.ts
Original file line number Diff line number Diff line change
@@ -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<readonly AuthenticationSession[]>;
}
}