diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index dbabf7fcbbf..29bd7d29565 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -24,7 +24,10 @@ "capabilities": { "virtualWorkspaces": true, "untrustedWorkspaces": { - "supported": true + "supported": "limited", + "restrictedConfigurations": [ + "github-enterprise.uri" + ] } }, "contributes": { diff --git a/extensions/github-authentication/src/extension.ts b/extensions/github-authentication/src/extension.ts index a9749b17995..62f69f85b56 100644 --- a/extensions/github-authentication/src/extension.ts +++ b/extensions/github-authentication/src/extension.ts @@ -4,22 +4,42 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { GitHubAuthenticationProvider, AuthProviderType } from './github'; +import { GitHubAuthenticationProvider, UriEventHandler } from './github'; + +function initGHES(context: vscode.ExtensionContext, uriHandler: UriEventHandler) { + const settingValue = vscode.workspace.getConfiguration().get('github-enterprise.uri'); + if (!settingValue) { + return undefined; + } + + // validate user value + let uri: vscode.Uri; + try { + uri = vscode.Uri.parse(settingValue, true); + } catch (e) { + vscode.window.showErrorMessage(vscode.l10n.t('GitHub Enterprise Server URI is not a valid URI: {0}', e.message ?? e)); + return; + } + + const githubEnterpriseAuthProvider = new GitHubAuthenticationProvider(context, uriHandler, uri); + context.subscriptions.push(githubEnterpriseAuthProvider); + return githubEnterpriseAuthProvider; +} export function activate(context: vscode.ExtensionContext) { - context.subscriptions.push(new GitHubAuthenticationProvider(context, AuthProviderType.github)); + const uriHandler = new UriEventHandler(); + context.subscriptions.push(uriHandler); + context.subscriptions.push(vscode.window.registerUriHandler(uriHandler)); - let githubEnterpriseAuthProvider: GitHubAuthenticationProvider | undefined; - if (vscode.workspace.getConfiguration().get('github-enterprise.uri')) { - githubEnterpriseAuthProvider = new GitHubAuthenticationProvider(context, AuthProviderType.githubEnterprise); - context.subscriptions.push(githubEnterpriseAuthProvider); - } + context.subscriptions.push(new GitHubAuthenticationProvider(context, uriHandler)); + + let githubEnterpriseAuthProvider: GitHubAuthenticationProvider | undefined = initGHES(context, uriHandler); context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async e => { if (e.affectsConfiguration('github-enterprise.uri')) { - if (!githubEnterpriseAuthProvider && vscode.workspace.getConfiguration().get('github-enterprise.uri')) { - githubEnterpriseAuthProvider = new GitHubAuthenticationProvider(context, AuthProviderType.githubEnterprise); - context.subscriptions.push(githubEnterpriseAuthProvider); + if (vscode.workspace.getConfiguration().get('github-enterprise.uri')) { + githubEnterpriseAuthProvider?.dispose(); + githubEnterpriseAuthProvider = initGHES(context, uriHandler); } } })); diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 0c7273d8bf0..28167ffc0cc 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -28,27 +28,49 @@ export enum AuthProviderType { githubEnterprise = 'github-enterprise' } +export class UriEventHandler extends vscode.EventEmitter implements vscode.UriHandler { + public handleUri(uri: vscode.Uri) { + this.fire(uri); + } +} + export class GitHubAuthenticationProvider implements vscode.AuthenticationProvider, vscode.Disposable { - private _sessionChangeEmitter = new vscode.EventEmitter(); - private _logger = new Log(this.type); - private _githubServer: IGitHubServer; - private _telemetryReporter: ExperimentationTelemetry; + private readonly _sessionChangeEmitter = new vscode.EventEmitter(); + private readonly _logger: Log; + private readonly _githubServer: IGitHubServer; + private readonly _telemetryReporter: ExperimentationTelemetry; + private readonly _keychain: Keychain; + private readonly _accountsSeen = new Set(); + private readonly _disposable: vscode.Disposable | undefined; - private _keychain: Keychain = new Keychain(this.context, `${this.type}.auth`, this._logger); private _sessionsPromise: Promise; - private _accountsSeen = new Set(); - private _disposable: vscode.Disposable; - constructor(private readonly context: vscode.ExtensionContext, private readonly type: AuthProviderType) { + constructor( + private readonly context: vscode.ExtensionContext, + uriHandler: UriEventHandler, + ghesUri?: vscode.Uri + ) { const { name, version, aiKey } = context.extension.packageJSON as { name: string; version: string; aiKey: string }; this._telemetryReporter = new ExperimentationTelemetry(context, new TelemetryReporter(name, version, aiKey)); + const type = ghesUri ? AuthProviderType.githubEnterprise : AuthProviderType.github; + + this._logger = new Log(type); + + this._keychain = new Keychain( + this.context, + type === AuthProviderType.github + ? `${type}.auth` + : `${ghesUri?.authority}${ghesUri?.path}.ghes.auth`, + this._logger); + this._githubServer = new GitHubServer( - this.type, + this._logger, + this._telemetryReporter, + uriHandler, // We only can use the Device Code flow when we have a full node environment because of CORS. context.extension.extensionKind === vscode.ExtensionKind.Workspace || vscode.env.uiKind === vscode.UIKind.Desktop, - this._logger, - this._telemetryReporter); + ghesUri); // Contains the current state of the sessions we have available. this._sessionsPromise = this.readSessions().then((sessions) => { @@ -59,14 +81,13 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid this._disposable = vscode.Disposable.from( this._telemetryReporter, - this._githubServer, vscode.authentication.registerAuthenticationProvider(type, this._githubServer.friendlyName, this, { supportsMultipleAccounts: false }), this.context.secrets.onDidChange(() => this.checkForUpdates()) ); } dispose() { - this._disposable.dispose(); + this._disposable?.dispose(); } get onDidChangeSessions() { diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index 830e6983700..8ada06e784c 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -8,7 +8,7 @@ import fetch, { Response } from 'node-fetch'; import { v4 as uuid } from 'uuid'; import { PromiseAdapter, promiseFromEvent } from './common/utils'; import { ExperimentationTelemetry } from './experimentationService'; -import { AuthProviderType } from './github'; +import { AuthProviderType, UriEventHandler } from './github'; import { Log } from './common/logger'; import { isSupportedEnvironment } from './common/env'; import { LoopbackAuthServer } from './authServer'; @@ -21,23 +21,11 @@ const NETWORK_ERROR = 'network error'; const REDIRECT_URL_STABLE = 'https://vscode.dev/redirect'; const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect'; -class UriEventHandler extends vscode.EventEmitter implements vscode.UriHandler { - constructor(private readonly Logger: Log) { - super(); - } - - public handleUri(uri: vscode.Uri) { - this.Logger.trace('Handling Uri...'); - this.fire(uri); - } -} - -export interface IGitHubServer extends vscode.Disposable { +export interface IGitHubServer { login(scopes: string): Promise; getUserInfo(token: string): Promise<{ id: string; accountName: string }>; sendAdditionalTelemetryInfo(token: string): Promise; friendlyName: string; - type: AuthProviderType; } interface IGitHubDeviceCodeResponse { @@ -73,38 +61,35 @@ async function getScopes(token: string, serverUri: vscode.Uri, logger: Log): Pro export class GitHubServer implements IGitHubServer { readonly friendlyName: string; - private _pendingNonces = new Map(); - private _codeExchangePromises = new Map; cancel: vscode.EventEmitter }>(); - private _disposable: vscode.Disposable | undefined; - private static _uriHandler: UriEventHandler | undefined; + private readonly _pendingNonces = new Map(); + private readonly _codeExchangePromises = new Map; cancel: vscode.EventEmitter }>(); + private readonly _type: AuthProviderType; + private _redirectEndpoint: string | undefined; constructor( - public readonly type: AuthProviderType, - private readonly _supportDeviceCodeFlow: boolean, private readonly _logger: Log, - private readonly _telemetryReporter: ExperimentationTelemetry + private readonly _telemetryReporter: ExperimentationTelemetry, + private readonly _uriHandler: UriEventHandler, + private readonly _supportDeviceCodeFlow: boolean, + private readonly _ghesUri?: vscode.Uri ) { - this.friendlyName = type === AuthProviderType.github ? 'GitHub' : 'GitHub Enterprise'; - - if (!GitHubServer._uriHandler) { - GitHubServer._uriHandler = new UriEventHandler(this._logger); - this._disposable = vscode.window.registerUriHandler(GitHubServer._uriHandler); - } + this._type = _ghesUri ? AuthProviderType.githubEnterprise : AuthProviderType.github; + this.friendlyName = this._type === AuthProviderType.github ? 'GitHub' : _ghesUri?.authority!; } get baseUri() { - if (this.type === AuthProviderType.github) { + if (this._type === AuthProviderType.github) { return vscode.Uri.parse('https://github.com/'); } - return vscode.Uri.parse(vscode.workspace.getConfiguration('github-enterprise').get('uri') || '', true); + return this._ghesUri!; } private async getRedirectEndpoint(): Promise { if (this._redirectEndpoint) { return this._redirectEndpoint; } - if (this.type === AuthProviderType.github) { + if (this._type === AuthProviderType.github) { const proxyEndpoints = await vscode.commands.executeCommand<{ [providerId: string]: string } | undefined>('workbench.getCodeExchangeProxyEndpoints'); // If we are running in insiders vscode.dev, then ensure we use the redirect route on that. this._redirectEndpoint = REDIRECT_URL_STABLE; @@ -139,10 +124,6 @@ export class GitHubServer implements IGitHubServer { return this._redirectEndpoint; } - dispose() { - this._disposable?.dispose(); - } - // TODO@joaomoreno TODO@TylerLeonhardt private async isNoCorsEnvironment(): Promise { const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/dummy`)); @@ -246,7 +227,7 @@ export class GitHubServer implements IGitHubServer { // before completing it. let codeExchangePromise = this._codeExchangePromises.get(scopes); if (!codeExchangePromise) { - codeExchangePromise = promiseFromEvent(GitHubServer._uriHandler!.event, this.handleUri(scopes)); + codeExchangePromise = promiseFromEvent(this._uriHandler!.event, this.handleUri(scopes)); this._codeExchangePromises.set(scopes, codeExchangePromise); } @@ -467,7 +448,7 @@ export class GitHubServer implements IGitHubServer { const endpointUrl = proxyEndpoints?.github ? `${proxyEndpoints.github}login/oauth/access_token` : GITHUB_TOKEN_URL; const body = new URLSearchParams([['code', code]]); - if (this.type === AuthProviderType.githubEnterprise) { + if (this._type === AuthProviderType.githubEnterprise) { body.append('github_enterprise', this.baseUri.toString(true)); body.append('redirect_uri', await this.getRedirectEndpoint()); } @@ -495,11 +476,11 @@ export class GitHubServer implements IGitHubServer { } private getServerUri(path: string = '') { - if (this.type === AuthProviderType.github) { + if (this._type === AuthProviderType.github) { return vscode.Uri.parse('https://api.github.com').with({ path }); } // GHES - const apiUri = vscode.Uri.parse(vscode.workspace.getConfiguration('github-enterprise').get('uri') || '', true); + const apiUri = this.baseUri; return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}/api/v3${path}`); } @@ -553,7 +534,7 @@ export class GitHubServer implements IGitHubServer { return; } - if (this.type === AuthProviderType.github) { + if (this._type === AuthProviderType.github) { return await this.checkEduDetails(token); }