Skip to content

Commit

Permalink
Add GitHub authentication provider extension, closes #90384
Browse files Browse the repository at this point in the history
  • Loading branch information
Rachel Macfarlane authored Feb 20, 2020
1 parent ebb6aaa commit eed3932
Show file tree
Hide file tree
Showing 18 changed files with 1,087 additions and 0 deletions.
5 changes: 5 additions & 0 deletions build/azure-pipelines/product-compile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ steps:
yarn gulp minify-vscode-reh-web
displayName: Compile
condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true'))
env:
OSS_GITHUB_ID: "a5d3c261b032765a78de"
OSS_GITHUB_SECRET: $(oss-github-client-secret)
INSIDERS_GITHUB_ID: "31f02627809389d9f111"
INSIDERS_GITHUB_SECRET: $(insiders-github-client-secret)

- script: |
set -e
Expand Down
12 changes: 12 additions & 0 deletions build/lib/compilation.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ function compileTask(src, out, build) {
if (src === 'src') {
generator.execute();
}
generateGitHubAuthConfig();
return srcPipe
.pipe(generator.stream)
.pipe(compile())
Expand All @@ -96,6 +97,17 @@ function watchTask(out, build) {
}
exports.watchTask = watchTask;
const REPO_SRC_FOLDER = path.join(__dirname, '../../src');
function generateGitHubAuthConfig() {
const schemes = ['OSS', 'INSIDERS'];
let content = {};
schemes.forEach(scheme => {
content[scheme] = {
id: process.env[`${scheme}_GITHUB_ID`],
secret: process.env[`${scheme}_GITHUB_SECRET`]
};
});
fs.writeFileSync(path.join(__dirname, '../../extensions/github-authentication/src/common/config.json'), JSON.stringify(content));
}
class MonacoGenerator {
constructor(isWatch) {
this._executeSoonTimer = null;
Expand Down
15 changes: 15 additions & 0 deletions build/lib/compilation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export function compileTask(src: string, out: string, build: boolean): () => Nod
generator.execute();
}

generateGitHubAuthConfig();

return srcPipe
.pipe(generator.stream)
.pipe(compile())
Expand Down Expand Up @@ -115,6 +117,19 @@ export function watchTask(out: string, build: boolean): () => NodeJS.ReadWriteSt

const REPO_SRC_FOLDER = path.join(__dirname, '../../src');

function generateGitHubAuthConfig() {
const schemes = ['OSS', 'INSIDERS'];
let content: { [key: string]: { id?: string, secret?: string }} = {};
schemes.forEach(scheme => {
content[scheme] = {
id: process.env[`${scheme}_GITHUB_ID`],
secret: process.env[`${scheme}_GITHUB_SECRET`]
};
});

fs.writeFileSync(path.join(__dirname, '../../extensions/github-authentication/src/common/config.json'), JSON.stringify(content));
}

class MonacoGenerator {
private readonly _isWatch: boolean;
public readonly stream: NodeJS.ReadWriteStream;
Expand Down
1 change: 1 addition & 0 deletions extensions/github-authentication/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/common/config.json
7 changes: 7 additions & 0 deletions extensions/github-authentication/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# GitHub Authentication for Visual Studio Code

**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.

## Features

This extension provides support for authenticating to GitHub.
20 changes: 20 additions & 0 deletions extensions/github-authentication/extension.webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

//@ts-check

'use strict';

const withDefaults = require('../shared.webpack.config');

module.exports = withDefaults({
context: __dirname,
entry: {
extension: './src/extension.ts',
},
externals: {
'keytar': 'commonjs keytar'
}
});
32 changes: 32 additions & 0 deletions extensions/github-authentication/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "github-authentication",
"displayName": "%displayName%",
"description": "%description%",
"publisher": "vscode",
"version": "0.0.1",
"engines": {
"vscode": "^1.41.0"
},
"enableProposedApi": true,
"categories": [
"Other"
],
"activationEvents": [
"*"
],
"main": "./out/extension.js",
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "gulp compile-extension:github-authentication",
"watch": "gulp watch-extension:github-authentication"
},
"dependencies": {
"uuid": "^3.3.3"
},
"devDependencies": {
"@types/keytar": "^4.4.2",
"@types/node": "^10.12.21",
"@types/uuid": "^3.4.6",
"typescript": "^3.7.5"
}
}
4 changes: 4 additions & 0 deletions extensions/github-authentication/package.nls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"displayName": "GitHub Authentication",
"description": "GitHub Authentication Provider"
}
46 changes: 46 additions & 0 deletions extensions/github-authentication/src/common/clientRegistrar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

export interface ClientDetails {
id?: string;
secret?: string;
}

export interface ClientConfig {
OSS: ClientDetails;
INSIDERS: ClientDetails;
}

export class Registrar {
private _config: ClientConfig;

constructor() {
this._config = require('./config.json') as ClientConfig;
}
getClientDetails(product: string): ClientDetails {
let details: ClientDetails | undefined;
switch (product) {
case 'code-oss':
details = this._config.OSS;
break;

case 'vscode-insiders':
details = this._config.INSIDERS;
break;

default:
throw new Error(`Unrecognized product ${product}`);
}

if (!details.id || !details.secret) {
throw new Error(`No client configuration available for ${product}`);
}

return details;
}
}

const ClientRegistrar = new Registrar();
export default ClientRegistrar;
73 changes: 73 additions & 0 deletions extensions/github-authentication/src/common/keychain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

// keytar depends on a native module shipped in vscode, so this is
// how we load it
import * as keytarType from 'keytar';
import { env } from 'vscode';
import Logger from './logger';

function getKeytar(): Keytar | undefined {
try {
return require('keytar');
} catch (err) {
console.log(err);
}

return undefined;
}

export type Keytar = {
getPassword: typeof keytarType['getPassword'];
setPassword: typeof keytarType['setPassword'];
deletePassword: typeof keytarType['deletePassword'];
};

const SERVICE_ID = `${env.uriScheme}-github.login`;
const ACCOUNT_ID = 'account';

export class Keychain {
private keytar: Keytar;

constructor() {
const keytar = getKeytar();
if (!keytar) {
throw new Error('System keychain unavailable');
}

this.keytar = keytar;
}

async setToken(token: string): Promise<void> {
try {
return await this.keytar.setPassword(SERVICE_ID, ACCOUNT_ID, token);
} catch (e) {
// Ignore
Logger.error(`Setting token failed: ${e}`);
}
}

async getToken(): Promise<string | null | undefined> {
try {
return await this.keytar.getPassword(SERVICE_ID, ACCOUNT_ID);
} catch (e) {
// Ignore
Logger.error(`Getting token failed: ${e}`);
return Promise.resolve(undefined);
}
}

async deleteToken(): Promise<boolean | undefined> {
try {
return await this.keytar.deletePassword(SERVICE_ID, ACCOUNT_ID);
} catch (e) {
// Ignore
Logger.error(`Deleting token failed: ${e}`);
return Promise.resolve(undefined);
}
}
}

export const keychain = new Keychain();
55 changes: 55 additions & 0 deletions extensions/github-authentication/src/common/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';

type LogLevel = 'Trace' | 'Info' | 'Error';

class Log {
private output: vscode.OutputChannel;

constructor() {
this.output = vscode.window.createOutputChannel('GitHub Authentication');
}

private data2String(data: any): string {
if (data instanceof Error) {
return data.stack || data.message;
}
if (data.success === false && data.message) {
return data.message;
}
return data.toString();
}

public info(message: string, data?: any): void {
this.logLevel('Info', message, data);
}

public error(message: string, data?: any): void {
this.logLevel('Error', message, data);
}

public logLevel(level: LogLevel, message: string, data?: any): void {
this.output.appendLine(`[${level} - ${this.now()}] ${message}`);
if (data) {
this.output.appendLine(this.data2String(data));
}
}

private now(): string {
const now = new Date();
return padLeft(now.getUTCHours() + '', 2, '0')
+ ':' + padLeft(now.getMinutes() + '', 2, '0')
+ ':' + padLeft(now.getUTCSeconds() + '', 2, '0') + '.' + now.getMilliseconds();
}
}

function padLeft(s: string, n: number, pad = ' ') {
return pad.repeat(Math.max(0, n - s.length)) + s;
}

const Logger = new Log();
export default Logger;
73 changes: 73 additions & 0 deletions extensions/github-authentication/src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Event, Disposable } from 'vscode';

export function filterEvent<T>(event: Event<T>, filter: (e: T) => boolean): Event<T> {
return (listener, thisArgs = null, disposables?) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables);
}

export function onceEvent<T>(event: Event<T>): Event<T> {
return (listener, thisArgs = null, disposables?) => {
const result = event(e => {
result.dispose();
return listener.call(thisArgs, e);
}, null, disposables);

return result;
};
}


export interface PromiseAdapter<T, U> {
(
value: T,
resolve:
(value?: U | PromiseLike<U>) => void,
reject:
(reason: any) => void
): any;
}

const passthrough = (value: any, resolve: (value?: any) => void) => resolve(value);

/**
* Return a promise that resolves with the next emitted event, or with some future
* event as decided by an adapter.
*
* If specified, the adapter is a function that will be called with
* `(event, resolve, reject)`. It will be called once per event until it resolves or
* rejects.
*
* The default adapter is the passthrough function `(value, resolve) => resolve(value)`.
*
* @param event the event
* @param adapter controls resolution of the returned promise
* @returns a promise that resolves or rejects as specified by the adapter
*/
export async function promiseFromEvent<T, U>(
event: Event<T>,
adapter: PromiseAdapter<T, U> = passthrough): Promise<U> {
let subscription: Disposable;
return new Promise<U>((resolve, reject) =>
subscription = event((value: T) => {
try {
Promise.resolve(adapter(value, resolve, reject))
.catch(reject);
} catch (error) {
reject(error);
}
})
).then(
(result: U) => {
subscription.dispose();
return result;
},
error => {
subscription.dispose();
throw error;
}
);
}
Loading

0 comments on commit eed3932

Please sign in to comment.