Fix(search): auth issues with Sourcegraph VSCode extension (#63175)

Fixes the issues requiring the workaround described in [this
video](https://www.loom.com/share/10a4a66a19b548c7b0866fe2cc358daa).

Closes #60710 

No more manual editing of `settings.json`.

The endpoint URL and access code can now all be managed from the UI

<!-- 💡 To write a useful PR description, make sure that your description
covers:
- WHAT this PR is changing:
    - How was it PREVIOUSLY.
    - How it will be from NOW on.
- WHY this PR is needed.
- CONTEXT, i.e. to which initiative, project or RFC it belongs.

The structure of the description doesn't matter as much as covering
these points, so use
your best judgement based on your context.
Learn how to write good pull request description:
https://www.notion.so/sourcegraph/Write-a-good-pull-request-description-610a7fd3e613496eb76f450db5a49b6e?pvs=4
-->


## Test plan

### First
Build and run locally.
```
git switch peterguy/vscode-sourcegraph-extension-fix-auth
cd client/vscode
pnpm run build
```
### Then
Launch extension in VSCode: open the `Run and Debug` sidebar view in VS
Code, then select `Launch VS Code Extension` from the dropdown menu.
Click on `Have an account?` to open the login dialog.
Enter an access token and the URL of the Sourcegraph instance to which
you would like to connect.
Click `Authenticate account`.
In the Help and Feedback section, click your username to open the logout
panel, then log out.
Repeat the login process.
You can check `settings.json` if you'd like to confirm that it's no
longer being used.
If you're logging in to dotcom, you'll probably se a SQL error. The
login process still works; the SQL error does not have long to live.

## Changelog

- Entering the URL and access token in the UI now works - no more manual
editing of `settings.json`
This commit is contained in:
Peter Guy 2024-06-12 07:07:10 -07:00 committed by GitHub
parent e0e234c509
commit 9d82cd17eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 90 additions and 100 deletions

View File

@ -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",

View File

@ -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

View File

@ -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 <R, V = object>(
request: string,
variables: V,
overrideAccessToken?: string,
overrideSourcegraphURL?: string
): Promise<GraphQLResult<R>> => {
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()

View File

@ -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<string>('url') || 'https://sourcegraph.com/'
const instanceUrl = endpointSetting()
let sourcegraphUrl = ''
// check if the current file is a remote file or not
if (uri.scheme === 'sourcegraph') {

View File

@ -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(

View File

@ -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<string>('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<string>('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)

View File

@ -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<void> {
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]

View File

@ -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<void> {
@ -25,8 +24,12 @@ export async function accessTokenSetting(secretStorage: vscode.SecretStorage): P
}
export async function removeOldAccessTokenSetting(): Promise<void> {
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
}

View File

@ -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<void> {
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<string>('sourcegraph.url') || 'https://sourcegraph.com'
// get the URl from either, 1. extension local storage (new)
let url = extensionContext?.globalState.get<string>(endpointKey)
if (!url) {
// 2. settings.json (old)
url = vscode.workspace.getConfiguration().get<string>(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<void> {
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 {

View File

@ -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.'
)
}
})
)
}

View File

@ -1,5 +0,0 @@
import * as vscode from 'vscode'
export function readConfiguration(): vscode.WorkspaceConfiguration {
return vscode.workspace.getConfiguration('sourcegraph')
}

View File

@ -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,

View File

@ -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<void> {
setEndpoint(undefined)
await this.secretStorage.delete(secretTokenKey)
await commands.executeCommand('workbench.action.reloadWindow')
return

View File

@ -280,8 +280,7 @@ export const AuthSidebarView: React.FunctionComponent<React.PropsWithChildren<Au
</VSCodeButton>
{state === 'failure' && (
<Alert variant="danger" className={classNames(styles.ctaParagraph, 'my-1')}>
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.
</Alert>
)}
<Text className="my-0">