diff --git a/che-theia-init-sources.yml b/che-theia-init-sources.yml index 366a91b96..22f62d725 100644 --- a/che-theia-init-sources.yml +++ b/che-theia-init-sources.yml @@ -20,4 +20,5 @@ sources: - plugins/welcome-plugin - plugins/ssh-plugin - plugins/telemetry-plugin + - plugins/github-auth-plugin checkoutTo: master diff --git a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-github-main.ts b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-github-main.ts index fc8f0149e..ba3f530d4 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-github-main.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-github-main.ts @@ -8,7 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ -import { CheGithubMain } from '../common/che-protocol'; +import { CheApiService, CheGithubMain } from '../common/che-protocol'; import { interfaces } from 'inversify'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import axios, { AxiosInstance } from 'axios'; @@ -18,9 +18,11 @@ export class CheGithubMainImpl implements CheGithubMain { private axiosInstance: AxiosInstance = axios; private apiUrl: string; private token: string | undefined; + private readonly cheApiService: CheApiService; constructor(container: interfaces.Container) { this.envVariableServer = container.get(EnvVariablesServer); + this.cheApiService = container.get(CheApiService); this.envVariableServer.getValue('CHE_API').then(variable => { if (variable && variable.value) { this.apiUrl = variable.value; @@ -36,6 +38,15 @@ export class CheGithubMainImpl implements CheGithubMain { }); } + async $getToken(): Promise { + await this.fetchToken(); + if (this.token) { + return this.token; + } else { + throw new Error('Failed to get GitHub authentication token'); + } + } + private async fetchToken(): Promise { if (!this.token) { await this.updateToken(); @@ -49,17 +60,17 @@ export class CheGithubMainImpl implements CheGithubMain { } private async updateToken(): Promise { - this.token = await this.getToken(); + this.token = await this.cheApiService.getOAuthToken('github'); if (!this.token) { await this.authenticate(); - this.token = await this.getToken(); + this.token = await this.cheApiService.getOAuthToken('github'); } } private authenticate(): Promise { return new Promise(async (resolve, reject) => { const redirectUrl = window.location.href; - const url = `${this.apiUrl}/oauth/authenticate?oauth_provider=github&userId=${await this.getUserId()}` + + const url = `${this.apiUrl}/oauth/authenticate?oauth_provider=github&userId=${await this.cheApiService.getUserId()}` + `&scope=write:public_key&redirect_after_login=${redirectUrl}`; const popupWindow = window.open(url, 'popup'); const popup_close_handler = async () => { @@ -85,18 +96,4 @@ export class CheGithubMainImpl implements CheGithubMain { const popupCloseHandlerIntervalId = window.setInterval(popup_close_handler, 80); }); } - - private async getToken(): Promise { - try { - const result = await this.axiosInstance.get<{ token: string }>(`${this.apiUrl}/oauth/token?oauth_provider=github`); - return result.data.token; - } catch (e) { - return undefined; - } - } - - private async getUserId(): Promise { - const result = await this.axiosInstance.get<{ id: string }>(`${this.apiUrl}/user`); - return result.data.id; - } } diff --git a/extensions/eclipse-che-theia-plugin-ext/src/common/che-protocol.ts b/extensions/eclipse-che-theia-plugin-ext/src/common/che-protocol.ts index 9cb867f36..e8b3d0f89 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/common/che-protocol.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/common/che-protocol.ts @@ -46,10 +46,6 @@ export interface CheFactoryMain { export interface CheDevfile { } -export interface CheGithub { - uploadPublicSshKey(publicKey: string): Promise; -} - export interface CheDevfileMain { $createWorkspace(devfilePath: string): Promise; } @@ -65,8 +61,14 @@ export interface CheSshMain { $deleteKey(service: string, name: string): Promise; } +export interface CheGithub { + uploadPublicSshKey(publicKey: string): Promise; + getToken(): Promise; +} + export interface CheGithubMain { $uploadPublicSshKey(publicKey: string): Promise; + $getToken(): Promise; } /** @@ -432,6 +434,7 @@ export interface CheApiService { getFactoryById(factoryId: string): Promise; + getUserId(): Promise; getUserPreferences(): Promise; getUserPreferences(filter: string | undefined): Promise; updateUserPreferences(update: Preferences): Promise; @@ -447,6 +450,7 @@ export interface CheApiService { getAllSshKey(service: string): Promise; submitTelemetryEvent(id: string, ownerId: string, ip: string, agent: string, resolution: string, properties: [string, string][]): Promise; submitTelemetryActivity(): Promise; + getOAuthToken(oAuthProvider: string): Promise; } export const CHE_TASK_SERVICE_PATH = '/che-task-service'; diff --git a/extensions/eclipse-che-theia-plugin-ext/src/node/che-api-service.ts b/extensions/eclipse-che-theia-plugin-ext/src/node/che-api-service.ts index a7cf52373..a4a81538c 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/node/che-api-service.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/node/che-api-service.ts @@ -32,6 +32,12 @@ export class CheApiServiceImpl implements CheApiService { return process.env.CHE_API_INTERNAL; } + async getUserId(): Promise { + const cheApiClient = await this.getCheApiClient(); + const user = await cheApiClient.getCurrentUser(); + return user.id; + } + async getUserPreferences(filter?: string): Promise { const cheApiClient = await this.getCheApiClient(); return cheApiClient.getUserPreferences(filter); @@ -282,6 +288,15 @@ export class CheApiServiceImpl implements CheApiService { } } + async getOAuthToken(oAuthProvider: string): Promise { + const cheApiClient = await this.getCheApiClient(); + try { + return await cheApiClient.getOAuthToken(oAuthProvider); + } catch (e) { + return undefined; + } + } + private getWorkspaceTelemetryClient(): TelemetryClient | undefined { if (!this.telemetryClient) { diff --git a/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-api.ts b/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-api.ts index 7034ac212..3870ca1d6 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-api.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-api.ts @@ -117,6 +117,9 @@ export function createAPIFactory(rpc: RPCProtocol): CheApiFactory { const github: typeof che.github = { uploadPublicSshKey(publicKey: string): Promise { return cheGithubImpl.uploadPublicSshKey(publicKey); + }, + getToken(): Promise { + return cheGithubImpl.getToken(); } }; diff --git a/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-github.ts b/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-github.ts index b53bd8818..e5e872609 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-github.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-github.ts @@ -22,4 +22,8 @@ export class CheGithubImpl implements CheGithub { uploadPublicSshKey(publicKey: string): Promise { return this.githubMain.$uploadPublicSshKey(publicKey); } + + getToken(): Promise { + return this.githubMain.$getToken(); + } } diff --git a/extensions/eclipse-che-theia-plugin/src/che-proposed.d.ts b/extensions/eclipse-che-theia-plugin/src/che-proposed.d.ts index bf5ff13ec..e7da6e640 100644 --- a/extensions/eclipse-che-theia-plugin/src/che-proposed.d.ts +++ b/extensions/eclipse-che-theia-plugin/src/che-proposed.d.ts @@ -46,6 +46,7 @@ declare module '@eclipse-che/plugin' { export namespace github { export function uploadPublicSshKey(publicKey: string): Promise; + export function getToken(): Promise; } export namespace ssh { diff --git a/plugins/github-auth-plugin/.gitignore b/plugins/github-auth-plugin/.gitignore new file mode 100644 index 000000000..042d83637 --- /dev/null +++ b/plugins/github-auth-plugin/.gitignore @@ -0,0 +1,3 @@ +lib/ +node_modules/ +*.theia diff --git a/plugins/github-auth-plugin/README.md b/plugins/github-auth-plugin/README.md new file mode 100644 index 000000000..39f3b296e --- /dev/null +++ b/plugins/github-auth-plugin/README.md @@ -0,0 +1,9 @@ +# Theia - vscode Github pull-request plugin authenticator + +The plugin calls Che oAuth API service to get the GitHub token. +Then it injects it to the user preferences. When the browser page is refreshed, +the vscode GitHub PR plugin fetches the token and cleans the token from the preferences file. + +## License + +[EPL-2.0](http://www.eclipse.org/legal/epl-2.0) diff --git a/plugins/github-auth-plugin/package.json b/plugins/github-auth-plugin/package.json new file mode 100644 index 000000000..cb8c7bed8 --- /dev/null +++ b/plugins/github-auth-plugin/package.json @@ -0,0 +1,42 @@ +{ + "name": "@eclipse-che/github-auth-plugin", + "version": "0.0.1", + "publisher": "Eclipse Che", + "keywords": [ + "theia-plugin" + ], + "description": "Authenticates the vscode Github pull-request plugin", + "license": "EPL-2.0", + + "files": [ + "src" + ], + "activationEvents": [ + "*" + ], + "devDependencies": { + "@theia/plugin": "next", + "@theia/plugin-packager": "latest", + "@eclipse-che/plugin": "0.0.1", + "rimraf": "2.6.2", + "typescript-formatter": "7.2.2", + "typescript": "2.9.2", + "ts-loader": "^4.1.0" + }, + "scripts": { + "prepare": "yarn run clean && yarn run build", + "clean": "rimraf lib", + "format": "tsfmt -r --useTsfmt ../../configs/tsfmt.json", + "lint": "tslint -c ../../configs/tslint.json --project tsconfig.json", + "lint:fix": "tslint -c ../../configs/tslint.json --fix --project .", + "compile": "tsc", + "build": "yarn lint:fix && concurrently -n \"format,lint,compile\" -c \"red,green,blue\" \"yarn format\" \"yarn lint\" \"yarn compile\" && theia-plugin pack", + "watch": "tsc -w" + }, + "engines": { + "theiaPlugin": "next" + }, + "theiaPlugin": { + "backend": "lib/github-auth-plugin.js" + } +} diff --git a/plugins/github-auth-plugin/src/github-auth-plugin.ts b/plugins/github-auth-plugin/src/github-auth-plugin.ts new file mode 100644 index 000000000..67ba6ada8 --- /dev/null +++ b/plugins/github-auth-plugin/src/github-auth-plugin.ts @@ -0,0 +1,35 @@ +/********************************************************************* + * Copyright (c) 2020 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ + +import * as theia from '@theia/plugin'; +import * as che from '@eclipse-che/plugin'; + +export function start(context: theia.PluginContext) { + if (theia.plugins.getPlugin('github.vscode-pull-request-github')) { + const command = { + id: 'github-plugin-authenticate', + label: 'GitHub authenticate' + }; + context.subscriptions.push(theia.commands.registerCommand(command, async () => { + const token = await che.github.getToken(); + const conf = await theia.workspace.getConfiguration(); + await conf.update('githubPullRequests.hosts', [{ + host: 'github.com', + token + }], theia.ConfigurationTarget.Global); + theia.window.showWarningMessage('GitHub token has been set to preferences. ' + + 'Refresh the page to reinitialise the vscode GitHub pull-request plugin with the token'); + })); + } +} + +export function stop() { + +} diff --git a/plugins/github-auth-plugin/tsconfig.json b/plugins/github-auth-plugin/tsconfig.json new file mode 100644 index 000000000..7a74a2a01 --- /dev/null +++ b/plugins/github-auth-plugin/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "skipLibCheck": true, + "lib": [ + "es6", + "webworker" + ], + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ] +} diff --git a/plugins/github-auth-plugin/tsfmt.json b/plugins/github-auth-plugin/tsfmt.json new file mode 100644 index 000000000..ad905f319 --- /dev/null +++ b/plugins/github-auth-plugin/tsfmt.json @@ -0,0 +1,18 @@ +{ + "baseIndentSize": 0, + "newLineCharacter": "\n", + "indentSize": 4, + "tabSize": 4, + "indentStyle": 4, + "convertTabsToSpaces": true, + "insertSpaceAfterCommaDelimiter": true, + "insertSpaceAfterSemicolonInForStatements": true, + "insertSpaceBeforeAndAfterBinaryOperators": true, + "insertSpaceAfterKeywordsInControlFlowStatements": true, + "insertSpaceAfterFunctionKeywordForAnonymousFunctions": false, + "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, + "insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, + "insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false, + "placeOpenBraceOnNewLineForFunctions": false, + "placeOpenBraceOnNewLineForControlBlocks": false +} diff --git a/yarn.lock b/yarn.lock index 5f9062fb3..d65df45c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -43,9 +43,9 @@ "@eclipse-che/api" latest "@eclipse-che/workspace-client@latest": - version "0.0.1-1574171760" - resolved "https://registry.yarnpkg.com/@eclipse-che/workspace-client/-/workspace-client-0.0.1-1574171760.tgz#e4066030fe4cb35f21a82e173e37633d37c76520" - integrity sha512-lCnrrb9jKkxbO9QSmFERHvDxrN+8j1X/3RypESu7n7nrG53ubEqnnquTbU+vEgrMHyg1x0ctLRiUBQarM8dslg== + version "0.0.1-1579077578" + resolved "https://registry.yarnpkg.com/@eclipse-che/workspace-client/-/workspace-client-0.0.1-1579077578.tgz#0fc322b536eb46bf271effc3f00a0435038b5966" + integrity sha512-j3hPUaMcld6cw0wuqQd/EjjUAkkq7J5iri8MCJYJf2eSylLnjYRabNfnfVS4REgj3b02VXLx3kuDGmNPLacSEw== dependencies: "@eclipse-che/api" "^7.0.0-beta-4.0" axios "0.19.0"