From 0d433893b7be7a201037caba1c0b08c184f6f897 Mon Sep 17 00:00:00 2001 From: Baptiste Augrain Date: Mon, 6 Sep 2021 15:22:30 +0200 Subject: [PATCH] use github personal access token (#672) --- DOCS.md | 11 ++ patches/use-github-pat.patch | 257 +++++++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 patches/use-github-pat.patch diff --git a/DOCS.md b/DOCS.md index a5acb1d..1ccaf39 100644 --- a/DOCS.md +++ b/DOCS.md @@ -9,6 +9,7 @@ - [Proprietary Debugging Tools](#proprietary-debugging-tools) - [Proprietary Extensions](#proprietary-extensions) - [Migrating from Visual Studio Code to VSCodium](#migrating) +- [Sign in with GitHub](#signin-github) - [How do I run VSCodium in portable mode?](#portable) - [How do I press and hold a key and have it repeat in VSCodium?](#press-and-hold) - [How do I open VSCodium from the terminal?](#terminal-support) @@ -122,6 +123,16 @@ To copy your settings manually: - Click the three dots `...` and choose 'Open settings.json' - Copy the contents of settings.json into the same place in VSCodium +## Sign in with GitHub + +In VSCodium, `Sign in with GitHub` is using a Personal Access Token.
+Follow the documentation https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token to create your token.
+Select the scopes dependending of the extension which need access to GitHub. (GitLens requires the `repo` scope.) + +### Linux + +If you are getting the error `Writing login information to the keychain failed with error 'The name org.freedesktop.secrets was not provided by any .service files'.`, you need to install the package `gnome-keyring`. + ## How do I run VSCodium in portable mode? You can follow the [Portable Mode instructions](https://code.visualstudio.com/docs/editor/portable) from the Visual Studio Code website. - **Windows** / **Linux** : the instructions can be followed as written. diff --git a/patches/use-github-pat.patch b/patches/use-github-pat.patch new file mode 100644 index 0000000..81bac0d --- /dev/null +++ b/patches/use-github-pat.patch @@ -0,0 +1,257 @@ +diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts +index 3d36081..5bac245 100644 +--- a/extensions/github-authentication/src/githubServer.ts ++++ b/extensions/github-authentication/src/githubServer.ts +@@ -6,8 +6,6 @@ + import * as nls from 'vscode-nls'; + import * as vscode from 'vscode'; + 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 { Log } from './common/logger'; +@@ -15,8 +13,6 @@ import { Log } from './common/logger'; + const localize = nls.loadMessageBundle(); + + const NETWORK_ERROR = 'network error'; +-const AUTH_RELAY_SERVER = 'vscode-auth.github.com'; +-// const AUTH_RELAY_STAGING_SERVER = 'client-auth-staging-14a768b.herokuapp.com'; + + class UriEventHandler extends vscode.EventEmitter implements vscode.UriHandler { + constructor(private readonly Logger: Log) { +@@ -29,14 +25,6 @@ class UriEventHandler extends vscode.EventEmitter implements vscode. + } + } + +-function parseQuery(uri: vscode.Uri) { +- return uri.query.split('&').reduce((prev: any, current) => { +- const queryString = current.split('='); +- prev[queryString[0]] = queryString[1]; +- return prev; +- }, {}); +-} +- + export interface IGitHubServer extends vscode.Disposable { + login(scopes: string): Promise; + getUserInfo(token: string): Promise<{ id: string, accountName: string }>; +@@ -96,11 +84,7 @@ async function getUserInfo(token: string, serverUri: vscode.Uri, logger: Log): P + export class GitHubServer implements IGitHubServer { + friendlyName = 'GitHub'; + type = AuthProviderType.github; +- private _statusBarItem: vscode.StatusBarItem | undefined; +- private _onDidManuallyProvideToken = new vscode.EventEmitter(); + +- private _pendingStates = new Map(); +- private _codeExchangePromises = new Map, cancel: vscode.EventEmitter }>(); + private _statusBarCommandId = `${this.type}.provide-manually`; + private _disposable: vscode.Disposable; + private _uriHandler = new UriEventHandler(this._logger); +@@ -115,150 +99,35 @@ export class GitHubServer implements IGitHubServer { + this._disposable.dispose(); + } + +- private isTestEnvironment(url: vscode.Uri): boolean { +- return /\.azurewebsites\.net$/.test(url.authority) || url.authority.startsWith('localhost:'); +- } +- +- // TODO@joaomoreno TODO@TylerLeonhardt +- private async isNoCorsEnvironment(): Promise { +- const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/dummy`)); +- return (uri.scheme === 'https' && /^(vscode|github)\./.test(uri.authority)) || (uri.scheme === 'http' && /^localhost/.test(uri.authority)); +- } +- + public async login(scopes: string): Promise { + this._logger.info(`Logging in for the following scopes: ${scopes}`); + +- // TODO@joaomoreno TODO@TylerLeonhardt +- const nocors = await this.isNoCorsEnvironment(); +- const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate${nocors ? '?nocors=true' : ''}`)); +- +- if (this.isTestEnvironment(callbackUri)) { +- 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; +- } ++ const token = await vscode.window.showInputBox({ prompt: 'GitHub Personal Access Token', ignoreFocusOut: true }); ++ if (!token) { throw new Error('Sign in failed: No token provided'); } + +- return scope.split(':').some(splitScopes => { +- return tokenScopes.includes(splitScopes); +- }); +- })) { +- throw new Error(`The provided token is does not match the requested scopes: ${scopes}`); ++ 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 token; +- } +- +- this.updateStatusBarItem(true); +- +- const state = uuid(); +- const existingStates = this._pendingStates.get(scopes) || []; +- this._pendingStates.set(scopes, [...existingStates, state]); +- +- const uri = vscode.Uri.parse(`https://${AUTH_RELAY_SERVER}/authorize/?callbackUri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&responseType=code&authServer=https://github.com${nocors ? '&nocors=true' : ''}`); +- await vscode.env.openExternal(uri); +- +- // Register a single listener for the URI callback, in case the user starts the login process multiple times +- // before completing it. +- let codeExchangePromise = this._codeExchangePromises.get(scopes); +- if (!codeExchangePromise) { +- codeExchangePromise = promiseFromEvent(this._uriHandler.event, this.exchangeCodeForToken(scopes)); +- this._codeExchangePromises.set(scopes, codeExchangePromise); ++ return scope.split(':').some(splitScopes => { ++ return tokenScopes.includes(splitScopes); ++ }); ++ })) { ++ throw new Error(`The provided token is does not match the requested scopes: ${scopes}`); + } + +- return Promise.race([ +- codeExchangePromise.promise, +- promiseFromEvent(this._onDidManuallyProvideToken.event, (token: string | undefined, resolve, reject): void => { +- if (!token) { +- reject('Cancelled'); +- } else { +- resolve(token); +- } +- }).promise, +- new Promise((_, reject) => setTimeout(() => reject('Cancelled'), 60000)) +- ]).finally(() => { +- this._pendingStates.delete(scopes); +- codeExchangePromise?.cancel.fire(); +- this._codeExchangePromises.delete(scopes); +- this.updateStatusBarItem(false); +- }); ++ return token; + } + +- private exchangeCodeForToken: (scopes: string) => PromiseAdapter = +- (scopes) => async (uri, resolve, reject) => { +- const query = parseQuery(uri); +- const code = query.code; +- +- const acceptedStates = this._pendingStates.get(scopes) || []; +- if (!acceptedStates.includes(query.state)) { +- // A common scenario of this happening is if you: +- // 1. Trigger a sign in with one set of scopes +- // 2. Before finishing 1, you trigger a sign in with a different set of scopes +- // In this scenario we should just return and wait for the next UriHandler event +- // to run as we are probably still waiting on the user to hit 'Continue' +- this._logger.info('State not found in accepted state. Skipping this execution...'); +- return; +- } +- +- const url = `https://${AUTH_RELAY_SERVER}/token?code=${code}&state=${query.state}`; +- this._logger.info('Exchanging code for token...'); +- +- // TODO@joao: remove +- if (query.nocors) { +- try { +- const json: any = await vscode.commands.executeCommand('_workbench.fetchJSON', url, 'POST'); +- this._logger.info('Token exchange success!'); +- resolve(json.access_token); +- } catch (err) { +- reject(err); +- } +- } else { +- try { +- const result = await fetch(url, { +- method: 'POST', +- headers: { +- Accept: 'application/json' +- } +- }); +- +- if (result.ok) { +- const json = await result.json(); +- this._logger.info('Token exchange success!'); +- resolve(json.access_token); +- } else { +- reject(result.statusText); +- } +- } catch (ex) { +- reject(ex); +- } +- } +- }; +- + private getServerUri(path: string = '') { + const apiUri = vscode.Uri.parse('https://api.github.com'); + return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}${path}`); + } + +- private updateStatusBarItem(isStart?: boolean) { +- if (isStart && !this._statusBarItem) { +- this._statusBarItem = vscode.window.createStatusBarItem('status.git.signIn', vscode.StatusBarAlignment.Left); +- this._statusBarItem.name = localize('status.git.signIn.name', "GitHub Sign-in"); +- this._statusBarItem.text = localize('signingIn', "$(mark-github) Signing in to github.com..."); +- this._statusBarItem.command = this._statusBarCommandId; +- this._statusBarItem.show(); +- } +- +- if (!isStart && this._statusBarItem) { +- this._statusBarItem.dispose(); +- this._statusBarItem = undefined; +- } +- } +- + private async manuallyProvideUri() { + const uri = await vscode.window.showInputBox({ + prompt: 'Uri', +@@ -290,41 +159,7 @@ export class GitHubServer implements IGitHubServer { + return getUserInfo(token, this.getServerUri('/user'), this._logger); + } + +- public async sendAdditionalTelemetryInfo(token: string): Promise { +- const nocors = await this.isNoCorsEnvironment(); +- +- if (nocors) { +- return; +- } +- +- try { +- const result = await fetch('https://education.github.com/api/user', { +- headers: { +- Authorization: `token ${token}`, +- 'faculty-check-preview': 'true', +- 'User-Agent': 'Visual-Studio-Code' +- } +- }); +- +- if (result.ok) { +- const json: { student: boolean, faculty: boolean } = await result.json(); +- +- /* __GDPR__ +- "session" : { +- "isEdu": { "classification": "NonIdentifiableDemographicInfo", "purpose": "FeatureInsight" } +- } +- */ +- this._telemetryReporter.sendTelemetryEvent('session', { +- isEdu: json.student +- ? 'student' +- : json.faculty +- ? 'faculty' +- : 'none' +- }); +- } +- } catch (e) { +- // No-op +- } ++ public async sendAdditionalTelemetryInfo(_token: string): Promise { + } + + public async checkEnterpriseVersion(token: string): Promise {