From ede7e954b4894802ee0e6e4d577abb929b9bb5e1 Mon Sep 17 00:00:00 2001 From: Dominic Cooney Date: Fri, 24 Jan 2025 10:18:17 +0900 Subject: [PATCH] Revert "External Authentication Providers Support for Cody (#6526)" This reverts commit 1ed83929d7a65c6d6ef1edcdca0569ec84fb8a78. --- agent/scripts/reverse-proxy.py | 80 -------------- agent/src/AgentWorkspaceConfiguration.ts | 2 +- agent/src/agent.ts | 9 +- .../cli/command-auth/AuthenticatedAccount.ts | 5 +- agent/src/cli/command-auth/command-login.ts | 4 +- agent/src/cli/command-bench/command-bench.ts | 2 +- agent/src/cli/command-bench/llm-judge.ts | 5 +- agent/src/local-e2e/helpers.ts | 5 +- agent/src/vscode-shim.ts | 3 +- lib/shared/src/configuration.ts | 29 +---- .../src/configuration/auth-resolver.test.ts | 101 ------------------ lib/shared/src/configuration/auth-resolver.ts | 101 ------------------ lib/shared/src/configuration/resolver.ts | 66 ++++++------ .../FeatureFlagProvider.test.ts | 2 +- lib/shared/src/models/sync.ts | 6 +- .../completions/browserClient.ts | 11 +- .../src/sourcegraph-api/graphql/client.ts | 29 ++--- lib/shared/src/sourcegraph-api/rest/client.ts | 23 ++-- lib/shared/src/sourcegraph-api/utils.ts | 18 ---- vscode/package.json | 47 -------- vscode/src/auth/auth.ts | 37 ++++--- vscode/src/auth/token-receiver.ts | 7 +- .../autoedits/adapters/cody-gateway.test.ts | 2 +- vscode/src/autoedits/adapters/cody-gateway.ts | 7 +- .../src/autoedits/autoedits-provider.test.ts | 2 +- vscode/src/chat/agentic/DeepCody.test.ts | 2 +- vscode/src/chat/chat-view/ChatController.ts | 33 +++--- vscode/src/chat/chat-view/prompt.test.ts | 2 - vscode/src/completions/default-client.ts | 11 +- vscode/src/completions/nodeClient.ts | 5 +- vscode/src/completions/providers/fireworks.ts | 7 +- vscode/src/configuration.test.ts | 3 - vscode/src/configuration.ts | 2 - .../rewrite-keyword-query.test.ts | 5 +- vscode/src/main.ts | 7 +- .../src/notifications/setup-notification.ts | 2 +- vscode/src/services/AuthProvider.test.ts | 10 +- vscode/src/services/AuthProvider.ts | 2 +- vscode/src/services/LocalStorageProvider.ts | 14 +-- vscode/src/services/UpstreamHealthProvider.ts | 8 +- .../open-telemetry/CodyTraceExport.ts | 12 +-- .../OpenTelemetryService.node.ts | 2 +- .../services/open-telemetry/trace-sender.ts | 16 +-- vscode/src/testutils/mocks.ts | 1 - vscode/webviews/AppWrapperForTest.tsx | 5 +- 45 files changed, 167 insertions(+), 585 deletions(-) delete mode 100644 agent/scripts/reverse-proxy.py delete mode 100644 lib/shared/src/configuration/auth-resolver.test.ts delete mode 100644 lib/shared/src/configuration/auth-resolver.ts diff --git a/agent/scripts/reverse-proxy.py b/agent/scripts/reverse-proxy.py deleted file mode 100644 index bba319477c4c..000000000000 --- a/agent/scripts/reverse-proxy.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 - -from aiohttp import web, ClientSession -from urllib.parse import urlparse -import argparse -import asyncio -import re - -async def proxy_handler(request): - async with ClientSession(auto_decompress=False) as session: - print(f'Request to: {request.url}') - - # Modify headers here - headers = dict(request.headers) - - # Reset the Host header to use target server host instead of the proxy host - if 'Host' in headers: - headers['Host'] = urlparse(target_url).netloc.split(':')[0] - - # 'chunked' encoding results in error 400 from Cloudflare, removing it still keeps response chunked anyway - if 'Transfer-Encoding' in headers: - del headers['Transfer-Encoding'] - - # Use value of 'Authorization: Bearer' to fill 'X-Forwarded-User' and remove 'Authorization' header - if 'Authorization' in headers: - match = re.match('Bearer (.*)', headers['Authorization']) - if match: - headers['X-Forwarded-User'] = match.group(1) - del headers['Authorization'] - - # Forward the request to target - async with session.request( - method=request.method, - url=f'{target_url}{request.path_qs}', - headers=headers, - data=await request.read() - ) as response: - proxy_response = web.StreamResponse( - status=response.status, - headers=response.headers - ) - - await proxy_response.prepare(request) - - # Stream the response back - async for chunk in response.content.iter_chunks(): - await proxy_response.write(chunk[0]) - - await proxy_response.write_eof() - return proxy_response - -app = web.Application() -app.router.add_route('*', '/{path_info:.*}', proxy_handler) - -""" -Reverse Proxy Server for testing External Auth Providers in Cody - -This script implements a simple reverse proxy server to facilitate testing of external authentication providers -with Cody. It's role is to simulate simulate HTTP authentication proxy setups. It handles incoming requests by: -- Forwarding them to a target Sourcegraph instance -- Converting Bearer tokens from Authorization headers into X-Forwarded-User headers -- Managing request/response streaming -- Handling header modifications required for Cloudflare compatibility - -Target Sourcegraph instance needs to be configured to use HTTP authentication proxies -as described in https://sourcegraph.com/docs/admin/auth#http-authentication-proxies -""" -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='External auth provider test proxy server') - parser.add_argument('target_url', help='Target Sourcegraph instance URL to proxy to') - parser.add_argument('proxy_port', type=int, nargs='?', default=5555, - help='Port for the proxy server (default: %(default)s)') - - args = parser.parse_args() - - target_url = args.target_url.rstrip('/') - port = args.proxy_port - - print(f'Starting proxy server on port {port} targeting {target_url}...') - web.run_app(app, port=port) diff --git a/agent/src/AgentWorkspaceConfiguration.ts b/agent/src/AgentWorkspaceConfiguration.ts index 58e504f1e970..ac8df4a37045 100644 --- a/agent/src/AgentWorkspaceConfiguration.ts +++ b/agent/src/AgentWorkspaceConfiguration.ts @@ -116,7 +116,7 @@ export class AgentWorkspaceConfiguration implements vscode.WorkspaceConfiguratio function mergeWithBaseConfig(config: any) { for (const [key, value] of Object.entries(config)) { - if (typeof value === 'object' && !Array.isArray(value)) { + if (typeof value === 'object') { const existing = _.get(baseConfig, key) ?? {} const merged = _.merge(existing, value) _.set(baseConfig, key, merged) diff --git a/agent/src/agent.ts b/agent/src/agent.ts index 42f8b2fe8171..1bad13eda865 100644 --- a/agent/src/agent.ts +++ b/agent/src/agent.ts @@ -1486,10 +1486,11 @@ export class Agent extends MessageHandler implements ExtensionClient { config: ExtensionConfiguration, params?: { forceAuthentication: boolean } ): Promise { - const isAuthChange = vscode_shim.isTokenOrEndpointChange(config) + const isAuthChange = vscode_shim.isAuthenticationChange(config) vscode_shim.setExtensionConfiguration(config) - // If this is an token or endpoint change we need to save them prior to firing events that update the clients + // If this is an authentication change we need to reauthenticate prior to firing events + // that update the clients try { if ((isAuthChange || params?.forceAuthentication) && config.serverEndpoint) { await authProvider.validateAndStoreCredentials( @@ -1499,9 +1500,7 @@ export class Agent extends MessageHandler implements ExtensionClient { }, auth: { serverEndpoint: config.serverEndpoint, - credentials: config.accessToken - ? { token: config.accessToken, source: 'paste' } - : undefined, + accessToken: config.accessToken ?? null, }, clientState: { anonymousUserID: config.anonymousUserID ?? null, diff --git a/agent/src/cli/command-auth/AuthenticatedAccount.ts b/agent/src/cli/command-auth/AuthenticatedAccount.ts index a631d11827e8..9a228e3fb224 100644 --- a/agent/src/cli/command-auth/AuthenticatedAccount.ts +++ b/agent/src/cli/command-auth/AuthenticatedAccount.ts @@ -42,10 +42,7 @@ export class AuthenticatedAccount { ): Promise { const graphqlClient = SourcegraphGraphQLAPIClient.withStaticConfig({ configuration: { telemetryLevel: 'agent' }, - auth: { - credentials: { token: options.accessToken }, - serverEndpoint: options.endpoint, - }, + auth: { accessToken: options.accessToken, serverEndpoint: options.endpoint }, clientState: { anonymousUserID: null }, }) const userInfo = await graphqlClient.getCurrentUserInfo() diff --git a/agent/src/cli/command-auth/command-login.ts b/agent/src/cli/command-auth/command-login.ts index dacd412bcd33..2cc9aedb745d 100644 --- a/agent/src/cli/command-auth/command-login.ts +++ b/agent/src/cli/command-auth/command-login.ts @@ -123,7 +123,7 @@ async function loginAction( : await captureAccessTokenViaBrowserRedirect(serverEndpoint, spinner) const client = SourcegraphGraphQLAPIClient.withStaticConfig({ configuration: { telemetryLevel: 'agent' }, - auth: { credentials: { token: options.accessToken }, serverEndpoint: serverEndpoint }, + auth: { accessToken: token, serverEndpoint: serverEndpoint }, clientState: { anonymousUserID: null }, }) const userInfo = await client.getCurrentUserInfo() @@ -256,7 +256,7 @@ async function promptUserAboutLoginMethod(spinner: Ora, options: LoginOptions): try { const client = SourcegraphGraphQLAPIClient.withStaticConfig({ configuration: { telemetryLevel: 'agent' }, - auth: { credentials: { token: options.accessToken }, serverEndpoint: options.endpoint }, + auth: { accessToken: options.accessToken, serverEndpoint: options.endpoint }, clientState: { anonymousUserID: null }, }) const userInfo = await client.getCurrentUserInfo() diff --git a/agent/src/cli/command-bench/command-bench.ts b/agent/src/cli/command-bench/command-bench.ts index 98dbc1025df5..17da06851ff5 100644 --- a/agent/src/cli/command-bench/command-bench.ts +++ b/agent/src/cli/command-bench/command-bench.ts @@ -331,7 +331,7 @@ export const benchCommand = new commander.Command('bench') setStaticResolvedConfigurationWithAuthCredentials({ configuration: { customHeaders: {} }, auth: { - credentials: { token: options.srcAccessToken }, + accessToken: options.srcAccessToken, serverEndpoint: options.srcEndpoint, }, }) diff --git a/agent/src/cli/command-bench/llm-judge.ts b/agent/src/cli/command-bench/llm-judge.ts index cf6f6c3b076d..8df9d4510c2f 100644 --- a/agent/src/cli/command-bench/llm-judge.ts +++ b/agent/src/cli/command-bench/llm-judge.ts @@ -16,10 +16,7 @@ export class LlmJudge { localStorage.setStorage('noop') setStaticResolvedConfigurationWithAuthCredentials({ configuration: { customHeaders: undefined }, - auth: { - credentials: { token: options.srcAccessToken }, - serverEndpoint: options.srcEndpoint, - }, + auth: { accessToken: options.srcAccessToken, serverEndpoint: options.srcEndpoint }, }) setClientCapabilities({ configuration: {}, agentCapabilities: undefined }) this.client = new SourcegraphNodeCompletionsClient() diff --git a/agent/src/local-e2e/helpers.ts b/agent/src/local-e2e/helpers.ts index f6a9d64569b1..703a4817c061 100644 --- a/agent/src/local-e2e/helpers.ts +++ b/agent/src/local-e2e/helpers.ts @@ -96,10 +96,7 @@ export class LocalSGInstance { // for checking the LLM configuration section. this.gqlclient = SourcegraphGraphQLAPIClient.withStaticConfig({ configuration: { customHeaders: headers, telemetryLevel: 'agent' }, - auth: { - credentials: { token: this.params.accessToken }, - serverEndpoint: this.params.serverEndpoint, - }, + auth: { accessToken: this.params.accessToken, serverEndpoint: this.params.serverEndpoint }, clientState: { anonymousUserID: null }, }) } diff --git a/agent/src/vscode-shim.ts b/agent/src/vscode-shim.ts index c2d258b6d20d..9b856be75ae2 100644 --- a/agent/src/vscode-shim.ts +++ b/agent/src/vscode-shim.ts @@ -136,8 +136,7 @@ export let extensionConfiguration: ExtensionConfiguration | undefined export function setExtensionConfiguration(newConfig: ExtensionConfiguration): void { extensionConfiguration = newConfig } - -export function isTokenOrEndpointChange(newConfig: ExtensionConfiguration): boolean { +export function isAuthenticationChange(newConfig: ExtensionConfiguration): boolean { if (!extensionConfiguration) { return true } diff --git a/lib/shared/src/configuration.ts b/lib/shared/src/configuration.ts index 3e43fc12f89d..fc38f13a51be 100644 --- a/lib/shared/src/configuration.ts +++ b/lib/shared/src/configuration.ts @@ -17,18 +17,8 @@ export type TokenSource = 'redirect' | 'paste' */ export interface AuthCredentials { serverEndpoint: string - credentials: HeaderCredential | TokenCredential | undefined -} - -export interface HeaderCredential { - // We use function instead of property to prevent accidential top level serialization - we never want to store this data - getHeaders(): Record - expiration: number | undefined -} - -export interface TokenCredential { - token: string - source?: TokenSource + accessToken: string | null + tokenSource?: TokenSource | undefined } export interface AutoEditsTokenLimit { @@ -81,19 +71,6 @@ export interface AgenticContextConfiguration { } } -export interface ExternalAuthCommand { - commandLine: readonly string[] - environment?: Record - shell?: string - timeout?: number - windowsHide?: boolean -} - -export interface ExternalAuthProvider { - endpoint: string - executable: ExternalAuthCommand -} - interface RawClientConfiguration { net: NetConfiguration codebase?: string @@ -188,8 +165,6 @@ interface RawClientConfiguration { */ overrideServerEndpoint?: string | undefined overrideAuthToken?: string | undefined - - authExternalProviders: ExternalAuthProvider[] } /** diff --git a/lib/shared/src/configuration/auth-resolver.test.ts b/lib/shared/src/configuration/auth-resolver.test.ts deleted file mode 100644 index 52f69e429e38..000000000000 --- a/lib/shared/src/configuration/auth-resolver.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { describe, expect, test } from 'vitest' -import { type HeaderCredential, type TokenSource, isWindows } from '..' -import { resolveAuth } from './auth-resolver' -import type { ClientSecrets } from './resolver' - -class TempClientSecrets implements ClientSecrets { - constructor(readonly store: Map) {} - - getToken(endpoint: string): Promise { - return Promise.resolve(this.store.get(endpoint)?.[0]) - } - getTokenSource(endpoint: string): Promise { - return Promise.resolve(this.store.get(endpoint)?.[1]) - } -} - -describe('auth-resolver', () => { - test('resolve with serverEndpoint and credentials overrides', async () => { - const auth = await resolveAuth( - 'sourcegraph.com', - { - authExternalProviders: [], - overrideServerEndpoint: 'my-endpoint.com', - overrideAuthToken: 'my-token', - }, - new TempClientSecrets(new Map([['sourcegraph.com/', ['sgp_212323123', 'paste']]])) - ) - - expect(auth.serverEndpoint).toBe('my-endpoint.com/') - expect(auth.credentials).toEqual({ token: 'my-token' }) - }) - - test('resolve with serverEndpoint override', async () => { - const auth = await resolveAuth( - 'sourcegraph.com', - { - authExternalProviders: [], - overrideServerEndpoint: 'my-endpoint.com', - overrideAuthToken: undefined, - }, - new TempClientSecrets(new Map([['my-endpoint.com/', ['sgp_212323123', 'paste']]])) - ) - - expect(auth.serverEndpoint).toBe('my-endpoint.com/') - expect(auth.credentials).toEqual({ token: 'sgp_212323123', source: 'paste' }) - }) - - test('resolve with token override', async () => { - const auth = await resolveAuth( - 'sourcegraph.com', - { - authExternalProviders: [], - overrideServerEndpoint: undefined, - overrideAuthToken: 'my-token', - }, - new TempClientSecrets(new Map([['sourcegraph.com/', ['sgp_777777777', 'paste']]])) - ) - - expect(auth.serverEndpoint).toBe('sourcegraph.com/') - expect(auth.credentials).toEqual({ token: 'my-token' }) - }) - - test('resolve custom auth provider', async () => { - const credentialsJson = JSON.stringify({ - headers: { Authorization: 'token X' }, - expiration: 1337, - }) - - const auth = await resolveAuth( - 'sourcegraph.com', - { - authExternalProviders: [ - { - endpoint: 'https://my-server.com', - executable: { - commandLine: [ - isWindows() ? `echo ${credentialsJson}` : `echo '${credentialsJson}'`, - ], - shell: isWindows() ? process.env.ComSpec : '/bin/bash', - timeout: 5000, - windowsHide: true, - }, - }, - ], - overrideServerEndpoint: 'https://my-server.com', - overrideAuthToken: undefined, - }, - new TempClientSecrets(new Map()) - ) - - expect(auth.serverEndpoint).toBe('https://my-server.com/') - - const headerCredential = auth.credentials as HeaderCredential - expect(headerCredential.expiration).toBe(1337) - expect(headerCredential.getHeaders()).toStrictEqual({ - Authorization: 'token X', - }) - - expect(JSON.stringify(headerCredential)).not.toContain('token X') - }) -}) diff --git a/lib/shared/src/configuration/auth-resolver.ts b/lib/shared/src/configuration/auth-resolver.ts deleted file mode 100644 index 414a21302262..000000000000 --- a/lib/shared/src/configuration/auth-resolver.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { - AuthCredentials, - ClientConfiguration, - ExternalAuthCommand, - ExternalAuthProvider, -} from '../configuration' -import type { ClientSecrets } from './resolver' - -export function normalizeServerEndpointURL(url: string): string { - return url.endsWith('/') ? url : `${url}/` -} - -async function executeCommand(cmd: ExternalAuthCommand): Promise { - if (typeof process === 'undefined' || !process.version) { - throw new Error('Command execution is only supported in Node.js environments') - } - - const { exec } = await import('node:child_process') - const { promisify } = await import('node:util') - const execAsync = promisify(exec) - - const command = cmd.commandLine.join(' ') - - // No need to check error code, promisify causes exec to throw in case of errors - const { stdout } = await execAsync(command, { - shell: cmd.shell, - timeout: cmd.timeout, - windowsHide: cmd.windowsHide, - env: { ...process.env, ...cmd.environment }, - }) - - return stdout.trim() -} - -interface HeaderCredentialResult { - headers: Record - expiration?: number | undefined -} - -async function getExternalProviderAuthResult( - serverEndpoint: string, - authExternalProviders: readonly ExternalAuthProvider[] -): Promise { - const externalProvider = authExternalProviders.find( - provider => normalizeServerEndpointURL(provider.endpoint) === serverEndpoint - ) - - if (externalProvider) { - const result = await executeCommand(externalProvider.executable) - return JSON.parse(result) - } - - return undefined -} - -export async function resolveAuth( - endpoint: string, - configuration: Pick< - ClientConfiguration, - 'authExternalProviders' | 'overrideServerEndpoint' | 'overrideAuthToken' - >, - clientSecrets: ClientSecrets -): Promise { - const { authExternalProviders, overrideServerEndpoint, overrideAuthToken } = configuration - const serverEndpoint = normalizeServerEndpointURL(overrideServerEndpoint || endpoint) - - if (overrideAuthToken) { - return { credentials: { token: overrideAuthToken }, serverEndpoint } - } - - const credentials = await getExternalProviderAuthResult(serverEndpoint, authExternalProviders).catch( - error => { - throw new Error(`Failed to execute external auth command: ${error}`) - } - ) - - if (credentials) { - return { - credentials: { - expiration: credentials?.expiration, - getHeaders() { - return credentials.headers - }, - }, - serverEndpoint, - } - } - - const token = await clientSecrets.getToken(serverEndpoint).catch(error => { - throw new Error( - `Failed to get access token for endpoint ${serverEndpoint}: ${error.message || error}` - ) - }) - - return { - credentials: token - ? { token, source: await clientSecrets.getTokenSource(serverEndpoint) } - : undefined, - serverEndpoint, - } -} diff --git a/lib/shared/src/configuration/resolver.ts b/lib/shared/src/configuration/resolver.ts index 4cdc90d95e27..331309fbfc7f 100644 --- a/lib/shared/src/configuration/resolver.ts +++ b/lib/shared/src/configuration/resolver.ts @@ -1,19 +1,16 @@ -import { Observable, Subject, map } from 'observable-fns' -import type { AuthCredentials, ClientConfiguration, TokenSource } from '../configuration' +import { Observable, map } from 'observable-fns' +import type { AuthCredentials, ClientConfiguration } from '../configuration' import { logError } from '../logger' import { - combineLatest, distinctUntilChanged, firstValueFrom, fromLateSetSource, promiseToObservable, - startWith, } from '../misc/observable' import { skipPendingOperation, switchMapReplayOperation } from '../misc/observableOperation' import type { DefaultsAndUserPreferencesByEndpoint } from '../models/modelsService' import { DOTCOM_URL } from '../sourcegraph-api/environments' import { type PartialDeep, type ReadonlyDeep, isError } from '../utils' -import { resolveAuth } from './auth-resolver' /** * The input from various sources that is needed to compute the {@link ResolvedConfiguration}. @@ -30,7 +27,6 @@ export interface ConfigurationInput { export interface ClientSecrets { getToken(endpoint: string): Promise - getTokenSource(endpoint: string): Promise } export interface ClientState { @@ -81,44 +77,42 @@ async function resolveConfiguration({ clientSecrets, clientState, reinstall: { isReinstalling, onReinstall }, -}: ConfigurationInput): Promise { +}: ConfigurationInput): Promise { const isReinstall = await isReinstalling() if (isReinstall) { await onReinstall() } - - const serverEndpoint = + // we allow for overriding the server endpoint from config if we haven't + // manually signed in somewhere else + const serverEndpoint = normalizeServerEndpointURL( clientConfiguration.overrideServerEndpoint || - clientState.lastUsedEndpoint || - DOTCOM_URL.toString() + (clientState.lastUsedEndpoint ?? DOTCOM_URL.toString()) + ) - try { - const auth = await resolveAuth(serverEndpoint, clientConfiguration, clientSecrets) - const cred = auth.credentials - if (cred !== undefined && 'expiration' in cred && cred.expiration !== undefined) { - const expirationMs = cred.expiration * 1000 - const expireInMs = expirationMs - Date.now() - if (expireInMs < 0) { - throw new Error( - 'Credentials expiration cannot be se to the past date:' + - `${new Date(expirationMs)} (${cred.expiration})` - ) - } - setInterval(() => _refreshConfigRequests.next(), expireInMs) - } - return { configuration: clientConfiguration, clientState, auth, isReinstall } - } catch (error) { - // We don't want to throw here, because that would cause the observable to terminate and - // all callers receiving no further config updates. - logError('resolveConfiguration', `Error resolving configuration: ${error}`) - const auth = { credentials: undefined, serverEndpoint } - return { configuration: clientConfiguration, clientState, auth, isReinstall } + // We must not throw here, because that would result in the `resolvedConfig` observable + // terminating and all callers receiving no further config updates. + const loadTokenFn = () => + clientSecrets.getToken(serverEndpoint).catch(error => { + logError( + 'resolveConfiguration', + `Failed to get access token for endpoint ${serverEndpoint}: ${error}` + ) + return null + }) + const accessToken = clientConfiguration.overrideAuthToken || ((await loadTokenFn()) ?? null) + return { + configuration: clientConfiguration, + clientState, + auth: { accessToken, serverEndpoint }, + isReinstall, } } -const _resolvedConfig = fromLateSetSource() +export function normalizeServerEndpointURL(url: string): string { + return url.endsWith('/') ? url : `${url}/` +} -const _refreshConfigRequests = new Subject() +const _resolvedConfig = fromLateSetSource() /** * Set the observable that will be used to provide the global {@link resolvedConfig}. This should be @@ -126,8 +120,8 @@ const _refreshConfigRequests = new Subject() */ export function setResolvedConfigurationObservable(input: Observable): void { _resolvedConfig.setSource( - combineLatest(input, _refreshConfigRequests.pipe(startWith(undefined))).pipe( - switchMapReplayOperation(([input]) => promiseToObservable(resolveConfiguration(input))), + input.pipe( + switchMapReplayOperation(input => promiseToObservable(resolveConfiguration(input))), skipPendingOperation(), map(value => { if (isError(value)) { diff --git a/lib/shared/src/experimentation/FeatureFlagProvider.test.ts b/lib/shared/src/experimentation/FeatureFlagProvider.test.ts index f3bb5c50e9a5..43bea01c31d3 100644 --- a/lib/shared/src/experimentation/FeatureFlagProvider.test.ts +++ b/lib/shared/src/experimentation/FeatureFlagProvider.test.ts @@ -24,7 +24,7 @@ describe('FeatureFlagProvider', () => { beforeAll(() => { vi.useFakeTimers() mockResolvedConfig({ - auth: { credentials: undefined, serverEndpoint: 'https://example.com' }, + auth: { accessToken: null, serverEndpoint: 'https://example.com' }, }) mockAuthStatus(AUTH_STATUS_FIXTURE_AUTHED) }) diff --git a/lib/shared/src/models/sync.ts b/lib/shared/src/models/sync.ts index 02a8b5f5f7fc..bd60b670e9f9 100644 --- a/lib/shared/src/models/sync.ts +++ b/lib/shared/src/models/sync.ts @@ -430,7 +430,11 @@ async function fetchServerSideModels( ): Promise { // Fetch the data via REST API. // NOTE: We may end up exposing this data via GraphQL, it's still TBD. - const client = new RestClient(config.auth, config.configuration.customHeaders) + const client = new RestClient( + config.auth.serverEndpoint, + config.auth.accessToken ?? undefined, + config.configuration.customHeaders + ) return await client.getAvailableModels(signal) } diff --git a/lib/shared/src/sourcegraph-api/completions/browserClient.ts b/lib/shared/src/sourcegraph-api/completions/browserClient.ts index 8475b4f3aa74..65b8d7ec8996 100644 --- a/lib/shared/src/sourcegraph-api/completions/browserClient.ts +++ b/lib/shared/src/sourcegraph-api/completions/browserClient.ts @@ -4,7 +4,6 @@ import { dependentAbortController } from '../../common/abortController' import { currentResolvedConfig } from '../../configuration/resolver' import { isError } from '../../utils' import { addClientInfoParams, addCodyClientIdentificationHeaders } from '../client-name-version' -import { addAuthHeaders } from '../utils' import { CompletionsResponseBuilder } from './CompletionsResponseBuilder' import { type CompletionRequestParameters, SourcegraphCompletionsClient } from './client' @@ -40,9 +39,10 @@ export class SourcegraphBrowserCompletionsClient extends SourcegraphCompletionsC ...requestParams.customHeaders, } as HeadersInit) addCodyClientIdentificationHeaders(headersInstance) - addAuthHeaders(config.auth, headersInstance, url) headersInstance.set('Content-Type', 'application/json; charset=utf-8') - + if (config.auth.accessToken) { + headersInstance.set('Authorization', `token ${config.auth.accessToken}`) + } const parameters = new URLSearchParams(globalThis.location.search) const trace = parameters.get('trace') if (trace) { @@ -132,8 +132,9 @@ export class SourcegraphBrowserCompletionsClient extends SourcegraphCompletionsC ...requestParams.customHeaders, }) addCodyClientIdentificationHeaders(headersInstance) - addAuthHeaders(auth, headersInstance, url) - + if (auth.accessToken) { + headersInstance.set('Authorization', `token ${auth.accessToken}`) + } if (new URLSearchParams(globalThis.location.search).get('trace')) { headersInstance.set('X-Sourcegraph-Should-Trace', 'true') } diff --git a/lib/shared/src/sourcegraph-api/graphql/client.ts b/lib/shared/src/sourcegraph-api/graphql/client.ts index b0782dd4102e..e2aeffbb1ee8 100644 --- a/lib/shared/src/sourcegraph-api/graphql/client.ts +++ b/lib/shared/src/sourcegraph-api/graphql/client.ts @@ -17,7 +17,6 @@ import { addTraceparent, wrapInActiveSpan } from '../../tracing' import { isError } from '../../utils' import { addCodyClientIdentificationHeaders } from '../client-name-version' import { isAbortError } from '../errors' -import { addAuthHeaders } from '../utils' import { type GraphQLResultCache, ObservableInvalidatedGraphQLResultCacheFactory } from './cache' import { BUILTIN_PROMPTS_QUERY, @@ -1560,23 +1559,23 @@ export class SourcegraphGraphQLAPIClient { const headers = new Headers(config.configuration?.customHeaders as HeadersInit | undefined) headers.set('Content-Type', 'application/json; charset=utf-8') + if (config.auth.accessToken) { + headers.set('Authorization', `token ${config.auth.accessToken}`) + } if (config.clientState.anonymousUserID && !process.env.CODY_WEB_DONT_SET_SOME_HEADERS) { headers.set('X-Sourcegraph-Actor-Anonymous-UID', config.clientState.anonymousUserID) } - const url = new URL( - buildGraphQLUrl({ - request: query, - baseUrl: config.auth.serverEndpoint, - }) - ) - addTraceparent(headers) addCodyClientIdentificationHeaders(headers) - addAuthHeaders(config.auth, headers, url) const queryName = query.match(QUERY_TO_NAME_REGEXP)?.[1] + const url = buildGraphQLUrl({ + request: query, + baseUrl: config.auth.serverEndpoint, + }) + const { abortController, timeoutSignal } = dependentAbortControllerWithTimeout(signal) return wrapInActiveSpan(`graphql.fetch${queryName ? `.${queryName}` : ''}`, () => fetch(url, { @@ -1587,7 +1586,7 @@ export class SourcegraphGraphQLAPIClient { }) .then(verifyResponseCode) .then(response => response.json() as T) - .catch(catchHTTPError(url.href, timeoutSignal)) + .catch(catchHTTPError(url, timeoutSignal)) ) } @@ -1613,15 +1612,17 @@ export class SourcegraphGraphQLAPIClient { const headers = new Headers(config.configuration?.customHeaders as HeadersInit | undefined) headers.set('Content-Type', 'application/json; charset=utf-8') + if (config.auth.accessToken) { + headers.set('Authorization', `token ${config.auth.accessToken}`) + } if (config.clientState.anonymousUserID && !process.env.CODY_WEB_DONT_SET_SOME_HEADERS) { headers.set('X-Sourcegraph-Actor-Anonymous-UID', config.clientState.anonymousUserID) } - const url = new URL(urlPath, config.auth.serverEndpoint) - addTraceparent(headers) addCodyClientIdentificationHeaders(headers) - addAuthHeaders(config.auth, headers, url) + + const url = new URL(urlPath, config.auth.serverEndpoint).href const { abortController, timeoutSignal } = dependentAbortControllerWithTimeout(signal) return wrapInActiveSpan(`httpapi.fetch${queryName ? `.${queryName}` : ''}`, () => @@ -1633,7 +1634,7 @@ export class SourcegraphGraphQLAPIClient { }) .then(verifyResponseCode) .then(response => response.json() as T) - .catch(catchHTTPError(url.href, timeoutSignal)) + .catch(catchHTTPError(url, timeoutSignal)) ) } } diff --git a/lib/shared/src/sourcegraph-api/rest/client.ts b/lib/shared/src/sourcegraph-api/rest/client.ts index e433a2ba1f7c..551ada9c70c0 100644 --- a/lib/shared/src/sourcegraph-api/rest/client.ts +++ b/lib/shared/src/sourcegraph-api/rest/client.ts @@ -1,6 +1,5 @@ import type { ServerModelConfiguration } from '../../models/modelsService' -import { type AuthCredentials, addAuthHeaders } from '../..' import { fetch } from '../../fetch' import { logError } from '../../logger' import { addTraceparent, wrapInActiveSpan } from '../../tracing' @@ -21,12 +20,14 @@ import { verifyResponseCode } from '../graphql/client' */ export class RestClient { /** - * Creates a new REST client to interact with a Sourcegraph instance. - * @param auth Authentication credentials containing endpoint URL and access token - * @param customHeaders Additional headers for requests (used by Cody Web to ensure proper auth flow) + * @param endpointUrl URL to the sourcegraph instance, e.g. "https://sourcegraph.acme.com". + * @param accessToken User access token to contact the sourcegraph instance. + * @param customHeaders Custom headers (primary is used by Cody Web case when Sourcegraph client + * providers set of custom headers to make sure that auth flow will work properly */ constructor( - private auth: AuthCredentials, + private endpointUrl: string, + private accessToken: string | undefined, private customHeaders: Record | undefined ) {} @@ -34,15 +35,15 @@ export class RestClient { // "name" is a developer-friendly term to label the request's trace span. private getRequest(name: string, urlSuffix: string, signal?: AbortSignal): Promise { const headers = new Headers(this.customHeaders) - - const endpoint = new URL(this.auth.serverEndpoint) - endpoint.pathname = urlSuffix - const url = endpoint.href - + if (this.accessToken) { + headers.set('Authorization', `token ${this.accessToken}`) + } addCodyClientIdentificationHeaders(headers) - addAuthHeaders(this.auth, headers, endpoint) addTraceparent(headers) + const endpoint = new URL(this.endpointUrl) + endpoint.pathname = urlSuffix + const url = endpoint.href return wrapInActiveSpan(`rest-api.${name}`, () => fetch(url, { method: 'GET', diff --git a/lib/shared/src/sourcegraph-api/utils.ts b/lib/shared/src/sourcegraph-api/utils.ts index 8a778f8c4516..a6d161b27903 100644 --- a/lib/shared/src/sourcegraph-api/utils.ts +++ b/lib/shared/src/sourcegraph-api/utils.ts @@ -2,9 +2,6 @@ // of a character, returns the remaining bytes of the partial character in a // new buffer. Note! This assumes that the prefix of buf *is* valid UTF8--it // only examines the bytes of the last character in the buffer and assumes it - -import type { AuthCredentials } from '..' - // will find an initial byte before the start of the buffer. export function toPartialUtf8String(buf: Buffer): { str: string; buf: Buffer } { if (buf.length === 0) { @@ -35,18 +32,3 @@ export function toPartialUtf8String(buf: Buffer): { str: string; buf: Buffer } { buf: Buffer.from(buf.slice(lastValidByteOffsetExclusive)), } } - -export function addAuthHeaders(auth: AuthCredentials, headers: Headers, url: URL): void { - // We want to be sure we sent authorization headers only to the valid endpoint - if (auth.credentials && url.host === new URL(auth.serverEndpoint).host) { - if ('token' in auth.credentials) { - headers.set('Authorization', `token ${auth.credentials.token}`) - } else if (typeof auth.credentials.getHeaders === 'function') { - for (const [key, value] of Object.entries(auth.credentials.getHeaders())) { - headers.set(key, value) - } - } else { - console.error('Cannot add headers: neither token nor headers found') - } - } -} diff --git a/vscode/package.json b/vscode/package.json index 62e0e1bea7cd..f22e960d9b32 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -1312,53 +1312,6 @@ "~/.mitmproxy/mitmproxy-ca-cert.pem", "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----" ] - }, - "cody.auth.externalProviders": { - "type": "array", - "markdownDescription": "Configure external authentication providers for Cody requests. Each provider consists of a command that generates HTTP headers used for authentication for a given endpoint.\n\n**How it works:**\n1. The specified command outputs a JSON object with header-value pairs\n2. These headers are included in authenticated Cody requests to the specified endpoint\n3. HTTP authentication proxy need to be used to enable custom authentication flows (e.g. JWT tokens, Oath2, etc)\n\nSee [HTTP Authentication Proxies](https://sourcegraph.com/docs/admin/auth#http-authentication-proxies) for proxy configuration.", - "items": { - "type": "object", - "required": ["endpoint", "executable"], - "properties": { - "endpoint": { - "type": "string", - "description": "The endpoint URL of the Sourcegraph instance" - }, - "executable": { - "type": "object", - "required": ["commandLine"], - "properties": { - "commandLine": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Command line arguments to execute the command." - }, - "environment": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Environment variables to set when executing the command." - }, - "shell": { - "type": "string", - "description": "If true, runs command inside of a shell. Uses '/bin/sh' on Unix, and process.env.ComSpec on Windows. A different shell can be specified as a string." - }, - "timeout": { - "type": "number", - "description": "Timeout for executing the command in milliseconds." - }, - "windowsHide": { - "type": "boolean", - "description": "Whether to hide the console window that would normally be created for the child process on Windows." - } - } - } - } - }, - "default": [] } } }, diff --git a/vscode/src/auth/auth.ts b/vscode/src/auth/auth.ts index 822614eb42c2..3f490c9b16d5 100644 --- a/vscode/src/auth/auth.ts +++ b/vscode/src/auth/auth.ts @@ -12,7 +12,6 @@ import { cenv, clientCapabilities, currentAuthStatus, - currentResolvedConfig, getAuthErrorMessage, getCodyAuthReferralCode, graphqlClient, @@ -21,7 +20,6 @@ import { isNetworkLikeError, telemetryRecorder, } from '@sourcegraph/cody-shared' -import { resolveAuth } from '@sourcegraph/cody-shared/src/configuration/auth-resolver' import { isSourcegraphToken } from '../chat/protocol' import { newAuthStatus } from '../chat/utils' import { logDebug } from '../output-channel-logger' @@ -85,22 +83,27 @@ export async function showSignInMenu( break } default: { - // Auto log user if token for the selected instance was found in secret or custom provider is configured + // Auto log user if token for the selected instance was found in secret const selectedEndpoint = item.uri - const { configuration } = await currentResolvedConfig() - const auth = await resolveAuth(selectedEndpoint, configuration, secretStorage) - - let authStatus = auth.credentials - ? await authProvider.validateAndStoreCredentials(auth, 'store-if-valid') + const token = await secretStorage.getToken(selectedEndpoint) + const tokenSource = await secretStorage.getTokenSource(selectedEndpoint) + let authStatus = token + ? await authProvider.validateAndStoreCredentials( + { serverEndpoint: selectedEndpoint, accessToken: token, tokenSource }, + 'store-if-valid' + ) : undefined - if (!authStatus?.authenticated) { - const token = await showAccessTokenInputBox(selectedEndpoint) - if (!token) { + const newToken = await showAccessTokenInputBox(selectedEndpoint) + if (!newToken) { return } authStatus = await authProvider.validateAndStoreCredentials( - { serverEndpoint: selectedEndpoint, credentials: { token, source: 'paste' } }, + { + serverEndpoint: selectedEndpoint, + accessToken: newToken, + tokenSource: 'paste', + }, 'store-if-valid' ) } @@ -225,12 +228,12 @@ const LoginMenuOptionItems = [ ] async function signinMenuForInstanceUrl(instanceUrl: string): Promise { - const token = await showAccessTokenInputBox(instanceUrl) - if (!token) { + const accessToken = await showAccessTokenInputBox(instanceUrl) + if (!accessToken) { return } const authStatus = await authProvider.validateAndStoreCredentials( - { serverEndpoint: instanceUrl, credentials: { token, source: 'paste' } }, + { serverEndpoint: instanceUrl, accessToken: accessToken, tokenSource: 'paste' }, 'store-if-valid' ) telemetryRecorder.recordEvent('cody.auth.signin.token', 'clicked', { @@ -309,7 +312,7 @@ export async function tokenCallbackHandler(uri: vscode.Uri): Promise { } const authStatus = await authProvider.validateAndStoreCredentials( - { serverEndpoint: endpoint, credentials: { token, source: 'redirect' } }, + { serverEndpoint: endpoint, accessToken: token, tokenSource: 'redirect' }, 'store-if-valid' ) telemetryRecorder.recordEvent('cody.auth.fromCallback.web', 'succeeded', { @@ -407,7 +410,7 @@ export async function validateCredentials( clientConfig?: CodyClientConfig ): Promise { // An access token is needed except for Cody Web, which uses cookies. - if (!config.auth.credentials && !clientCapabilities().isCodyWeb) { + if (!config.auth.accessToken && !clientCapabilities().isCodyWeb) { return { authenticated: false, endpoint: config.auth.serverEndpoint, pendingValidation: false } } diff --git a/vscode/src/auth/token-receiver.ts b/vscode/src/auth/token-receiver.ts index 490d8ac92a5d..0a93195690fe 100644 --- a/vscode/src/auth/token-receiver.ts +++ b/vscode/src/auth/token-receiver.ts @@ -14,7 +14,7 @@ const FIVE_MINUTES = 5 * 60 * 1000 // the user follow a redirect. export function startTokenReceiver( endpoint: string, - onNewToken: (credentials: Pick) => void, + onNewToken: (credentials: Pick) => void, timeout = FIVE_MINUTES ): Promise { const endpointUrl = new URL(endpoint) @@ -46,10 +46,7 @@ export function startTokenReceiver( 'accessToken' in json && typeof json.accessToken === 'string' ) { - onNewToken({ - serverEndpoint: endpoint, - credentials: { token: json.accessToken, source: 'redirect' }, - }) + onNewToken({ serverEndpoint: endpoint, accessToken: json.accessToken }) res.writeHead(200, headers) res.write('ok') diff --git a/vscode/src/autoedits/adapters/cody-gateway.test.ts b/vscode/src/autoedits/adapters/cody-gateway.test.ts index d776761602a3..f3fcbdb4dd09 100644 --- a/vscode/src/autoedits/adapters/cody-gateway.test.ts +++ b/vscode/src/autoedits/adapters/cody-gateway.test.ts @@ -33,7 +33,7 @@ describe('CodyGatewayAdapter', () => { mockResolvedConfig({ configuration: {}, auth: { - credentials: { token: 'sgp_local_f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0' }, + accessToken: 'sgp_local_f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0', serverEndpoint: DOTCOM_URL.toString(), }, }) diff --git a/vscode/src/autoedits/adapters/cody-gateway.ts b/vscode/src/autoedits/adapters/cody-gateway.ts index eab2caec7575..8e4100195eca 100644 --- a/vscode/src/autoedits/adapters/cody-gateway.ts +++ b/vscode/src/autoedits/adapters/cody-gateway.ts @@ -33,12 +33,7 @@ export class CodyGatewayAdapter implements AutoeditsModelAdapter { private async getApiKey(): Promise { const resolvedConfig = await currentResolvedConfig() - // TODO (pkukielka): Check if fastpath should support custom auth providers and how - const accessToken = - resolvedConfig.auth.credentials && 'token' in resolvedConfig.auth.credentials - ? resolvedConfig.auth.credentials.token - : null - const fastPathAccessToken = dotcomTokenToGatewayToken(accessToken) + const fastPathAccessToken = dotcomTokenToGatewayToken(resolvedConfig.auth.accessToken) if (!fastPathAccessToken) { autoeditsOutputChannelLogger.logError('getApiKey', 'FastPath access token is not available') throw new Error('FastPath access token is not available') diff --git a/vscode/src/autoedits/autoedits-provider.test.ts b/vscode/src/autoedits/autoedits-provider.test.ts index 2faf7dcb30c1..ed40c64b3270 100644 --- a/vscode/src/autoedits/autoedits-provider.test.ts +++ b/vscode/src/autoedits/autoedits-provider.test.ts @@ -60,7 +60,7 @@ describe('AutoeditsProvider', () => { mockResolvedConfig({ configuration: {}, auth: { - credentials: { token: 'sgp_local_f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0' }, + accessToken: 'sgp_local_f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0', serverEndpoint: DOTCOM_URL.toString(), }, }) diff --git a/vscode/src/chat/agentic/DeepCody.test.ts b/vscode/src/chat/agentic/DeepCody.test.ts index a3be8fac7c09..1eba1d683a22 100644 --- a/vscode/src/chat/agentic/DeepCody.test.ts +++ b/vscode/src/chat/agentic/DeepCody.test.ts @@ -57,7 +57,7 @@ describe('DeepCody', () => { } as any) beforeEach(async () => { - mockResolvedConfig({ configuration: {}, auth: { serverEndpoint: DOTCOM_URL.toString() } }) + mockResolvedConfig({ configuration: {} }) mockClientCapabilities(CLIENT_CAPABILITIES_FIXTURE) mockAuthStatus(codyProAuthStatus) localStorageData = {} diff --git a/vscode/src/chat/chat-view/ChatController.ts b/vscode/src/chat/chat-view/ChatController.ts index c478a5a1efdf..ce9a37a571fa 100644 --- a/vscode/src/chat/chat-view/ChatController.ts +++ b/vscode/src/chat/chat-view/ChatController.ts @@ -1,5 +1,4 @@ import { - type AuthCredentials, type AuthStatus, type BillingCategory, type BillingProduct, @@ -74,7 +73,6 @@ import * as vscode from 'vscode' import { type Span, context } from '@opentelemetry/api' import { captureException } from '@sentry/core' import type { SubMessage } from '@sourcegraph/cody-shared/src/chat/transcript/messages' -import { resolveAuth } from '@sourcegraph/cody-shared/src/configuration/auth-resolver' import type { TelemetryEventParameters } from '@sourcegraph/telemetry' import { Subject, map } from 'observable-fns' import type { URI } from 'vscode-uri' @@ -413,6 +411,10 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv ) break case 'auth': { + if (message.authKind === 'callback' && message.endpoint) { + redirectToEndpointLogin(message.endpoint) + break + } if (message.authKind === 'simplified-onboarding') { const endpoint = DOTCOM_URL.href @@ -452,28 +454,19 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv } break } - if ( - (message.authKind === 'signin' || message.authKind === 'callback') && - message.endpoint - ) { + if (message.authKind === 'signin' && message.endpoint) { try { const { endpoint, value: token } = message - let auth: AuthCredentials | undefined = undefined - - if (token) { - auth = { credentials: { token, source: 'paste' }, serverEndpoint: endpoint } - } else { - const { configuration } = await currentResolvedConfig() - auth = await resolveAuth(endpoint, configuration, secretStorage) + const credentials = { + serverEndpoint: endpoint, + accessToken: token || (await secretStorage.getToken(endpoint)) || null, + tokenSource: token ? 'paste' : await secretStorage.getTokenSource(endpoint), } - - if (!auth || !auth.credentials) { - return redirectToEndpointLogin(endpoint) + if (!credentials.accessToken) { + return redirectToEndpointLogin(credentials.serverEndpoint) } - - await authProvider.validateAndStoreCredentials(auth, 'always-store') + await authProvider.validateAndStoreCredentials(credentials, 'always-store') } catch (error) { - void vscode.window.showErrorMessage(`Authentication failed: ${error}`) this.postError(new Error(`Authentication failed: ${error}`)) } break @@ -509,7 +502,7 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv const authStatus = await authProvider.validateAndStoreCredentials( { serverEndpoint: DOTCOM_URL.href, - credentials: { token }, + accessToken: token, }, 'store-if-valid' ) diff --git a/vscode/src/chat/chat-view/prompt.test.ts b/vscode/src/chat/chat-view/prompt.test.ts index d16635f7aa06..b7652c1efa8a 100644 --- a/vscode/src/chat/chat-view/prompt.test.ts +++ b/vscode/src/chat/chat-view/prompt.test.ts @@ -8,7 +8,6 @@ import { type ModelsData, contextFiltersProvider, createModel, - graphqlClient, mockAuthStatus, mockClientCapabilities, mockResolvedConfig, @@ -25,7 +24,6 @@ import { DefaultPrompter } from './prompt' describe('DefaultPrompter', () => { beforeEach(() => { vi.spyOn(contextFiltersProvider, 'isUriIgnored').mockResolvedValue(false) - vi.spyOn(graphqlClient, 'fetchSourcegraphAPI').mockResolvedValue(true) mockAuthStatus(AUTH_STATUS_FIXTURE_AUTHED) mockResolvedConfig({ configuration: {} }) mockClientCapabilities(CLIENT_CAPABILITIES_FIXTURE) diff --git a/vscode/src/completions/default-client.ts b/vscode/src/completions/default-client.ts index 992b174d5642..7b3912745d6c 100644 --- a/vscode/src/completions/default-client.ts +++ b/vscode/src/completions/default-client.ts @@ -14,8 +14,6 @@ import { RateLimitError, type SerializedCodeCompletionsParams, TracedError, - addAuthHeaders, - addCodyClientIdentificationHeaders, addTraceparent, contextFiltersProvider, createSSEIterator, @@ -51,8 +49,8 @@ class DefaultCodeCompletionsClient implements CodeCompletionsClient { const { auth, configuration } = await currentResolvedConfig() const query = new URLSearchParams(getClientInfoParams()) - const url = new URL(`/.api/completions/code?${query.toString()}`, auth.serverEndpoint) - const log = autocompleteLifecycleOutputChannelLogger?.startCompletion(params, url.href) + const url = new URL(`/.api/completions/code?${query.toString()}`, auth.serverEndpoint).href + const log = autocompleteLifecycleOutputChannelLogger?.startCompletion(params, url) const { signal } = abortController return tracer.startActiveSpan( @@ -71,8 +69,9 @@ class DefaultCodeCompletionsClient implements CodeCompletionsClient { // Force HTTP connection reuse to reduce latency. // c.f. https://github.com/microsoft/vscode/issues/173861 headers.set('Content-Type', 'application/json; charset=utf-8') - addCodyClientIdentificationHeaders(headers) - addAuthHeaders(auth, headers, url) + if (auth.accessToken) { + headers.set('Authorization', `token ${auth.accessToken}`) + } if (tracingFlagEnabled) { headers.set('X-Sourcegraph-Should-Trace', '1') diff --git a/vscode/src/completions/nodeClient.ts b/vscode/src/completions/nodeClient.ts index cd561d66491f..4f2fc6500492 100644 --- a/vscode/src/completions/nodeClient.ts +++ b/vscode/src/completions/nodeClient.ts @@ -13,7 +13,6 @@ import { NetworkError, RateLimitError, SourcegraphCompletionsClient, - addAuthHeaders, addClientInfoParams, addCodyClientIdentificationHeaders, currentResolvedConfig, @@ -96,13 +95,13 @@ export class SourcegraphNodeCompletionsClient extends SourcegraphCompletionsClie // responses afterwards. 'Accept-Encoding': 'gzip;q=0', 'X-Sourcegraph-Interaction-ID': interactionId || '', + ...(auth.accessToken ? { Authorization: `token ${auth.accessToken}` } : null), ...configuration?.customHeaders, ...requestParams.customHeaders, ...getTraceparentHeaders(), Connection: 'keep-alive', }) addCodyClientIdentificationHeaders(headers) - addAuthHeaders(auth, headers, url) const request = requestFn( url, @@ -300,6 +299,7 @@ export class SourcegraphNodeCompletionsClient extends SourcegraphCompletionsClie const headers = new Headers({ 'Content-Type': 'application/json', 'Accept-Encoding': 'gzip;q=0', + ...(auth.accessToken ? { Authorization: `token ${auth.accessToken}` } : null), ...configuration.customHeaders, ...requestParams.customHeaders, ...getTraceparentHeaders(), @@ -307,7 +307,6 @@ export class SourcegraphNodeCompletionsClient extends SourcegraphCompletionsClie }) addCodyClientIdentificationHeaders(headers) - addAuthHeaders(auth, headers, url) const response = await fetch(url.toString(), { method: 'POST', diff --git a/vscode/src/completions/providers/fireworks.ts b/vscode/src/completions/providers/fireworks.ts index 50bc43ec82e8..8f8548dc5b25 100644 --- a/vscode/src/completions/providers/fireworks.ts +++ b/vscode/src/completions/providers/fireworks.ts @@ -133,12 +133,7 @@ class FireworksProvider extends Provider { typeof process !== 'undefined' if (canFastPathBeUsed) { - // TODO (pkukielka): Check if fastpath should support custom auth providers and how - const accessToken = - config.auth.credentials && 'token' in config.auth.credentials - ? config.auth.credentials.token - : null - const fastPathAccessToken = dotcomTokenToGatewayToken(accessToken) + const fastPathAccessToken = dotcomTokenToGatewayToken(config.auth.accessToken) const localFastPathAccessToken = process.env.NODE_ENV === 'development' diff --git a/vscode/src/configuration.test.ts b/vscode/src/configuration.test.ts index ec2d03661e1e..edaf96e05bca 100644 --- a/vscode/src/configuration.test.ts +++ b/vscode/src/configuration.test.ts @@ -138,8 +138,6 @@ describe('getConfiguration', () => { return false case 'cody.agentic.context.experimentalOptions': return { shell: { allow: ['git'] } } - case 'cody.auth.externalProviders': - return [] default: assert(false, `unexpected key: ${key}`) } @@ -208,7 +206,6 @@ describe('getConfiguration', () => { overrideAuthToken: undefined, overrideServerEndpoint: undefined, - authExternalProviders: [], } satisfies ClientConfiguration) }) }) diff --git a/vscode/src/configuration.ts b/vscode/src/configuration.ts index ea3572ef1b9e..caacb4b313d7 100644 --- a/vscode/src/configuration.ts +++ b/vscode/src/configuration.ts @@ -129,8 +129,6 @@ export function getConfiguration( */ agenticContextExperimentalOptions: config.get(CONFIG_KEY.agenticContextExperimentalOptions, {}), - authExternalProviders: config.get(CONFIG_KEY.authExternalProviders, []), - /** * Hidden settings for internal use only. */ diff --git a/vscode/src/local-context/rewrite-keyword-query.test.ts b/vscode/src/local-context/rewrite-keyword-query.test.ts index e0958e7fc089..099ab22236e5 100644 --- a/vscode/src/local-context/rewrite-keyword-query.test.ts +++ b/vscode/src/local-context/rewrite-keyword-query.test.ts @@ -30,9 +30,8 @@ describe('rewrite-query', () => { mockResolvedConfig({ configuration: { customHeaders: {} }, auth: { - credentials: { - token: TESTING_CREDENTIALS.dotcom.token ?? TESTING_CREDENTIALS.dotcom.redactedToken, - }, + accessToken: + TESTING_CREDENTIALS.dotcom.token ?? TESTING_CREDENTIALS.dotcom.redactedToken, serverEndpoint: TESTING_CREDENTIALS.dotcom.serverEndpoint, }, }) diff --git a/vscode/src/main.ts b/vscode/src/main.ts index c22275e114cb..e35e42dfc7e3 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -679,11 +679,8 @@ async function registerTestCommands( } }), // Access token - this is only used in configuration tests - vscode.commands.registerCommand('cody.test.token', async (serverEndpoint, token) => - authProvider.validateAndStoreCredentials( - { credentials: { token }, serverEndpoint }, - 'always-store' - ) + vscode.commands.registerCommand('cody.test.token', async (serverEndpoint, accessToken) => + authProvider.validateAndStoreCredentials({ serverEndpoint, accessToken }, 'always-store') ) ) } diff --git a/vscode/src/notifications/setup-notification.ts b/vscode/src/notifications/setup-notification.ts index e131eaf3c433..0c0812aea15a 100644 --- a/vscode/src/notifications/setup-notification.ts +++ b/vscode/src/notifications/setup-notification.ts @@ -8,7 +8,7 @@ import { telemetryRecorder } from '@sourcegraph/cody-shared' import { showActionNotification } from '.' export const showSetupNotification = async (auth: AuthCredentials): Promise => { - if (auth.serverEndpoint && auth.credentials) { + if (auth.serverEndpoint && auth.accessToken) { // User has already attempted to configure Cody. // Regardless of if they are authenticated or not, we don't want to prompt them. return diff --git a/vscode/src/services/AuthProvider.test.ts b/vscode/src/services/AuthProvider.test.ts index 3f6db32795f8..2ff7fd5a7ab0 100644 --- a/vscode/src/services/AuthProvider.test.ts +++ b/vscode/src/services/AuthProvider.test.ts @@ -81,7 +81,7 @@ describe('AuthProvider', () => { const { values, clearValues } = readValuesFrom(authStatus) resolvedConfig.next({ configuration: {}, - auth: { serverEndpoint: 'https://example.com/', credentials: { token: 't' } }, + auth: { serverEndpoint: 'https://example.com/', accessToken: 't' }, clientState: { anonymousUserID: '123' }, } satisfies PartialDeep as ResolvedConfiguration) @@ -108,7 +108,7 @@ describe('AuthProvider', () => { validateCredentialsMock.mockReturnValue(asyncValue(authedAuthStatusBob, 10)) resolvedConfig.next({ configuration: {}, - auth: { serverEndpoint: 'https://other.example.com/', credentials: { token: 't2' } }, + auth: { serverEndpoint: 'https://other.example.com/', accessToken: 't2' }, clientState: { anonymousUserID: '123' }, } satisfies PartialDeep as ResolvedConfiguration) await vi.advanceTimersByTimeAsync(1) @@ -156,7 +156,7 @@ describe('AuthProvider', () => { const { values, clearValues } = readValuesFrom(authStatus) resolvedConfig.next({ configuration: {}, - auth: { serverEndpoint: 'https://example.com/', credentials: { token: 't' } }, + auth: { serverEndpoint: 'https://example.com/', accessToken: 't' }, clientState: { anonymousUserID: '123' }, } satisfies PartialDeep as ResolvedConfiguration) @@ -176,7 +176,7 @@ describe('AuthProvider', () => { const promise = authProvider.validateAndStoreCredentials( { configuration: {}, - auth: { serverEndpoint: 'https://other.example.com/', credentials: { token: 't2' } }, + auth: { serverEndpoint: 'https://other.example.com/', accessToken: 't2' }, clientState: { anonymousUserID: '123' }, }, 'always-store' @@ -212,7 +212,7 @@ describe('AuthProvider', () => { const { values, clearValues } = readValuesFrom(authStatus) resolvedConfig.next({ configuration: {}, - auth: { serverEndpoint: 'https://example.com/', credentials: { token: 't' } }, + auth: { serverEndpoint: 'https://example.com/', accessToken: 't' }, clientState: { anonymousUserID: '123' }, } satisfies PartialDeep as ResolvedConfiguration) diff --git a/vscode/src/services/AuthProvider.ts b/vscode/src/services/AuthProvider.ts index 3b3731dffd39..0ea7e60d503c 100644 --- a/vscode/src/services/AuthProvider.ts +++ b/vscode/src/services/AuthProvider.ts @@ -15,6 +15,7 @@ import { distinctUntilChanged, clientCapabilities as getClientCapabilities, isAbortError, + normalizeServerEndpointURL, resolvedConfig as resolvedConfig_, setAuthStatusObservable as setAuthStatusObservable_, startWith, @@ -22,7 +23,6 @@ import { telemetryRecorder, withLatestFrom, } from '@sourcegraph/cody-shared' -import { normalizeServerEndpointURL } from '@sourcegraph/cody-shared/src/configuration/auth-resolver' import isEqual from 'lodash/isEqual' import { Observable, Subject } from 'observable-fns' import * as vscode from 'vscode' diff --git a/vscode/src/services/LocalStorageProvider.ts b/vscode/src/services/LocalStorageProvider.ts index 6b5f2689377e..f947a585540b 100644 --- a/vscode/src/services/LocalStorageProvider.ts +++ b/vscode/src/services/LocalStorageProvider.ts @@ -115,26 +115,26 @@ class LocalStorage implements LocalStorageForModelPreferences { * would give an inconsistent view of the state. */ public async saveEndpointAndToken( - auth: Pick + credentials: Pick ): Promise { - if (!auth.serverEndpoint) { + if (!credentials.serverEndpoint) { return } // Do not save an access token as the last-used endpoint, to prevent user mistakes. - if (isSourcegraphToken(auth.serverEndpoint)) { + if (isSourcegraphToken(credentials.serverEndpoint)) { return } - const serverEndpoint = new URL(auth.serverEndpoint).href + const serverEndpoint = new URL(credentials.serverEndpoint).href // Pass `false` to avoid firing the change event until we've stored all of the values. await this.set(this.LAST_USED_ENDPOINT, serverEndpoint, false) await this.addEndpointHistory(serverEndpoint, false) - if (auth.credentials && 'token' in auth.credentials) { + if (credentials.accessToken) { await secretStorage.storeToken( serverEndpoint, - auth.credentials.token, - auth.credentials.source + credentials.accessToken, + credentials.tokenSource ) } this.onChange.fire() diff --git a/vscode/src/services/UpstreamHealthProvider.ts b/vscode/src/services/UpstreamHealthProvider.ts index a5fb5be15675..8667dd2bc75c 100644 --- a/vscode/src/services/UpstreamHealthProvider.ts +++ b/vscode/src/services/UpstreamHealthProvider.ts @@ -1,6 +1,5 @@ import { type BrowserOrNodeResponse, - addAuthHeaders, addCodyClientIdentificationHeaders, addTraceparent, currentResolvedConfig, @@ -95,10 +94,11 @@ class UpstreamHealthProvider implements vscode.Disposable { addTraceparent(sharedHeaders) addCodyClientIdentificationHeaders(sharedHeaders) - const url = new URL('/healthz', auth.serverEndpoint) const upstreamHeaders = new Headers(sharedHeaders) - addAuthHeaders(auth, upstreamHeaders, url) - + if (auth.accessToken) { + upstreamHeaders.set('Authorization', `token ${auth.accessToken}`) + } + const url = new URL('/healthz', auth.serverEndpoint) const upstreamResult = await wrapInActiveSpan('upstream-latency.upstream', span => { span.setAttribute('sampled', true) return measureLatencyToUri(upstreamHeaders, url.toString()) diff --git a/vscode/src/services/open-telemetry/CodyTraceExport.ts b/vscode/src/services/open-telemetry/CodyTraceExport.ts index 57380f09f040..b0e0d482daab 100644 --- a/vscode/src/services/open-telemetry/CodyTraceExport.ts +++ b/vscode/src/services/open-telemetry/CodyTraceExport.ts @@ -1,7 +1,6 @@ import type { ExportResult } from '@opentelemetry/core' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http' import type { ReadableSpan } from '@opentelemetry/sdk-trace-base' -import { type AuthCredentials, addAuthHeaders } from '@sourcegraph/cody-shared' const MAX_TRACE_RETAIN_MS = 60 * 1000 @@ -11,16 +10,15 @@ export class CodyTraceExporter extends OTLPTraceExporter { constructor({ traceUrl, - auth, + accessToken, isTracingEnabled, - }: { traceUrl: string; auth: AuthCredentials | null; isTracingEnabled: boolean }) { - const headers = new Headers() - if (auth) addAuthHeaders(auth, headers, new URL(traceUrl)) - + }: { traceUrl: string; accessToken: string | null; isTracingEnabled: boolean }) { super({ url: traceUrl, httpAgentOptions: { rejectUnauthorized: false }, - headers: Object.fromEntries(headers.entries()), + headers: { + ...(accessToken ? { Authorization: `token ${accessToken}` } : {}), + }, }) this.isTracingEnabled = isTracingEnabled } diff --git a/vscode/src/services/open-telemetry/OpenTelemetryService.node.ts b/vscode/src/services/open-telemetry/OpenTelemetryService.node.ts index 53c5920a5756..49ba2d4ac170 100644 --- a/vscode/src/services/open-telemetry/OpenTelemetryService.node.ts +++ b/vscode/src/services/open-telemetry/OpenTelemetryService.node.ts @@ -79,7 +79,7 @@ export class OpenTelemetryService { new CodyTraceExporter({ traceUrl, isTracingEnabled: this.isTracingEnabled, - auth, + accessToken: auth.accessToken, }) ) ) diff --git a/vscode/src/services/open-telemetry/trace-sender.ts b/vscode/src/services/open-telemetry/trace-sender.ts index 0b7f0973a217..d7e7bb410d82 100644 --- a/vscode/src/services/open-telemetry/trace-sender.ts +++ b/vscode/src/services/open-telemetry/trace-sender.ts @@ -1,4 +1,5 @@ -import { addAuthHeaders, currentResolvedConfig, fetch } from '@sourcegraph/cody-shared' +import { currentResolvedConfig } from '@sourcegraph/cody-shared' +import fetch from 'node-fetch' import { logDebug, logError } from '../../output-channel-logger' /** @@ -21,19 +22,18 @@ export const TraceSender = { */ async function doSendTraceData(spanData: any): Promise { const { auth } = await currentResolvedConfig() - if (!auth.credentials) { + if (!auth.accessToken) { logError('TraceSender', 'Cannot send trace data: not authenticated') throw new Error('Not authenticated') } - const traceUrl = new URL('/-/debug/otlp/v1/traces', auth.serverEndpoint) - - const headers = new Headers({ 'Content-Type': 'application/json' }) - addAuthHeaders(auth, headers, traceUrl) - + const traceUrl = new URL('/-/debug/otlp/v1/traces', auth.serverEndpoint).toString() const response = await fetch(traceUrl, { method: 'POST', - headers: headers, + headers: { + 'Content-Type': 'application/json', + ...(auth.accessToken ? { Authorization: `token ${auth.accessToken}` } : {}), + }, body: spanData, }) diff --git a/vscode/src/testutils/mocks.ts b/vscode/src/testutils/mocks.ts index 30b72ebb5cdf..3a8f84f2fe8e 100644 --- a/vscode/src/testutils/mocks.ts +++ b/vscode/src/testutils/mocks.ts @@ -948,5 +948,4 @@ export const DEFAULT_VSCODE_SETTINGS = { experimentalGuardrailsTimeoutSeconds: undefined, overrideAuthToken: undefined, overrideServerEndpoint: undefined, - authExternalProviders: [], } satisfies ClientConfiguration diff --git a/vscode/webviews/AppWrapperForTest.tsx b/vscode/webviews/AppWrapperForTest.tsx index 05c5d730bb62..f9f6ab3cbac1 100644 --- a/vscode/webviews/AppWrapperForTest.tsx +++ b/vscode/webviews/AppWrapperForTest.tsx @@ -115,10 +115,7 @@ export const AppWrapperForTest: FunctionComponent<{ children: ReactNode }> = ({ detectIntent: () => Observable.of(), resolvedConfig: () => Observable.of({ - auth: { - credentials: { token: 'abc' }, - serverEndpoint: 'https://example.com', - }, + auth: { accessToken: 'abc', serverEndpoint: 'https://example.com' }, configuration: { autocomplete: true, devModels: [{ model: 'my-model', provider: 'my-provider' }],