mirror of
https://github.com/Microsoft/vscode
synced 2024-10-12 06:17:18 +00:00
GitHub Enterprise Auth improvements (#165082)
1. Namespace secrets based on the value of github-enterprise.uri to support "multiple separate GHES instances" 2. If the setting value disappears, continue using last set value. Fixes https://github.com/microsoft/vscode-pull-request-github/issues/3992 3. Mark github-enterprise.uri as requires trust 3. Refactoring like: * UriHandler is handled in extension.ts and passed down everywhere since we can only have 1 instance of it * misc style (`private` usage, better `disposable` handling)
This commit is contained in:
parent
c60980ca7a
commit
9b3e147d6d
|
@ -24,7 +24,10 @@
|
|||
"capabilities": {
|
||||
"virtualWorkspaces": true,
|
||||
"untrustedWorkspaces": {
|
||||
"supported": true
|
||||
"supported": "limited",
|
||||
"restrictedConfigurations": [
|
||||
"github-enterprise.uri"
|
||||
]
|
||||
}
|
||||
},
|
||||
"contributes": {
|
||||
|
|
|
@ -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<string>('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<string>('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<string>('github-enterprise.uri')) {
|
||||
githubEnterpriseAuthProvider = new GitHubAuthenticationProvider(context, AuthProviderType.githubEnterprise);
|
||||
context.subscriptions.push(githubEnterpriseAuthProvider);
|
||||
if (vscode.workspace.getConfiguration().get<string>('github-enterprise.uri')) {
|
||||
githubEnterpriseAuthProvider?.dispose();
|
||||
githubEnterpriseAuthProvider = initGHES(context, uriHandler);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
|
|
@ -28,27 +28,49 @@ export enum AuthProviderType {
|
|||
githubEnterprise = 'github-enterprise'
|
||||
}
|
||||
|
||||
export class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
|
||||
public handleUri(uri: vscode.Uri) {
|
||||
this.fire(uri);
|
||||
}
|
||||
}
|
||||
|
||||
export class GitHubAuthenticationProvider implements vscode.AuthenticationProvider, vscode.Disposable {
|
||||
private _sessionChangeEmitter = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
|
||||
private _logger = new Log(this.type);
|
||||
private _githubServer: IGitHubServer;
|
||||
private _telemetryReporter: ExperimentationTelemetry;
|
||||
private readonly _sessionChangeEmitter = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
|
||||
private readonly _logger: Log;
|
||||
private readonly _githubServer: IGitHubServer;
|
||||
private readonly _telemetryReporter: ExperimentationTelemetry;
|
||||
private readonly _keychain: Keychain;
|
||||
private readonly _accountsSeen = new Set<string>();
|
||||
private readonly _disposable: vscode.Disposable | undefined;
|
||||
|
||||
private _keychain: Keychain = new Keychain(this.context, `${this.type}.auth`, this._logger);
|
||||
private _sessionsPromise: Promise<vscode.AuthenticationSession[]>;
|
||||
private _accountsSeen = new Set<string>();
|
||||
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() {
|
||||
|
|
|
@ -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<vscode.Uri> 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<string>;
|
||||
getUserInfo(token: string): Promise<{ id: string; accountName: string }>;
|
||||
sendAdditionalTelemetryInfo(token: string): Promise<void>;
|
||||
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<string, string[]>();
|
||||
private _codeExchangePromises = new Map<string, { promise: Promise<string>; cancel: vscode.EventEmitter<void> }>();
|
||||
private _disposable: vscode.Disposable | undefined;
|
||||
private static _uriHandler: UriEventHandler | undefined;
|
||||
private readonly _pendingNonces = new Map<string, string[]>();
|
||||
private readonly _codeExchangePromises = new Map<string, { promise: Promise<string>; cancel: vscode.EventEmitter<void> }>();
|
||||
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<string>('uri') || '', true);
|
||||
return this._ghesUri!;
|
||||
}
|
||||
|
||||
private async getRedirectEndpoint(): Promise<string> {
|
||||
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<boolean> {
|
||||
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<string>('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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue