diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index ff7bebe43b1..e20ff091776 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -108,6 +108,40 @@ declare module 'vscode' { */ export const providerIds: string[]; + /** + * Returns whether a provider has any sessions matching the requested scopes. This request + * is transparent to the user, not UI is shown. Rejects if a provider with providerId is not + * registered. + * @param providerId The id of the provider + * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication + * provider + */ + export function hasSessions(providerId: string, scopes: string[]): Thenable; + + export interface GetSessionOptions { + /** + * Whether login should be performed if there is no matching session. Defaults to false. + */ + createIfNone?: boolean; + + /** + * Whether the existing user session preference should be cleared. Set to allow the user to switch accounts. + * Defaults to false. + */ + clearSessionPreference?: boolean; + } + + /** + * Get an authentication session matching the desired scopes. Rejects if a provider with providerId is not + * registered, or if the user does not consent to sharing authentication information with + * the extension. If there are multiple sessions with the same scopes, the user will be shown a + * quickpick to select which account they would like to use. + * @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 + * @param options The [getSessionOptions](#GetSessionOptions) to use + */ + export function getSession(providerId: string, scopes: string[], options: GetSessionOptions): Thenable; + /** * Get existing authentication sessions. Rejects if a provider with providerId is not * registered, or if the user does not consent to sharing authentication information with diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index ccabaa3af36..1ae18d4aee4 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -294,7 +294,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, @IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, - @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, + @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication); @@ -314,6 +315,72 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu this.authenticationService.sessionsUpdate(id, event); } + async $getSession(providerId: string, providerName: string, extensionId: string, extensionName: string, potentialSessions: modes.AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise { + if (!potentialSessions.length) { + throw new Error('No potential sessions found'); + } + + if (clearSessionPreference) { + this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.GLOBAL); + } else { + const existingSessionPreference = this.storageService.get(`${extensionName}-${providerId}`, StorageScope.GLOBAL); + if (existingSessionPreference) { + const matchingSession = potentialSessions.find(session => session.id === existingSessionPreference); + if (matchingSession) { + const allowed = await this.$getSessionsPrompt(providerId, matchingSession.account.displayName, providerName, extensionId, extensionName); + if (allowed) { + return matchingSession; + } + } + } + } + + return new Promise((resolve, reject) => { + const quickPick = this.quickInputService.createQuickPick<{ label: string, session?: modes.AuthenticationSession }>(); + quickPick.ignoreFocusOut = true; + const items: { label: string, session?: modes.AuthenticationSession }[] = potentialSessions.map(session => { + return { + label: session.account.displayName, + session + }; + }); + + items.push({ + label: nls.localize('useOtherAccount', "Sign in to another account") + }); + + quickPick.items = items; + quickPick.title = nls.localize('selectAccount', "The extension '{0}' wants to access a {1} account", extensionName, providerName); + quickPick.placeholder = nls.localize('getSessionPlateholder', "Select an account for '{0}' to use or Esc to cancel", extensionName); + + quickPick.onDidAccept(async _ => { + const selected = quickPick.selectedItems[0]; + + const session = selected.session ?? await this.authenticationService.login(providerId, scopes); + + const accountName = session.account.displayName; + const allowList = readAllowedExtensions(this.storageService, providerId, accountName); + allowList.push({ id: extensionId, name: extensionName }); + this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL); + + this.storageService.store(`${extensionName}-${providerId}`, session.id, StorageScope.GLOBAL); + + quickPick.dispose(); + resolve(session); + }); + + quickPick.onDidHide(_ => { + if (!quickPick.selectedItems[0]) { + reject('User did not consent to account access'); + } + + quickPick.dispose(); + }); + + quickPick.show(); + }); + } + async $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise { addAccountUsage(providerId, accountName, extensionName); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index eed6c1f9d12..57de023e859 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -196,6 +196,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get providerIds(): string[] { return extHostAuthentication.providerIds; }, + hasSessions(providerId: string, scopes: string[]): Thenable { + return extHostAuthentication.hasSessions(providerId, scopes); + }, + getSession(providerId: string, scopes: string[], options: vscode.authentication.GetSessionOptions): Thenable { + return extHostAuthentication.getSession(extension, providerId, scopes, options); + }, getSessions(providerId: string, scopes: string[]): Thenable { return extHostAuthentication.getSessions(extension, providerId, scopes); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 9340648be72..316f06d40d8 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -160,6 +160,7 @@ export interface MainThreadAuthenticationShape extends IDisposable { $registerAuthenticationProvider(id: string, displayName: string): void; $unregisterAuthenticationProvider(id: string): void; $onDidChangeSessions(providerId: string, event: modes.AuthenticationSessionsChangeEvent): void; + $getSession(providerId: string, providerName: string, extensionId: string, extensionName: string, potentialSessions: modes.AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise; $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise; $loginPrompt(providerName: string, extensionName: string): Promise; $setTrustedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise; diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 888aefea967..df7f296eceb 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -33,6 +33,46 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { return ids; } + async hasSessions(providerId: string, scopes: string[]): Promise { + const provider = this._authenticationProviders.get(providerId); + if (!provider) { + throw new Error(`No authentication provider with id '${providerId}' is currently registered.`); + } + + const orderedScopes = scopes.sort().join(' '); + return !!(await provider.getSessions()).filter(session => session.scopes.sort().join(' ') === orderedScopes).length; + } + + async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.authentication.GetSessionOptions): Promise { + const provider = this._authenticationProviders.get(providerId); + if (!provider) { + throw new Error(`No authentication provider with id '${providerId}' is currently registered.`); + } + + const orderedScopes = scopes.sort().join(' '); + const sessions = (await provider.getSessions()).filter(session => session.scopes.sort().join(' ') === orderedScopes); + if (sessions.length) { + // On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid + const extensionName = requestingExtension.displayName || requestingExtension.name; + const selected = await this._proxy.$getSession(provider.id, provider.displayName, ExtensionIdentifier.toKey(requestingExtension.identifier), extensionName, sessions, scopes, !!options.clearSessionPreference); + return sessions.find(session => session.id === selected.id); + } else { + if (options.createIfNone) { + const extensionName = requestingExtension.displayName || requestingExtension.name; + const isAllowed = await this._proxy.$loginPrompt(provider.displayName, extensionName); + if (!isAllowed) { + throw new Error('User did not consent to login.'); + } + + const session = await provider.login(scopes); + await this._proxy.$setTrustedExtension(provider.id, session.account.displayName, ExtensionIdentifier.toKey(requestingExtension.identifier), extensionName); + return session; + } else { + return undefined; + } + } + } + async getSessions(requestingExtension: IExtensionDescription, providerId: string, scopes: string[]): Promise { const provider = this._authenticationProviders.get(providerId); if (!provider) {