mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 04:49:35 +00:00
GHES oauth flow support (#160679)
* GHES oauth flow support * update server number * delete testing line
This commit is contained in:
parent
41aacfa3fb
commit
fc1ef74eb0
|
@ -6,7 +6,7 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { Keychain } from './common/keychain';
|
import { Keychain } from './common/keychain';
|
||||||
import { GitHubEnterpriseServer, GitHubServer, IGitHubServer } from './githubServer';
|
import { GitHubServer, IGitHubServer } from './githubServer';
|
||||||
import { arrayEquals } from './common/utils';
|
import { arrayEquals } from './common/utils';
|
||||||
import { ExperimentationTelemetry } from './experimentationService';
|
import { ExperimentationTelemetry } from './experimentationService';
|
||||||
import TelemetryReporter from '@vscode/extension-telemetry';
|
import TelemetryReporter from '@vscode/extension-telemetry';
|
||||||
|
@ -43,15 +43,12 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
|
||||||
const { name, version, aiKey } = context.extension.packageJSON as { name: string; version: string; aiKey: string };
|
const { name, version, aiKey } = context.extension.packageJSON as { name: string; version: string; aiKey: string };
|
||||||
this._telemetryReporter = new ExperimentationTelemetry(context, new TelemetryReporter(name, version, aiKey));
|
this._telemetryReporter = new ExperimentationTelemetry(context, new TelemetryReporter(name, version, aiKey));
|
||||||
|
|
||||||
if (this.type === AuthProviderType.github) {
|
this._githubServer = new GitHubServer(
|
||||||
this._githubServer = new GitHubServer(
|
this.type,
|
||||||
// We only can use the Device Code flow when we have a full node environment because of CORS.
|
// 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,
|
context.extension.extensionKind === vscode.ExtensionKind.Workspace || vscode.env.uiKind === vscode.UIKind.Desktop,
|
||||||
this._logger,
|
this._logger,
|
||||||
this._telemetryReporter);
|
this._telemetryReporter);
|
||||||
} else {
|
|
||||||
this._githubServer = new GitHubEnterpriseServer(this._logger, this._telemetryReporter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains the current state of the sessions we have available.
|
// Contains the current state of the sessions we have available.
|
||||||
this._sessionsPromise = this.readSessions().then((sessions) => {
|
this._sessionsPromise = this.readSessions().then((sessions) => {
|
||||||
|
|
|
@ -17,8 +17,6 @@ import path = require('path');
|
||||||
|
|
||||||
const localize = nls.loadMessageBundle();
|
const localize = nls.loadMessageBundle();
|
||||||
const CLIENT_ID = '01ab8ac9400c4e429b23';
|
const CLIENT_ID = '01ab8ac9400c4e429b23';
|
||||||
const GITHUB_AUTHORIZE_URL = 'https://github.com/login/oauth/authorize';
|
|
||||||
// TODO: change to stable when that happens
|
|
||||||
const GITHUB_TOKEN_URL = 'https://vscode.dev/codeExchangeProxyEndpoints/github/login/oauth/access_token';
|
const GITHUB_TOKEN_URL = 'https://vscode.dev/codeExchangeProxyEndpoints/github/login/oauth/access_token';
|
||||||
const NETWORK_ERROR = 'network error';
|
const NETWORK_ERROR = 'network error';
|
||||||
|
|
||||||
|
@ -74,71 +72,77 @@ async function getScopes(token: string, serverUri: vscode.Uri, logger: Log): Pro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserInfo(token: string, serverUri: vscode.Uri, logger: Log): Promise<{ id: string; accountName: string }> {
|
|
||||||
let result: Response;
|
|
||||||
try {
|
|
||||||
logger.info('Getting user info...');
|
|
||||||
result = await fetch(serverUri.toString(), {
|
|
||||||
headers: {
|
|
||||||
Authorization: `token ${token}`,
|
|
||||||
'User-Agent': 'Visual-Studio-Code'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (ex) {
|
|
||||||
logger.error(ex.message);
|
|
||||||
throw new Error(NETWORK_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.ok) {
|
|
||||||
try {
|
|
||||||
const json = await result.json();
|
|
||||||
logger.info('Got account info!');
|
|
||||||
return { id: json.id, accountName: json.login };
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(`Unexpected error parsing response from GitHub: ${e.message ?? e}`);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// either display the response message or the http status text
|
|
||||||
let errorMessage = result.statusText;
|
|
||||||
try {
|
|
||||||
const json = await result.json();
|
|
||||||
if (json.message) {
|
|
||||||
errorMessage = json.message;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
logger.error(`Getting account info failed: ${errorMessage}`);
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GitHubServer implements IGitHubServer {
|
export class GitHubServer implements IGitHubServer {
|
||||||
friendlyName = 'GitHub';
|
readonly friendlyName: string;
|
||||||
type = AuthProviderType.github;
|
|
||||||
|
|
||||||
private _pendingNonces = new Map<string, string[]>();
|
private _pendingNonces = new Map<string, string[]>();
|
||||||
private _codeExchangePromises = new Map<string, { promise: Promise<string>; cancel: vscode.EventEmitter<void> }>();
|
private _codeExchangePromises = new Map<string, { promise: Promise<string>; cancel: vscode.EventEmitter<void> }>();
|
||||||
private _disposable: vscode.Disposable;
|
private _disposable: vscode.Disposable | undefined;
|
||||||
private _uriHandler = new UriEventHandler(this._logger);
|
private static _uriHandler: UriEventHandler | undefined;
|
||||||
private readonly getRedirectEndpoint: Thenable<string>;
|
private _redirectEndpoint: string | undefined;
|
||||||
|
|
||||||
constructor(private readonly _supportDeviceCodeFlow: boolean, private readonly _logger: Log, private readonly _telemetryReporter: ExperimentationTelemetry) {
|
constructor(
|
||||||
this._disposable = vscode.window.registerUriHandler(this._uriHandler);
|
public readonly type: AuthProviderType,
|
||||||
|
private readonly _supportDeviceCodeFlow: boolean,
|
||||||
|
private readonly _logger: Log,
|
||||||
|
private readonly _telemetryReporter: ExperimentationTelemetry
|
||||||
|
) {
|
||||||
|
this.friendlyName = type === AuthProviderType.github ? 'GitHub' : 'GitHub Enterprise';
|
||||||
|
|
||||||
this.getRedirectEndpoint = vscode.commands.executeCommand<{ [providerId: string]: string } | undefined>('workbench.getCodeExchangeProxyEndpoints').then((proxyEndpoints) => {
|
if (!GitHubServer._uriHandler) {
|
||||||
|
GitHubServer._uriHandler = new UriEventHandler(this._logger);
|
||||||
|
this._disposable = vscode.window.registerUriHandler(GitHubServer._uriHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get baseUri() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getRedirectEndpoint(): Promise<string> {
|
||||||
|
if (this._redirectEndpoint) {
|
||||||
|
return this._redirectEndpoint;
|
||||||
|
}
|
||||||
|
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.
|
// If we are running in insiders vscode.dev, then ensure we use the redirect route on that.
|
||||||
let redirectUri = REDIRECT_URL_STABLE;
|
this._redirectEndpoint = REDIRECT_URL_STABLE;
|
||||||
if (proxyEndpoints?.github && new URL(proxyEndpoints.github).hostname === 'insiders.vscode.dev') {
|
if (proxyEndpoints?.github && new URL(proxyEndpoints.github).hostname === 'insiders.vscode.dev') {
|
||||||
redirectUri = REDIRECT_URL_INSIDERS;
|
this._redirectEndpoint = REDIRECT_URL_INSIDERS;
|
||||||
}
|
}
|
||||||
return redirectUri;
|
return this._redirectEndpoint;
|
||||||
});
|
} else {
|
||||||
|
// GHES
|
||||||
|
const result = await fetch(this.getServerUri('/meta').toString(true));
|
||||||
|
if (result.ok) {
|
||||||
|
try {
|
||||||
|
const json: { installed_version: string } = await result.json();
|
||||||
|
const [majorStr, minorStr, _patch] = json.installed_version.split('.');
|
||||||
|
const major = Number(majorStr);
|
||||||
|
const minor = Number(minorStr);
|
||||||
|
if (major >= 4 || major === 3 && minor >= 8
|
||||||
|
) {
|
||||||
|
// GHES 3.8 and above used vscode.dev/redirect as the route.
|
||||||
|
// It only supports a single redirect endpoint, so we can't use
|
||||||
|
// insiders.vscode.dev/redirect when we're running in Insiders, unfortunately.
|
||||||
|
this._redirectEndpoint = 'https://vscode.dev/redirect';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this._logger.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO in like 1 year change the default vscode.dev/redirect maybe
|
||||||
|
this._redirectEndpoint = 'https://vscode-auth.github.com/';
|
||||||
|
}
|
||||||
|
return this._redirectEndpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this._disposable.dispose();
|
this._disposable?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO@joaomoreno TODO@TylerLeonhardt
|
// TODO@joaomoreno TODO@TylerLeonhardt
|
||||||
|
@ -217,26 +221,30 @@ export class GitHubServer implements IGitHubServer {
|
||||||
this._logger.info(`Trying without local server... (${scopes})`);
|
this._logger.info(`Trying without local server... (${scopes})`);
|
||||||
return await vscode.window.withProgress<string>({
|
return await vscode.window.withProgress<string>({
|
||||||
location: vscode.ProgressLocation.Notification,
|
location: vscode.ProgressLocation.Notification,
|
||||||
title: localize('signingIn', "Signing in to github.com..."),
|
title: localize('signingIn', 'Signing in to {0}...', this.baseUri.authority),
|
||||||
cancellable: true
|
cancellable: true
|
||||||
}, async (_, token) => {
|
}, async (_, token) => {
|
||||||
const existingNonces = this._pendingNonces.get(scopes) || [];
|
const existingNonces = this._pendingNonces.get(scopes) || [];
|
||||||
this._pendingNonces.set(scopes, [...existingNonces, nonce]);
|
this._pendingNonces.set(scopes, [...existingNonces, nonce]);
|
||||||
const redirectUri = await this.getRedirectEndpoint;
|
const redirectUri = await this.getRedirectEndpoint();
|
||||||
const searchParams = new URLSearchParams([
|
const searchParams = new URLSearchParams([
|
||||||
['client_id', CLIENT_ID],
|
['client_id', CLIENT_ID],
|
||||||
['redirect_uri', redirectUri],
|
['redirect_uri', redirectUri],
|
||||||
['scope', scopes],
|
['scope', scopes],
|
||||||
['state', encodeURIComponent(callbackUri.toString(true))]
|
['state', encodeURIComponent(callbackUri.toString(true))]
|
||||||
]);
|
]);
|
||||||
const uri = vscode.Uri.parse(`${GITHUB_AUTHORIZE_URL}?${searchParams.toString()}`);
|
|
||||||
|
const uri = vscode.Uri.parse(this.baseUri.with({
|
||||||
|
path: '/login/oauth/authorize',
|
||||||
|
query: searchParams.toString()
|
||||||
|
}).toString(true));
|
||||||
await vscode.env.openExternal(uri);
|
await vscode.env.openExternal(uri);
|
||||||
|
|
||||||
// Register a single listener for the URI callback, in case the user starts the login process multiple times
|
// Register a single listener for the URI callback, in case the user starts the login process multiple times
|
||||||
// before completing it.
|
// before completing it.
|
||||||
let codeExchangePromise = this._codeExchangePromises.get(scopes);
|
let codeExchangePromise = this._codeExchangePromises.get(scopes);
|
||||||
if (!codeExchangePromise) {
|
if (!codeExchangePromise) {
|
||||||
codeExchangePromise = promiseFromEvent(this._uriHandler.event, this.handleUri(scopes));
|
codeExchangePromise = promiseFromEvent(GitHubServer._uriHandler!.event, this.handleUri(scopes));
|
||||||
this._codeExchangePromises.set(scopes, codeExchangePromise);
|
this._codeExchangePromises.set(scopes, codeExchangePromise);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,17 +266,21 @@ export class GitHubServer implements IGitHubServer {
|
||||||
this._logger.info(`Trying with local server... (${scopes})`);
|
this._logger.info(`Trying with local server... (${scopes})`);
|
||||||
return await vscode.window.withProgress<string>({
|
return await vscode.window.withProgress<string>({
|
||||||
location: vscode.ProgressLocation.Notification,
|
location: vscode.ProgressLocation.Notification,
|
||||||
title: localize('signingInAnotherWay', "Signing in to github.com..."),
|
title: localize('signingInAnotherWay', "Signing in to {0}...", this.baseUri.authority),
|
||||||
cancellable: true
|
cancellable: true
|
||||||
}, async (_, token) => {
|
}, async (_, token) => {
|
||||||
const redirectUri = await this.getRedirectEndpoint;
|
const redirectUri = await this.getRedirectEndpoint();
|
||||||
const searchParams = new URLSearchParams([
|
const searchParams = new URLSearchParams([
|
||||||
['client_id', CLIENT_ID],
|
['client_id', CLIENT_ID],
|
||||||
['redirect_uri', redirectUri],
|
['redirect_uri', redirectUri],
|
||||||
['scope', scopes],
|
['scope', scopes],
|
||||||
]);
|
]);
|
||||||
const loginUrl = `${GITHUB_AUTHORIZE_URL}?${searchParams.toString()}`;
|
|
||||||
const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl);
|
const loginUrl = this.baseUri.with({
|
||||||
|
path: '/login/oauth/authorize',
|
||||||
|
query: searchParams.toString()
|
||||||
|
});
|
||||||
|
const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl.toString(true));
|
||||||
const port = await server.start();
|
const port = await server.start();
|
||||||
|
|
||||||
let codeToExchange;
|
let codeToExchange;
|
||||||
|
@ -295,8 +307,11 @@ export class GitHubServer implements IGitHubServer {
|
||||||
this._logger.info(`Trying device code flow... (${scopes})`);
|
this._logger.info(`Trying device code flow... (${scopes})`);
|
||||||
|
|
||||||
// Get initial device code
|
// Get initial device code
|
||||||
const uri = `https://github.com/login/device/code?client_id=${CLIENT_ID}&scope=${scopes}`;
|
const uri = this.baseUri.with({
|
||||||
const result = await fetch(uri, {
|
path: '/login/device/code',
|
||||||
|
query: `client_id=${CLIENT_ID}&scope=${scopes}`
|
||||||
|
});
|
||||||
|
const result = await fetch(uri.toString(true), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json'
|
Accept: 'application/json'
|
||||||
|
@ -363,7 +378,10 @@ export class GitHubServer implements IGitHubServer {
|
||||||
json.verification_uri,
|
json.verification_uri,
|
||||||
json.user_code)
|
json.user_code)
|
||||||
}, async (_, token) => {
|
}, async (_, token) => {
|
||||||
const refreshTokenUri = `https://github.com/login/oauth/access_token?client_id=${CLIENT_ID}&device_code=${json.device_code}&grant_type=urn:ietf:params:oauth:grant-type:device_code`;
|
const refreshTokenUri = this.baseUri.with({
|
||||||
|
path: '/login/oauth/access_token',
|
||||||
|
query: `client_id=${CLIENT_ID}&device_code=${json.device_code}&grant_type=urn:ietf:params:oauth:grant-type:device_code`
|
||||||
|
});
|
||||||
|
|
||||||
// Try for 2 minutes
|
// Try for 2 minutes
|
||||||
const attempts = 120 / json.interval;
|
const attempts = 120 / json.interval;
|
||||||
|
@ -374,7 +392,7 @@ export class GitHubServer implements IGitHubServer {
|
||||||
}
|
}
|
||||||
let accessTokenResult;
|
let accessTokenResult;
|
||||||
try {
|
try {
|
||||||
accessTokenResult = await fetch(refreshTokenUri, {
|
accessTokenResult = await fetch(refreshTokenUri.toString(true), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json'
|
Accept: 'application/json'
|
||||||
|
@ -439,7 +457,11 @@ export class GitHubServer implements IGitHubServer {
|
||||||
const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
|
const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
|
||||||
const endpointUrl = proxyEndpoints?.github ? `${proxyEndpoints.github}login/oauth/access_token` : GITHUB_TOKEN_URL;
|
const endpointUrl = proxyEndpoints?.github ? `${proxyEndpoints.github}login/oauth/access_token` : GITHUB_TOKEN_URL;
|
||||||
|
|
||||||
const body = `code=${code}`;
|
const body = new URLSearchParams([['code', code]]);
|
||||||
|
if (this.type === AuthProviderType.githubEnterprise) {
|
||||||
|
body.append('github_enterprise', this.baseUri.toString(true));
|
||||||
|
body.append('redirect_uri', await this.getRedirectEndpoint());
|
||||||
|
}
|
||||||
const result = await fetch(endpointUrl, {
|
const result = await fetch(endpointUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -448,7 +470,7 @@ export class GitHubServer implements IGitHubServer {
|
||||||
'Content-Length': body.toString()
|
'Content-Length': body.toString()
|
||||||
|
|
||||||
},
|
},
|
||||||
body
|
body: body.toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
|
@ -464,12 +486,52 @@ export class GitHubServer implements IGitHubServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getServerUri(path: string = '') {
|
private getServerUri(path: string = '') {
|
||||||
const apiUri = vscode.Uri.parse('https://api.github.com');
|
if (this.type === AuthProviderType.github) {
|
||||||
return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}${path}`);
|
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);
|
||||||
|
return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}/api/v3${path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUserInfo(token: string): Promise<{ id: string; accountName: string }> {
|
public async getUserInfo(token: string): Promise<{ id: string; accountName: string }> {
|
||||||
return getUserInfo(token, this.getServerUri('/user'), this._logger);
|
let result: Response;
|
||||||
|
try {
|
||||||
|
this._logger.info('Getting user info...');
|
||||||
|
result = await fetch(this.getServerUri('/user').toString(), {
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${token}`,
|
||||||
|
'User-Agent': 'Visual-Studio-Code'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (ex) {
|
||||||
|
this._logger.error(ex.message);
|
||||||
|
throw new Error(NETWORK_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
try {
|
||||||
|
const json = await result.json();
|
||||||
|
this._logger.info('Got account info!');
|
||||||
|
return { id: json.id, accountName: json.login };
|
||||||
|
} catch (e) {
|
||||||
|
this._logger.error(`Unexpected error parsing response from GitHub: ${e.message ?? e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// either display the response message or the http status text
|
||||||
|
let errorMessage = result.statusText;
|
||||||
|
try {
|
||||||
|
const json = await result.json();
|
||||||
|
if (json.message) {
|
||||||
|
errorMessage = json.message;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
this._logger.error(`Getting account info failed: ${errorMessage}`);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendAdditionalTelemetryInfo(token: string): Promise<void> {
|
public async sendAdditionalTelemetryInfo(token: string): Promise<void> {
|
||||||
|
@ -482,6 +544,15 @@ export class GitHubServer implements IGitHubServer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.type === AuthProviderType.github) {
|
||||||
|
return await this.checkEduDetails(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GHES
|
||||||
|
await this.checkEnterpriseVersion(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkEduDetails(token: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const result = await fetch('https://education.github.com/api/user', {
|
const result = await fetch('https://education.github.com/api/user', {
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -513,7 +584,7 @@ export class GitHubServer implements IGitHubServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkEnterpriseVersion(token: string): Promise<void> {
|
private async checkEnterpriseVersion(token: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const result = await fetch(this.getServerUri('/meta').toString(), {
|
const result = await fetch(this.getServerUri('/meta').toString(), {
|
||||||
|
@ -543,75 +614,3 @@ export class GitHubServer implements IGitHubServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GitHubEnterpriseServer implements IGitHubServer {
|
|
||||||
friendlyName = 'GitHub Enterprise';
|
|
||||||
type = AuthProviderType.githubEnterprise;
|
|
||||||
|
|
||||||
constructor(private readonly _logger: Log, private readonly telemetryReporter: ExperimentationTelemetry) { }
|
|
||||||
|
|
||||||
dispose() { }
|
|
||||||
|
|
||||||
public async login(scopes: string): Promise<string> {
|
|
||||||
this._logger.info(`Logging in for the following scopes: ${scopes}`);
|
|
||||||
|
|
||||||
const token = await vscode.window.showInputBox({ prompt: 'GitHub Personal Access Token', ignoreFocusOut: true });
|
|
||||||
if (!token) { throw new Error('Sign in failed: No token provided'); }
|
|
||||||
|
|
||||||
const tokenScopes = await getScopes(token, this.getServerUri('/'), this._logger); // Example: ['repo', 'user']
|
|
||||||
const scopesList = scopes.split(' '); // Example: 'read:user repo user:email'
|
|
||||||
if (!scopesList.every(scope => {
|
|
||||||
const included = tokenScopes.includes(scope);
|
|
||||||
if (included || !scope.includes(':')) {
|
|
||||||
return included;
|
|
||||||
}
|
|
||||||
|
|
||||||
return scope.split(':').some(splitScopes => {
|
|
||||||
return tokenScopes.includes(splitScopes);
|
|
||||||
});
|
|
||||||
})) {
|
|
||||||
throw new Error(`The provided token does not match the requested scopes: ${scopes}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getServerUri(path: string = '') {
|
|
||||||
const apiUri = vscode.Uri.parse(vscode.workspace.getConfiguration('github-enterprise').get<string>('uri') || '', true);
|
|
||||||
return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}/api/v3${path}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getUserInfo(token: string): Promise<{ id: string; accountName: string }> {
|
|
||||||
return getUserInfo(token, this.getServerUri('/user'), this._logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async sendAdditionalTelemetryInfo(token: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const result = await fetch(this.getServerUri('/meta').toString(), {
|
|
||||||
headers: {
|
|
||||||
Authorization: `token ${token}`,
|
|
||||||
'User-Agent': 'Visual-Studio-Code'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.ok) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const json: { verifiable_password_authentication: boolean; installed_version: string } = await result.json();
|
|
||||||
|
|
||||||
/* __GDPR__
|
|
||||||
"ghe-session" : {
|
|
||||||
"owner": "TylerLeonhardt",
|
|
||||||
"version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
this.telemetryReporter.sendTelemetryEvent('ghe-session', {
|
|
||||||
version: json.installed_version
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue