Merge branch 'main' into dmitrysonder/issue236939

pull/236941/head
Dmitry Sonder 2024-12-30 09:34:57 +04:00 committed by GitHub
commit 5592fb560a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 187 additions and 89 deletions

View File

@ -3,18 +3,17 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AccountInfo, AuthenticationResult, ClientAuthError, ClientAuthErrorCodes, ServerError } from '@azure/msal-node';
import { AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationProviderSessionOptions, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, env, EventEmitter, ExtensionContext, l10n, LogOutputChannel, Uri, window } from 'vscode';
import { AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationProviderSessionOptions, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, EventEmitter, ExtensionContext, ExtensionKind, l10n, LogOutputChannel, window } from 'vscode';
import { Environment } from '@azure/ms-rest-azure-env';
import { CachedPublicClientApplicationManager } from './publicClientCache';
import { UriHandlerLoopbackClient } from '../common/loopbackClientAndOpener';
import { UriEventHandler } from '../UriEventHandler';
import { ICachedPublicClientApplication } from '../common/publicClientCache';
import { MicrosoftAccountType, MicrosoftAuthenticationTelemetryReporter } from '../common/telemetryReporter';
import { loopbackTemplate } from './loopbackTemplate';
import { ScopeData } from '../common/scopeData';
import { EventBufferer } from '../common/event';
import { BetterTokenStorage } from '../betterSecretStorage';
import { IStoredSession } from '../AADHelper';
import { ExtensionHost, getMsalFlows } from './flows';
const redirectUri = 'https://vscode.dev/redirect';
const MSA_TID = '9188040d-6c67-4c5b-b112-36a304b66dad';
@ -187,86 +186,70 @@ export class MsalAuthProvider implements AuthenticationProvider {
this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'starting');
const cachedPca = await this.getOrCreatePublicClientApplication(scopeData.clientId, scopeData.tenant);
let result: AuthenticationResult | undefined;
try {
const windowHandle = window.nativeHandle ? Buffer.from(window.nativeHandle) : undefined;
result = await cachedPca.acquireTokenInteractive({
openBrowser: async (url: string) => { await env.openExternal(Uri.parse(url)); },
scopes: scopeData.scopesToSend,
// The logic for rendering one or the other of these templates is in the
// template itself, so we pass the same one for both.
successTemplate: loopbackTemplate,
errorTemplate: loopbackTemplate,
// Pass the label of the account to the login hint so that we prefer signing in to that account
loginHint: options.account?.label,
// If we aren't logging in to a specific account, then we can use the prompt to make sure they get
// the option to choose a different account.
prompt: options.account?.label ? undefined : 'select_account',
windowHandle
});
} catch (e) {
if (e instanceof CancellationError) {
const yes = l10n.t('Yes');
const result = await window.showErrorMessage(
l10n.t('Having trouble logging in?'),
{
modal: true,
detail: l10n.t('Would you like to try a different way to sign in to your Microsoft account? ({0})', 'protocol handler')
},
yes
);
if (!result) {
// Used for showing a friendlier message to the user when the explicitly cancel a flow.
let userCancelled: boolean | undefined;
const yes = l10n.t('Yes');
const no = l10n.t('No');
const promptToContinue = async (mode: string) => {
if (userCancelled === undefined) {
// We haven't had a failure yet so wait to prompt
return;
}
const message = userCancelled
? l10n.t('Having trouble logging in? Would you like to try a different way? ({0})', mode)
: l10n.t('You have not yet finished authorizing this extension to use your Microsoft Account. Would you like to try a different way? ({0})', mode);
const result = await window.showWarningMessage(message, yes, no);
if (result !== yes) {
throw new CancellationError();
}
};
const flows = getMsalFlows({
extensionHost: typeof navigator === 'undefined'
? this._context.extension.extensionKind === ExtensionKind.UI ? ExtensionHost.Local : ExtensionHost.Remote
: ExtensionHost.WebWorker,
});
let lastError: Error | undefined;
for (const flow of flows) {
if (flow !== flows[0]) {
try {
await promptToContinue(flow.label);
} finally {
this._telemetryReporter.sendLoginFailedEvent();
}
}
try {
const result = await flow.trigger({
cachedPca,
scopes: scopeData.scopesToSend,
loginHint: options.account?.label,
windowHandle: window.nativeHandle ? Buffer.from(window.nativeHandle) : undefined,
logger: this._logger,
uriHandler: this._uriHandler
});
const session = this.sessionFromAuthenticationResult(result, scopeData.originalScopes);
this._telemetryReporter.sendLoginEvent(session.scopes);
this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'returned session');
this._onDidChangeSessionsEmitter.fire({ added: [session], changed: [], removed: [] });
return session;
} catch (e) {
lastError = e;
if (e instanceof ServerError || (e as ClientAuthError)?.errorCode === ClientAuthErrorCodes.userCanceled) {
this._telemetryReporter.sendLoginFailedEvent();
throw e;
}
}
// This error comes from the backend and is likely not due to the user's machine
// failing to open a port or something local that would require us to try the
// URL handler loopback client.
if (e instanceof ServerError) {
this._telemetryReporter.sendLoginFailedEvent();
throw e;
}
// The user closed the modal window
if ((e as ClientAuthError).errorCode === ClientAuthErrorCodes.userCanceled) {
this._telemetryReporter.sendLoginFailedEvent();
throw e;
}
// The user wants to try the loopback client or we got an error likely due to spinning up the server
const loopbackClient = new UriHandlerLoopbackClient(this._uriHandler, redirectUri, this._logger);
try {
const windowHandle = window.nativeHandle ? Buffer.from(window.nativeHandle) : undefined;
result = await cachedPca.acquireTokenInteractive({
openBrowser: (url: string) => loopbackClient.openBrowser(url),
scopes: scopeData.scopesToSend,
loopbackClient,
loginHint: options.account?.label,
prompt: options.account?.label ? undefined : 'select_account',
windowHandle
});
} catch (e) {
this._telemetryReporter.sendLoginFailedEvent();
throw e;
// Continue to next flow
if (e instanceof CancellationError) {
userCancelled = true;
}
}
}
if (!result) {
this._telemetryReporter.sendLoginFailedEvent();
throw new Error('No result returned from MSAL');
}
const session = this.sessionFromAuthenticationResult(result, scopeData.originalScopes);
this._telemetryReporter.sendLoginEvent(session.scopes);
this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'returned session');
// This is the only scenario in which we need to fire the _onDidChangeSessionsEmitter out of band...
// the badge flow (when the client passes no options in to getSession) will only remove a badge if a session
// was created that _matches the scopes_ that that badge requests. See `onDidChangeSessions` for more info.
// TODO: This should really be fixed in Core.
this._onDidChangeSessionsEmitter.fire({ added: [session], changed: [], removed: [] });
return session;
this._telemetryReporter.sendLoginFailedEvent();
throw lastError ?? new Error('No auth flow succeeded');
}
async removeSession(sessionId: string): Promise<void> {

View File

@ -133,11 +133,11 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica
cancellable: true,
title: l10n.t('Signing in to Microsoft...')
},
(_process, token) => raceCancellationAndTimeoutError(
this._sequencer.queue(() => this._pca.acquireTokenInteractive(request)),
(_process, token) => this._sequencer.queue(() => raceCancellationAndTimeoutError(
this._pca.acquireTokenInteractive(request),
token,
1000 * 60 * 5
)
))
);
// this._setupRefresh(result);
if (this._isBrokerAvailable) {

View File

@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AuthenticationResult } from '@azure/msal-node';
import { Uri, LogOutputChannel, env } from 'vscode';
import { ICachedPublicClientApplication } from '../common/publicClientCache';
import { UriHandlerLoopbackClient } from '../common/loopbackClientAndOpener';
import { UriEventHandler } from '../UriEventHandler';
import { loopbackTemplate } from './loopbackTemplate';
const redirectUri = 'https://vscode.dev/redirect';
export const enum ExtensionHost {
WebWorker,
Remote,
Local
}
interface IMsalFlowOptions {
supportsRemoteExtensionHost: boolean;
supportsWebWorkerExtensionHost: boolean;
}
interface IMsalFlowTriggerOptions {
cachedPca: ICachedPublicClientApplication;
scopes: string[];
loginHint?: string;
windowHandle?: Buffer;
logger: LogOutputChannel;
uriHandler: UriEventHandler;
}
interface IMsalFlow {
readonly label: string;
readonly options: IMsalFlowOptions;
trigger(options: IMsalFlowTriggerOptions): Promise<AuthenticationResult>;
}
class DefaultLoopbackFlow implements IMsalFlow {
label = 'default';
options: IMsalFlowOptions = {
supportsRemoteExtensionHost: true,
supportsWebWorkerExtensionHost: true
};
async trigger({ cachedPca, scopes, loginHint, windowHandle, logger }: IMsalFlowTriggerOptions): Promise<AuthenticationResult> {
logger.info('Trying default msal flow...');
return await cachedPca.acquireTokenInteractive({
openBrowser: async (url: string) => { await env.openExternal(Uri.parse(url)); },
scopes,
successTemplate: loopbackTemplate,
errorTemplate: loopbackTemplate,
loginHint,
prompt: loginHint ? undefined : 'select_account',
windowHandle
});
}
}
class UrlHandlerFlow implements IMsalFlow {
label = 'protocol handler';
options: IMsalFlowOptions = {
supportsRemoteExtensionHost: false,
supportsWebWorkerExtensionHost: false
};
async trigger({ cachedPca, scopes, loginHint, windowHandle, logger, uriHandler }: IMsalFlowTriggerOptions): Promise<AuthenticationResult> {
logger.info('Trying protocol handler flow...');
const loopbackClient = new UriHandlerLoopbackClient(uriHandler, redirectUri, logger);
return await cachedPca.acquireTokenInteractive({
openBrowser: (url: string) => loopbackClient.openBrowser(url),
scopes,
loopbackClient,
loginHint,
prompt: loginHint ? undefined : 'select_account',
windowHandle
});
}
}
const allFlows: IMsalFlow[] = [
new DefaultLoopbackFlow(),
new UrlHandlerFlow()
];
export interface IMsalFlowQuery {
extensionHost: ExtensionHost;
}
export function getMsalFlows(query: IMsalFlowQuery): IMsalFlow[] {
return allFlows.filter(flow => {
let useFlow: boolean = true;
switch (query.extensionHost) {
case ExtensionHost.Remote:
useFlow &&= flow.options.supportsRemoteExtensionHost;
break;
case ExtensionHost.WebWorker:
useFlow &&= flow.options.supportsWebWorkerExtensionHost;
break;
}
return useFlow;
});
}

View File

@ -947,7 +947,7 @@ interface IAbstractFindControllerOptions extends IFindWidgetOptions {
showNotFoundMessage?: boolean;
}
interface IFindControllerOptions extends IAbstractFindControllerOptions {
export interface IFindControllerOptions extends IAbstractFindControllerOptions {
defaultFindMode?: TreeFindMode;
defaultFindMatchType?: TreeFindMatchType;
}

View File

@ -7,7 +7,7 @@ import { IDragAndDropData } from '../../dnd.js';
import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, IListVirtualDelegate } from '../list/list.js';
import { ElementsDragAndDropData, ListViewTargetSector } from '../list/listView.js';
import { IListStyles } from '../list/listWidget.js';
import { ComposedTreeDelegate, TreeFindMode as TreeFindMode, IAbstractTreeOptions, IAbstractTreeOptionsUpdate, TreeFindMatchType, AbstractTreePart, LabelFuzzyScore, FindFilter, FindController, ITreeFindToggleChangeEvent } from './abstractTree.js';
import { ComposedTreeDelegate, TreeFindMode as TreeFindMode, IAbstractTreeOptions, IAbstractTreeOptionsUpdate, TreeFindMatchType, AbstractTreePart, LabelFuzzyScore, FindFilter, FindController, ITreeFindToggleChangeEvent, IFindControllerOptions } from './abstractTree.js';
import { ICompressedTreeElement, ICompressedTreeNode } from './compressedObjectTreeModel.js';
import { getVisibleState, isFilterResult } from './indexTreeModel.js';
import { CompressibleObjectTree, ICompressibleKeyboardNavigationLabelProvider, ICompressibleObjectTreeOptions, ICompressibleTreeRenderer, IObjectTreeOptions, IObjectTreeSetChildrenOptions, ObjectTree } from './objectTree.js';
@ -629,7 +629,12 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
this.tree.onDidChangeCollapseState(this._onDidChangeCollapseState, this, this.disposables);
if (asyncFindEnabled) {
const findOptions = { styles: options.findWidgetStyles, showNotFoundMessage: options.showNotFoundMessage };
const findOptions: IFindControllerOptions = {
styles: options.findWidgetStyles,
showNotFoundMessage: options.showNotFoundMessage,
defaultFindMatchType: options.defaultFindMatchType,
defaultFindMode: options.defaultFindMode,
};
this.findController = this.disposables.add(new AsyncFindController(this.tree, options.findProvider!, findFilter!, this.tree.options.contextViewProvider!, findOptions));
this.focusNavigationFilter = node => this.findController!.shouldFocusWhenNavigating(node);
@ -657,8 +662,18 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
return new ObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions);
}
updateOptions(options: IAsyncDataTreeOptionsUpdate = {}): void {
this.tree.updateOptions(options);
updateOptions(optionsUpdate: IAsyncDataTreeOptionsUpdate = {}): void {
if (this.findController) {
if (optionsUpdate.defaultFindMode !== undefined) {
this.findController.mode = optionsUpdate.defaultFindMode;
}
if (optionsUpdate.defaultFindMatchType !== undefined) {
this.findController.matchType = optionsUpdate.defaultFindMatchType;
}
}
this.tree.updateOptions(optionsUpdate);
}
get options(): IAsyncDataTreeOptions<T, TFilterData> {
@ -1513,10 +1528,6 @@ export class CompressibleAsyncDataTree<TInput, T, TFilterData = void> extends As
};
}
override updateOptions(options: ICompressibleAsyncDataTreeOptionsUpdate = {}): void {
this.tree.updateOptions(options);
}
override getViewState(): IAsyncDataTreeViewState {
if (!this.identityProvider) {
throw new TreeError(this.user, 'Can\'t get tree view state without an identity provider');

View File

@ -28,7 +28,6 @@ class NotebookSynchronizerSaveParticipant extends NotebookSaveParticipant {
}
override async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
console.log('notebook synchronizer participate');
const session = this._chatEditingService.currentEditingSessionObs.get();
if (!session) {