diff --git a/client/vscode/BUILD.bazel b/client/vscode/BUILD.bazel index 4d74f7e8321..d73a79918f9 100644 --- a/client/vscode/BUILD.bazel +++ b/client/vscode/BUILD.bazel @@ -142,8 +142,6 @@ ts_project( "src/settings/accessTokenSetting.ts", "src/settings/displayWarnings.ts", "src/settings/endpointSetting.ts", - "src/settings/invalidation.ts", - "src/settings/readConfiguration.ts", "src/settings/recommendations.ts", "src/settings/uninstall.ts", "src/state.ts", diff --git a/client/vscode/CHANGELOG.md b/client/vscode/CHANGELOG.md index 3e623d570e3..5a0cf0faae6 100644 --- a/client/vscode/CHANGELOG.md +++ b/client/vscode/CHANGELOG.md @@ -13,6 +13,7 @@ The Sourcegraph extension uses major.EVEN_NUMBER.patch (eg. 2.0.1) for release v ### Fixes - Various UI fixes for dark and light themes [pull/50598](https://github.com/sourcegraph/sourcegraph/pull/50598) +- Fix authentication so it works through the UI instead of requiring manual modification of `settings.json` [pull/63175](https://github.com/sourcegraph/sourcegraph/pull/63175) ## 2.2.15 diff --git a/client/vscode/src/backend/requestGraphQl.ts b/client/vscode/src/backend/requestGraphQl.ts index b5fb3982206..a44e2a987f1 100644 --- a/client/vscode/src/backend/requestGraphQl.ts +++ b/client/vscode/src/backend/requestGraphQl.ts @@ -8,29 +8,16 @@ import { endpointRequestHeadersSetting, endpointSetting } from '../settings/endp import { fetch, getProxyAgent, Headers, type HeadersInit } from './fetch' -let invalidated = false - -/** - * To be called when Sourcegraph URL changes. - */ -export function invalidateClient(): void { - invalidated = true -} - export const requestGraphQLFromVSCode = async ( request: string, variables: V, overrideAccessToken?: string, overrideSourcegraphURL?: string ): Promise> => { - if (invalidated) { - throw new Error( - 'Sourcegraph GraphQL Client has been invalidated due to instance URL change. Restart VS Code to fix.' - ) - } - const session = await authentication.getSession(endpointSetting(), [], { createIfNone: false }) const sourcegraphURL = overrideSourcegraphURL || endpointSetting() - const accessToken = overrideAccessToken || session?.accessToken + const accessToken = + overrideAccessToken || + (await authentication.getSession(sourcegraphURL, [], { createIfNone: false }))?.accessToken const nameMatch = request.match(/^\s*(?:query|mutation)\s+(\w+)/) const apiURL = `${GRAPHQL_URI}${nameMatch ? '?' + nameMatch[1] : ''}` const customHeaders = endpointRequestHeadersSetting() diff --git a/client/vscode/src/commands/browserActionsNode.ts b/client/vscode/src/commands/browserActionsNode.ts index 2898f1354a7..70d6e796230 100644 --- a/client/vscode/src/commands/browserActionsNode.ts +++ b/client/vscode/src/commands/browserActionsNode.ts @@ -1,5 +1,7 @@ import vscode, { env } from 'vscode' +import { endpointSetting } from '../settings/endpointSetting' + import { getSourcegraphFileUrl, repoInfo } from './git-helpers' import { generateSourcegraphBlobLink } from './initialize' @@ -12,8 +14,7 @@ export async function browserActions(action: string, logRedirectEvent: (uri: str throw new Error('No active editor') } const uri = editor.document.uri - const instanceUrl = - vscode.workspace.getConfiguration('sourcegraph').get('url') || 'https://sourcegraph.com/' + const instanceUrl = endpointSetting() let sourcegraphUrl = '' // check if the current file is a remote file or not if (uri.scheme === 'sourcegraph') { diff --git a/client/vscode/src/commands/browserActionsWeb.ts b/client/vscode/src/commands/browserActionsWeb.ts index 43df9a6e5d8..36af0c16e0d 100644 --- a/client/vscode/src/commands/browserActionsWeb.ts +++ b/client/vscode/src/commands/browserActionsWeb.ts @@ -1,5 +1,7 @@ import vscode, { env } from 'vscode' +import { endpointSetting } from '../settings/endpointSetting' + import { generateSourcegraphBlobLink } from './initialize' /** @@ -12,8 +14,8 @@ export async function browserActions(action: string, logRedirectEvent: (uri: str throw new Error('No active editor') } const uri = editor.document.uri - const instanceUrl = vscode.workspace.getConfiguration('sourcegraph').get('url') - let sourcegraphUrl = String() + const instanceUrl = endpointSetting() + let sourcegraphUrl = '' // check if the current file is a remote file or not if (uri.scheme === 'sourcegraph') { sourcegraphUrl = generateSourcegraphBlobLink( diff --git a/client/vscode/src/commands/initialize.ts b/client/vscode/src/commands/initialize.ts index 59c0d36c6d3..7549c4610fd 100644 --- a/client/vscode/src/commands/initialize.ts +++ b/client/vscode/src/commands/initialize.ts @@ -5,6 +5,7 @@ import type { EventSource } from '@sourcegraph/shared/src/graphql-operations' import { version } from '../../package.json' import { logEvent } from '../backend/eventLogger' import { SourcegraphUri } from '../file-system/SourcegraphUri' +import { endpointSetting } from '../settings/endpointSetting' import { type LocalStorageService, ANONYMOUS_USER_ID_KEY } from '../settings/LocalStorageService' import { browserActions } from './browserActionsNode' @@ -29,8 +30,7 @@ export function initializeCodeSharingCommands( // Search Selected Text in Sourcegraph Search Tab context.subscriptions.push( vscode.commands.registerCommand('sourcegraph.selectionSearchWeb', async () => { - const instanceUrl = - vscode.workspace.getConfiguration('sourcegraph').get('url') || 'https://sourcegraph.com' + const instanceUrl = endpointSetting() const editor = vscode.window.activeTextEditor const selectedQuery = editor?.document.getText(editor.selection) if (!editor || !selectedQuery) { @@ -73,9 +73,7 @@ export function generateSourcegraphBlobLink( endLine: number, endChar: number ): string { - const instanceUrl = new URL( - vscode.workspace.getConfiguration('sourcegraph').get('url') || 'https://sourcegraph.com' - ) + const instanceUrl = new URL(endpointSetting()) // Using SourcegraphUri.parse to properly decode repo revision const decodedUri = SourcegraphUri.parse(uri.toString()) const finalUri = new URL(decodedUri.uri) diff --git a/client/vscode/src/extension.ts b/client/vscode/src/extension.ts index dd3a1c25829..05dd9e9d26e 100644 --- a/client/vscode/src/extension.ts +++ b/client/vscode/src/extension.ts @@ -21,35 +21,35 @@ import { SourcegraphUri } from './file-system/SourcegraphUri' import type { Event } from './graphql-operations' import { accessTokenSetting, processOldToken } from './settings/accessTokenSetting' import { endpointRequestHeadersSetting, endpointSetting } from './settings/endpointSetting' -import { invalidateContextOnSettingsChange } from './settings/invalidation' import { LocalStorageService, SELECTED_SEARCH_CONTEXT_SPEC_KEY } from './settings/LocalStorageService' import { watchUninstall } from './settings/uninstall' import { createVSCEStateMachine, type VSCEQueryState } from './state' import { copySourcegraphLinks, focusSearchPanel, openSourcegraphLinks, registerWebviews } from './webview/commands' import { secretTokenKey, SourcegraphAuthActions, SourcegraphAuthProvider } from './webview/platform/AuthProvider' +export let extensionContext: vscode.ExtensionContext /** * See CONTRIBUTING docs for the Architecture Diagram */ export async function activate(context: vscode.ExtensionContext): Promise { + extensionContext = context + const initialInstanceURL = endpointSetting() const secretStorage = context.secrets // Register SourcegraphAuthProvider context.subscriptions.push( vscode.authentication.registerAuthenticationProvider( - endpointSetting(), + initialInstanceURL, secretTokenKey, new SourcegraphAuthProvider(secretStorage) ) ) await processOldToken(secretStorage) - const initialInstanceURL = endpointSetting() const initialAccessToken = await secretStorage.get(secretTokenKey) const createIfNone = initialAccessToken ? { createIfNone: true } : { createIfNone: false } - const session = await vscode.authentication.getSession(endpointSetting(), [], createIfNone) + const session = await vscode.authentication.getSession(initialInstanceURL, [], createIfNone) const authenticatedUser = observeAuthenticatedUser(secretStorage) const localStorageService = new LocalStorageService(context.globalState) const stateMachine = createVSCEStateMachine({ localStorageService }) - invalidateContextOnSettingsChange({ context, stateMachine }) initializeSearchContexts({ localStorageService, stateMachine, context }) const sourcegraphSettings = initializeSourcegraphSettings({ context }) const editorTheme = vscode.ColorThemeKind[vscode.window.activeColorTheme.kind] diff --git a/client/vscode/src/settings/accessTokenSetting.ts b/client/vscode/src/settings/accessTokenSetting.ts index acadd4801be..2be4876c32e 100644 --- a/client/vscode/src/settings/accessTokenSetting.ts +++ b/client/vscode/src/settings/accessTokenSetting.ts @@ -4,7 +4,6 @@ import { isOlderThan, observeInstanceVersionNumber } from '../backend/instanceVe import { secretTokenKey } from '../webview/platform/AuthProvider' import { endpointHostnameSetting, endpointProtocolSetting } from './endpointSetting' -import { readConfiguration } from './readConfiguration' // IMPORTANT: Call this function only once when extention is first activated export async function processOldToken(secretStorage: vscode.SecretStorage): Promise { @@ -25,8 +24,12 @@ export async function accessTokenSetting(secretStorage: vscode.SecretStorage): P } export async function removeOldAccessTokenSetting(): Promise { - await readConfiguration().update('accessToken', undefined, vscode.ConfigurationTarget.Global) - await readConfiguration().update('accessToken', undefined, vscode.ConfigurationTarget.Workspace) + await vscode.workspace + .getConfiguration() + .update('sourcegraph.accessToken', undefined, vscode.ConfigurationTarget.Global) + await vscode.workspace + .getConfiguration() + .update('sourcegraph.accessToken', undefined, vscode.ConfigurationTarget.Workspace) return } diff --git a/client/vscode/src/settings/endpointSetting.ts b/client/vscode/src/settings/endpointSetting.ts index f845454664f..f614fcb72c1 100644 --- a/client/vscode/src/settings/endpointSetting.ts +++ b/client/vscode/src/settings/endpointSetting.ts @@ -1,20 +1,75 @@ import * as vscode from 'vscode' -import { readConfiguration } from './readConfiguration' +import { extensionContext } from '../extension' +import { secretTokenKey, SourcegraphAuthProvider } from '../webview/platform/AuthProvider' + +const defaultEndpointURL = 'https://sourcegraph.com' + +const endpointKey = 'sourcegraph.url' + +async function removeOldEndpointURLSetting(): Promise { + await vscode.workspace.getConfiguration().update(endpointKey, undefined, vscode.ConfigurationTarget.Global) + await vscode.workspace.getConfiguration().update(endpointKey, undefined, vscode.ConfigurationTarget.Workspace) + return +} export function endpointSetting(): string { - const url = vscode.workspace.getConfiguration().get('sourcegraph.url') || 'https://sourcegraph.com' + // get the URl from either, 1. extension local storage (new) + let url = extensionContext?.globalState.get(endpointKey) + if (!url) { + // 2. settings.json (old) + url = vscode.workspace.getConfiguration().get(endpointKey) + if (url) { + // if settings.json, migrate to extension local storage + extensionContext?.globalState.update(endpointKey, url).then( + () => { + void removeOldEndpointURLSetting() + }, + error => { + console.error(error) + } + ) + } else { + // or, 3. default value + url = defaultEndpointURL + } + } return removeEndingSlash(url) } -export async function setEndpoint(newEndpoint: string): Promise { - const newEndpointURL = newEndpoint ? removeEndingSlash(newEndpoint) : 'https://sourcegraph.com' +export function setEndpoint(newEndpoint: string | undefined): void { + const newEndpointURL = newEndpoint ? removeEndingSlash(newEndpoint) : defaultEndpointURL const currentEndpointHostname = new URL(endpointSetting()).hostname const newEndpointHostname = new URL(newEndpointURL).hostname if (currentEndpointHostname !== newEndpointHostname) { - await readConfiguration().update('url', newEndpointURL) + extensionContext?.globalState.update(endpointKey, newEndpointURL).then( + () => { + // after changing the endpoint URL, register an authentication provder for it. + // trying and erroring (because one already exists) is probably just as cheap/expensive + // as trying `vscode.authentication.getSession(newEndpointURL, [], { createIfNone: false })`, + // catching an error and registering an auth provider. + try { + const provider = vscode.authentication.registerAuthenticationProvider( + newEndpointURL, + secretTokenKey, + new SourcegraphAuthProvider(extensionContext?.secrets) + ) + extensionContext?.subscriptions.push(provider) + } catch (error) { + // unsetting the endpoint reverts to the default, + // which probably already has an auth provider, + // which would cause an error, + // so ignore it. + if (!(error as Error).message.includes('is already registered')) { + console.error(error) + } + } + }, + error => { + console.error(error) + } + ) } - return } export function endpointHostnameSetting(): string { diff --git a/client/vscode/src/settings/invalidation.ts b/client/vscode/src/settings/invalidation.ts deleted file mode 100644 index 768078746e4..00000000000 --- a/client/vscode/src/settings/invalidation.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as vscode from 'vscode' - -import { invalidateClient } from '../backend/requestGraphQl' -import type { VSCEStateMachine } from '../state' - -/** - * Listens for Sourcegraph URL and invalidates the GraphQL client - * to prevent data "contamination" (e.g. sending private repo names to Cloud instance). - */ -export function invalidateContextOnSettingsChange({ - context, - stateMachine, -}: { - context: vscode.ExtensionContext - stateMachine: VSCEStateMachine -}): void { - function disposeAllResources(): void { - for (const subscription of context.subscriptions) { - subscription.dispose() - } - } - - context.subscriptions.push( - vscode.workspace.onDidChangeConfiguration(async config => { - if (config.affectsConfiguration('sourcegraph.url')) { - invalidateClient() - disposeAllResources() - stateMachine.emit({ type: 'sourcegraph_url_change' }) - // Swallow errors since if `showInformationMessage` fails, we assume that something is wrong - // with the VS Code extension host and don't retry. - await vscode.window.showInformationMessage( - 'Restart VS Code to use the Sourcegraph extension after URL change.' - ) - } - }) - ) -} diff --git a/client/vscode/src/settings/readConfiguration.ts b/client/vscode/src/settings/readConfiguration.ts deleted file mode 100644 index e49ee368021..00000000000 --- a/client/vscode/src/settings/readConfiguration.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as vscode from 'vscode' - -export function readConfiguration(): vscode.WorkspaceConfiguration { - return vscode.workspace.getConfiguration('sourcegraph') -} diff --git a/client/vscode/src/state.ts b/client/vscode/src/state.ts index fab2389945a..363b61e4129 100644 --- a/client/vscode/src/state.ts +++ b/client/vscode/src/state.ts @@ -99,7 +99,7 @@ function createInitialState({ localStorageService }: { localStorageService: Loca // Temporary placeholder events. We will replace these with the actual events as we implement the webviews. -export type VSCEEvent = SearchEvent | TabsEvent | SettingsEvent +export type VSCEEvent = SearchEvent | TabsEvent type SearchEvent = | { type: 'set_query_state' } @@ -118,10 +118,6 @@ type TabsEvent = | { type: 'remote_file_focused' } | { type: 'remote_file_unfocused' } -interface SettingsEvent { - type: 'sourcegraph_url_change' -} - export function createVSCEStateMachine({ localStorageService, }: { @@ -135,15 +131,6 @@ export function createVSCEStateMachine({ return state } - // Events with the same behavior regardless of current state - if (event.type === 'sourcegraph_url_change') { - return { - status: 'context-invalidated', - context: { - ...createInitialState({ localStorageService }).context, - }, - } - } if (event.type === 'set_selected_search_context_spec') { return { ...state, diff --git a/client/vscode/src/webview/platform/AuthProvider.ts b/client/vscode/src/webview/platform/AuthProvider.ts index d2554bbe5fe..04988537524 100644 --- a/client/vscode/src/webview/platform/AuthProvider.ts +++ b/client/vscode/src/webview/platform/AuthProvider.ts @@ -138,7 +138,7 @@ export class SourcegraphAuthActions { try { await this.secretStorage.store(secretTokenKey, newtoken) if (this.currentEndpoint !== newuri) { - await setEndpoint(newuri) + setEndpoint(newuri) } return } catch (error) { @@ -147,6 +147,7 @@ export class SourcegraphAuthActions { } public async logout(): Promise { + setEndpoint(undefined) await this.secretStorage.delete(secretTokenKey) await commands.executeCommand('workbench.action.reloadWindow') return diff --git a/client/vscode/src/webview/sidebars/auth/AuthSidebarView.tsx b/client/vscode/src/webview/sidebars/auth/AuthSidebarView.tsx index df8175a5cd5..603d96cf0c3 100644 --- a/client/vscode/src/webview/sidebars/auth/AuthSidebarView.tsx +++ b/client/vscode/src/webview/sidebars/auth/AuthSidebarView.tsx @@ -280,8 +280,7 @@ export const AuthSidebarView: React.FunctionComponent {state === 'failure' && ( - Unable to verify your access token for {hostname}. Please try again with a new access token or - restart VS Code if the instance URL has been updated. + Unable to verify your access token for {hostname}. Please try again with a new access token. )}