Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Commit 5ee5e88

Browse files
authored
fix(search): VSCode Search extension: remove auth provider (#63262)
VSCode extensions can use authentication providers to enable authentication actions from the Accounts menu, and to allow for federated authentication. The Sourcegraph Search extension needs neither of those - it can meet all of its authentication needs with the existing actions in is side panel. On top of that, the authentication provider isn't implemented quite correctly, which leads to issues like #43608. This PR removes the authentication provider. It retains all of the expected behavior of the extension: the user can still authenticate against sourcegraph.com or private instances using an access token, and log out. ## Test plan ### First Build and run locally. ``` git switch peterguy/vscode-remove-auth-provider 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. - Note that the Accounts icon does not display a "1" badge and "Sign in with SOURCEGRAPH_AUTH" is not present in the 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`. - Check the Accounts icon and menu - should not be anything there that's a result of Sourcegraph Search. - In the Help and Feedback section, click your username to open the logout panel, then log out. - Note again, the lack of modifications to the Accounts icon and menu from the Sourcegraph Search extension. ## Changelog - Remove interaction between the Sourcegraph Search extension and the Accounts menu.
1 parent c521d98 commit 5ee5e88

File tree

8 files changed

+34
-207
lines changed

8 files changed

+34
-207
lines changed

client/vscode/src/backend/instanceVersion.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import { requestGraphQLFromVSCode } from './requestGraphQl'
1818
* - insiders version format: 134683_2022-03-02_5188fes0101
1919
*/
2020
export const observeInstanceVersionNumber = (
21-
accessToken: string,
22-
endpointURL: string
21+
accessToken?: string,
22+
endpointURL?: string
2323
): Observable<string | undefined> =>
2424
from(requestGraphQLFromVSCode<SiteVersionResult>(siteVersionQuery, {}, accessToken, endpointURL)).pipe(
2525
map(dataOrThrowErrors),

client/vscode/src/backend/requestGraphQl.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import { authentication } from 'vscode'
2-
31
import { asError } from '@sourcegraph/common'
42
import { checkOk, GRAPHQL_URI, type GraphQLResult, isHTTPAuthError } from '@sourcegraph/http-client'
53

6-
import { handleAccessTokenError } from '../settings/accessTokenSetting'
4+
import { handleAccessTokenError, getAccessToken } from '../settings/accessTokenSetting'
75
import { endpointRequestHeadersSetting, endpointSetting } from '../settings/endpointSetting'
86

97
import { fetch, getProxyAgent, Headers, type HeadersInit } from './fetch'
@@ -15,9 +13,7 @@ export const requestGraphQLFromVSCode = async <R, V = object>(
1513
overrideSourcegraphURL?: string
1614
): Promise<GraphQLResult<R>> => {
1715
const sourcegraphURL = overrideSourcegraphURL || endpointSetting()
18-
const accessToken =
19-
overrideAccessToken ||
20-
(await authentication.getSession(sourcegraphURL, [], { createIfNone: false }))?.accessToken
16+
const accessToken = overrideAccessToken || (await getAccessToken())
2117
const nameMatch = request.match(/^\s*(?:query|mutation)\s+(\w+)/)
2218
const apiURL = `${GRAPHQL_URI}${nameMatch ? '?' + nameMatch[1] : ''}`
2319
const customHeaders = endpointRequestHeadersSetting()

client/vscode/src/backend/streamSearch.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,21 @@ import { aggregateStreamingSearch } from '@sourcegraph/shared/src/search/stream'
88

99
import type { ExtensionCoreAPI } from '../contract'
1010
import { SearchPatternType } from '../graphql-operations'
11+
import { getAccessToken } from '../settings/accessTokenSetting'
1112
import type { VSCEStateMachine } from '../state'
1213
import { focusSearchPanel } from '../webview/commands'
1314

1415
import { isOlderThan, observeInstanceVersionNumber } from './instanceVersion'
1516

16-
export function createStreamSearch({
17+
export async function createStreamSearch({
1718
context,
1819
stateMachine,
1920
sourcegraphURL,
20-
session,
2121
}: {
2222
context: vscode.ExtensionContext
2323
stateMachine: VSCEStateMachine
2424
sourcegraphURL: string
25-
session: vscode.AuthenticationSession | undefined
26-
}): ExtensionCoreAPI['streamSearch'] {
25+
}): Promise<ExtensionCoreAPI['streamSearch']> {
2726
// Ensure only one search is active at a time
2827
let previousSearchSubscription: Subscription | null
2928

@@ -32,7 +31,7 @@ export function createStreamSearch({
3231
previousSearchSubscription?.unsubscribe()
3332
},
3433
})
35-
const token = session?.accessToken === undefined ? '' : session?.accessToken
34+
const token = await getAccessToken()
3635
const instanceVersionNumber = observeInstanceVersionNumber(token, sourcegraphURL)
3736

3837
return function streamSearch(query, options) {

client/vscode/src/extension.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ import { openSourcegraphUriCommand } from './file-system/commands'
1919
import { initializeSourcegraphFileSystem } from './file-system/initialize'
2020
import { SourcegraphUri } from './file-system/SourcegraphUri'
2121
import type { Event } from './graphql-operations'
22-
import { accessTokenSetting, processOldToken } from './settings/accessTokenSetting'
22+
import { getAccessToken, processOldToken } from './settings/accessTokenSetting'
2323
import { endpointRequestHeadersSetting, endpointSetting } from './settings/endpointSetting'
2424
import { LocalStorageService, SELECTED_SEARCH_CONTEXT_SPEC_KEY } from './settings/LocalStorageService'
2525
import { watchUninstall } from './settings/uninstall'
2626
import { createVSCEStateMachine, type VSCEQueryState } from './state'
2727
import { copySourcegraphLinks, focusSearchPanel, openSourcegraphLinks, registerWebviews } from './webview/commands'
28-
import { secretTokenKey, SourcegraphAuthActions, SourcegraphAuthProvider } from './webview/platform/AuthProvider'
28+
import { SourcegraphAuthActions } from './webview/platform/AuthProvider'
2929

3030
export let extensionContext: vscode.ExtensionContext
3131
/**
@@ -35,18 +35,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
3535
extensionContext = context
3636
const initialInstanceURL = endpointSetting()
3737
const secretStorage = context.secrets
38-
// Register SourcegraphAuthProvider
39-
context.subscriptions.push(
40-
vscode.authentication.registerAuthenticationProvider(
41-
initialInstanceURL,
42-
secretTokenKey,
43-
new SourcegraphAuthProvider(secretStorage)
44-
)
45-
)
4638
await processOldToken(secretStorage)
47-
const initialAccessToken = await secretStorage.get(secretTokenKey)
48-
const createIfNone = initialAccessToken ? { createIfNone: true } : { createIfNone: false }
49-
const session = await vscode.authentication.getSession(initialInstanceURL, [], createIfNone)
39+
const initialAccessToken = await getAccessToken()
5040
const authenticatedUser = observeAuthenticatedUser(secretStorage)
5141
const localStorageService = new LocalStorageService(context.globalState)
5242
const stateMachine = createVSCEStateMachine({ localStorageService })
@@ -70,11 +60,10 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
7060
// Use for file tree panel
7161
const { fs } = initializeSourcegraphFileSystem({ context, initialInstanceURL })
7262
// Use api endpoint for stream search
73-
const streamSearch = createStreamSearch({
63+
const streamSearch = await createStreamSearch({
7464
context,
7565
stateMachine,
7666
sourcegraphURL: `${initialInstanceURL}/.api`,
77-
session,
7867
})
7968
const authActions = new SourcegraphAuthActions(secretStorage)
8069
const extensionCoreAPI: ExtensionCoreAPI = {
@@ -91,7 +80,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
9180
openSourcegraphFile: (uri: string) => openSourcegraphUriCommand(fs, SourcegraphUri.parse(uri)),
9281
openLink: uri => openSourcegraphLinks(uri),
9382
copyLink: uri => copySourcegraphLinks(uri),
94-
getAccessToken: accessTokenSetting(context.secrets),
83+
getAccessToken: getAccessToken(),
9584
removeAccessToken: () => authActions.logout(),
9685
setEndpointUri: (accessToken, uri) => authActions.login(accessToken, uri),
9786
reloadWindow: () => vscode.commands.executeCommand('workbench.action.reloadWindow'),

client/vscode/src/settings/accessTokenSetting.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,32 @@
11
import * as vscode from 'vscode'
22

33
import { isOlderThan, observeInstanceVersionNumber } from '../backend/instanceVersion'
4+
import { extensionContext } from '../extension'
45
import { secretTokenKey } from '../webview/platform/AuthProvider'
56

67
import { endpointHostnameSetting, endpointProtocolSetting } from './endpointSetting'
78

89
// IMPORTANT: Call this function only once when extention is first activated
910
export async function processOldToken(secretStorage: vscode.SecretStorage): Promise<void> {
10-
// Process the token that lives in user configuration
11-
// Move them to secrets and then remove them by setting it as undefined
11+
// Process the token that used to live in user configuration
12+
// Move it to secrets and then remove it from user configuration
1213
const storageToken = await secretStorage.get(secretTokenKey)
1314
const oldToken = vscode.workspace.getConfiguration().get<string>('sourcegraph.accessToken') || ''
1415
if (!storageToken && oldToken.length > 8) {
1516
await secretStorage.store(secretTokenKey, oldToken)
16-
await removeOldAccessTokenSetting()
17+
await vscode.workspace
18+
.getConfiguration()
19+
.update('sourcegraph.accessToken', undefined, vscode.ConfigurationTarget.Global)
20+
await vscode.workspace
21+
.getConfiguration()
22+
.update('sourcegraph.accessToken', undefined, vscode.ConfigurationTarget.Workspace)
1723
}
1824
return
1925
}
2026

21-
export async function accessTokenSetting(secretStorage: vscode.SecretStorage): Promise<string> {
22-
const currentToken = await secretStorage.get(secretTokenKey)
23-
return currentToken || ''
24-
}
25-
26-
export async function removeOldAccessTokenSetting(): Promise<void> {
27-
await vscode.workspace
28-
.getConfiguration()
29-
.update('sourcegraph.accessToken', undefined, vscode.ConfigurationTarget.Global)
30-
await vscode.workspace
31-
.getConfiguration()
32-
.update('sourcegraph.accessToken', undefined, vscode.ConfigurationTarget.Workspace)
33-
return
27+
export async function getAccessToken(): Promise<string | undefined> {
28+
const token = await extensionContext?.secrets.get(secretTokenKey)
29+
return token
3430
}
3531

3632
// Ensure that only one access token error message is shown at a time.
@@ -42,7 +38,7 @@ export async function handleAccessTokenError(badToken: string, endpointURL: stri
4238

4339
const message = !badToken
4440
? `A valid access token is required to connect to ${endpointURL}`
45-
: `Connection to ${endpointURL} failed. Please try reloading VS Code if your Sourcegraph instance URL has been updated.`
41+
: `Connection to ${endpointURL} failed. Please check your access token and network connection.`
4642

4743
const version = await observeInstanceVersionNumber(badToken, endpointURL).toPromise()
4844
const supportsTokenCallback = version && isOlderThan(version, { major: 3, minor: 41 })

client/vscode/src/settings/endpointSetting.ts

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as vscode from 'vscode'
22

33
import { extensionContext } from '../extension'
4-
import { secretTokenKey, SourcegraphAuthProvider } from '../webview/platform/AuthProvider'
54

65
const defaultEndpointURL = 'https://sourcegraph.com'
76

@@ -43,28 +42,7 @@ export function setEndpoint(newEndpoint: string | undefined): void {
4342
const newEndpointHostname = new URL(newEndpointURL).hostname
4443
if (currentEndpointHostname !== newEndpointHostname) {
4544
extensionContext?.globalState.update(endpointKey, newEndpointURL).then(
46-
() => {
47-
// after changing the endpoint URL, register an authentication provder for it.
48-
// trying and erroring (because one already exists) is probably just as cheap/expensive
49-
// as trying `vscode.authentication.getSession(newEndpointURL, [], { createIfNone: false })`,
50-
// catching an error and registering an auth provider.
51-
try {
52-
const provider = vscode.authentication.registerAuthenticationProvider(
53-
newEndpointURL,
54-
secretTokenKey,
55-
new SourcegraphAuthProvider(extensionContext?.secrets)
56-
)
57-
extensionContext?.subscriptions.push(provider)
58-
} catch (error) {
59-
// unsetting the endpoint reverts to the default,
60-
// which probably already has an auth provider,
61-
// which would cause an error,
62-
// so ignore it.
63-
if (!(error as Error).message.includes('is already registered')) {
64-
console.error(error)
65-
}
66-
}
67-
},
45+
() => {},
6846
error => {
6947
console.error(error)
7048
}

client/vscode/src/webview/commands.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { initializeCodeIntel } from '../code-intel/initialize'
1010
import type { ExtensionCoreAPI } from '../contract'
1111
import type { SourcegraphFileSystemProvider } from '../file-system/SourcegraphFileSystemProvider'
1212
import { SearchPatternType } from '../graphql-operations'
13-
import { endpointRequestHeadersSetting, endpointSetting } from '../settings/endpointSetting'
13+
import { endpointRequestHeadersSetting } from '../settings/endpointSetting'
1414

1515
import {
1616
initializeHelpSidebarWebview,
@@ -59,14 +59,12 @@ export function registerWebviews({
5959
)
6060

6161
// Update `EventSource` Authorization header on access token / headers change.
62-
// It will also be changed when the token has been changed --handled by Auth Provider
6362
context.subscriptions.push(
6463
vscode.workspace.onDidChangeConfiguration(async config => {
65-
const session = await vscode.authentication.getSession(endpointSetting(), [], { forceNewSession: false })
66-
if (config.affectsConfiguration('sourcegraph.requestHeaders') && session) {
67-
const newCustomHeaders = endpointRequestHeadersSetting()
64+
if (config.affectsConfiguration('sourcegraph.requestHeaders')) {
65+
const token = await extensionCoreAPI.getAccessToken
6866
polyfillEventSource(
69-
session.accessToken ? { Authorization: `token ${session.accessToken}`, ...newCustomHeaders } : {},
67+
token ? { Authorization: `token ${token}`, ...endpointRequestHeadersSetting() } : {},
7068
getProxyAgent()
7169
)
7270
}

client/vscode/src/webview/platform/AuthProvider.ts

Lines changed: 3 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,16 @@
1-
import {
2-
authentication,
3-
type AuthenticationProvider,
4-
type AuthenticationProviderAuthenticationSessionsChangeEvent,
5-
type AuthenticationSession,
6-
commands,
7-
Disposable,
8-
type Event,
9-
EventEmitter,
10-
type SecretStorage,
11-
} from 'vscode'
1+
import { commands, type SecretStorage } from 'vscode'
122

13-
import polyfillEventSource from '@sourcegraph/shared/src/polyfills/vendor/eventSource'
14-
15-
import { getProxyAgent } from '../../backend/fetch'
16-
import { endpointRequestHeadersSetting, endpointSetting, setEndpoint } from '../../settings/endpointSetting'
3+
import { setEndpoint } from '../../settings/endpointSetting'
174

185
export const secretTokenKey = 'SOURCEGRAPH_AUTH'
196

20-
class SourcegraphAuthSession implements AuthenticationSession {
21-
public readonly account = {
22-
id: SourcegraphAuthProvider.id,
23-
label: SourcegraphAuthProvider.label,
24-
}
25-
public readonly id = SourcegraphAuthProvider.id
26-
public readonly scopes = []
27-
28-
constructor(public readonly accessToken: string) {}
29-
}
30-
31-
export class SourcegraphAuthProvider implements AuthenticationProvider, Disposable {
32-
public static id = endpointSetting()
33-
private static secretKey = secretTokenKey
34-
public static label = secretTokenKey
35-
36-
// Kept track of token changes through out the session
37-
private currentToken: string | undefined
38-
private initializedDisposable: Disposable | undefined
39-
private _onDidChangeSessions = new EventEmitter<AuthenticationProviderAuthenticationSessionsChangeEvent>()
40-
public get onDidChangeSessions(): Event<AuthenticationProviderAuthenticationSessionsChangeEvent> {
41-
return this._onDidChangeSessions.event
42-
}
43-
44-
constructor(private readonly secretStorage: SecretStorage) {}
45-
46-
public dispose(): void {
47-
this.initializedDisposable?.dispose()
48-
}
49-
50-
private async ensureInitialized(): Promise<void> {
51-
if (this.initializedDisposable === undefined) {
52-
await this.cacheTokenFromStorage()
53-
this.initializedDisposable = Disposable.from(
54-
this.secretStorage.onDidChange(async event => {
55-
if (event.key === SourcegraphAuthProvider.secretKey) {
56-
await this.checkForUpdates()
57-
}
58-
}),
59-
authentication.onDidChangeSessions(async event => {
60-
if (event.provider.id === SourcegraphAuthProvider.id) {
61-
await this.checkForUpdates()
62-
}
63-
})
64-
)
65-
}
66-
}
67-
68-
// Check if token has been updated across VS Code
69-
private async checkForUpdates(): Promise<void> {
70-
const added: AuthenticationSession[] = []
71-
const removed: AuthenticationSession[] = []
72-
const changed: AuthenticationSession[] = []
73-
const previousToken = this.currentToken
74-
const session = (await this.getSessions())[0]
75-
if (session?.accessToken && !previousToken) {
76-
added.push(session)
77-
} else if (!session?.accessToken && previousToken) {
78-
removed.push(session)
79-
} else if (session?.accessToken !== previousToken) {
80-
changed.push(session)
81-
} else {
82-
return
83-
}
84-
await this.cacheTokenFromStorage()
85-
// Update the polyfillEventSource on token changes
86-
polyfillEventSource(
87-
this.currentToken
88-
? { Authorization: `token ${this.currentToken}`, ...endpointRequestHeadersSetting() }
89-
: {},
90-
getProxyAgent()
91-
)
92-
this._onDidChangeSessions.fire({ added, removed, changed })
93-
}
94-
95-
// Get token from Storage
96-
private async cacheTokenFromStorage(): Promise<string | undefined> {
97-
const token = await this.secretStorage.get(SourcegraphAuthProvider.secretKey)
98-
this.currentToken = token
99-
return this.currentToken
100-
}
101-
102-
// This is called first when `vscode.authentication.getSessions` is called.
103-
public async getSessions(_scopes?: string[]): Promise<readonly AuthenticationSession[]> {
104-
await this.ensureInitialized()
105-
const token = await this.cacheTokenFromStorage()
106-
return token ? [new SourcegraphAuthSession(token)] : []
107-
}
108-
109-
// This is called after `this.getSessions` is called,
110-
// and only when `createIfNone` or `forceNewSession` are set to true
111-
public async createSession(_scopes: string[]): Promise<AuthenticationSession> {
112-
await this.ensureInitialized()
113-
// Get token from scret storage
114-
let token = await this.secretStorage.get(SourcegraphAuthProvider.secretKey)
115-
if (token) {
116-
console.log('Successfully logged in to Sourcegraph', token)
117-
await this.secretStorage.store(SourcegraphAuthProvider.secretKey, token)
118-
}
119-
if (!token) {
120-
token = ''
121-
}
122-
return new SourcegraphAuthSession(token)
123-
}
124-
125-
// To sign out
126-
public async removeSession(_sessionId: string): Promise<void> {
127-
await this.secretStorage.delete(SourcegraphAuthProvider.secretKey)
128-
console.log('Successfully logged out of Sourcegraph')
129-
}
130-
}
131-
1327
export class SourcegraphAuthActions {
133-
private currentEndpoint = endpointSetting()
134-
1358
constructor(private readonly secretStorage: SecretStorage) {}
1369

13710
public async login(newtoken: string, newuri: string): Promise<void> {
13811
try {
13912
await this.secretStorage.store(secretTokenKey, newtoken)
140-
if (this.currentEndpoint !== newuri) {
141-
setEndpoint(newuri)
142-
}
13+
setEndpoint(newuri)
14314
return
14415
} catch (error) {
14516
console.error(error)

0 commit comments

Comments
 (0)