diff --git a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-frontend-module.ts b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-frontend-module.ts index 7d109a060..e5a10bb4a 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-frontend-module.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-frontend-module.ts @@ -47,6 +47,7 @@ import { WebviewEnvironment } from '@theia/plugin-ext/lib/main/browser/webview/w import { CheWebviewEnvironment } from './che-webview-environment'; import { TaskStatusHandler } from './task-status-handler'; import { PluginFrontendViewContribution } from '@theia/plugin-ext/lib/main/browser/plugin-frontend-view-contribution'; +import { OauthUtils } from './oauth-utils'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(CheApiProvider).toSelf().inSingletonScope(); @@ -105,4 +106,5 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(MiniBrowserOpenHandler).to(CheMiniBrowserOpenHandler).inSingletonScope(); bind(TaskStatusHandler).toSelf().inSingletonScope(); + bind(OauthUtils).toSelf().inSingletonScope(); }); 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 0ca660a75..1a76dbbcf 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 @@ -19,7 +19,7 @@ export class CheGithubMainImpl implements CheGithubMain { private readonly oAuthUtils: OauthUtils; constructor(container: interfaces.Container) { - this.oAuthUtils = new OauthUtils(container); + this.oAuthUtils = container.get(OauthUtils); } async $uploadPublicSshKey(publicKey: string): Promise { @@ -53,10 +53,13 @@ export class CheGithubMainImpl implements CheGithubMain { private async updateToken(): Promise { const oAuthProvider = 'github'; - this.token = await this.oAuthUtils.getToken(oAuthProvider); - if (!this.token) { - await this.oAuthUtils.authenticate(oAuthProvider, ['write:public_key']); + try { this.token = await this.oAuthUtils.getToken(oAuthProvider); + } catch (e) { + if (e.message.indexOf('Request failed with status code 401') > 0) { + await this.oAuthUtils.authenticate(oAuthProvider, ['write:public_key']); + this.token = await this.oAuthUtils.getToken(oAuthProvider); + } } } } diff --git a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-oauth-main.ts b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-oauth-main.ts index e50fd0e57..01809db7c 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-oauth-main.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-oauth-main.ts @@ -16,7 +16,7 @@ export class CheOauthMainImpl implements CheOauthMain { private readonly oAuthUtils: OauthUtils; constructor(container: interfaces.Container) { - this.oAuthUtils = new OauthUtils(container); + this.oAuthUtils = container.get(OauthUtils); } $getProviders(): Promise { @@ -26,4 +26,7 @@ export class CheOauthMainImpl implements CheOauthMain { $isAuthenticated(provider: string): Promise { return this.oAuthUtils.isAuthenticated(provider); } + $isRegistered(provider: string): Promise { + return this.oAuthUtils.isRegistered(provider); + } } diff --git a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-openshift-main.ts b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-openshift-main.ts index 2a93e1ad3..4192d60d6 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-openshift-main.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-openshift-main.ts @@ -11,19 +11,44 @@ import { CheOpenshiftMain } from '../common/che-protocol'; import { interfaces } from 'inversify'; import { OauthUtils } from './oauth-utils'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; export class CheOpenshiftMainImpl implements CheOpenshiftMain { private readonly oAuthUtils: OauthUtils; + private isMultiUser: boolean; constructor(container: interfaces.Container) { - this.oAuthUtils = new OauthUtils(container); + this.oAuthUtils = container.get(OauthUtils); + container.get(EnvVariablesServer).getValue('CHE_MACHINE_TOKEN').then(variable => { + if (variable && variable.value && variable.value.length > 0) { + this.isMultiUser = true; + } + }); } async $getToken(): Promise { - const oAuthProvider = 'openshift'; - let token = await this.oAuthUtils.getToken(oAuthProvider); - if (!token) { - await this.oAuthUtils.authenticate(oAuthProvider); + let oAuthProvider = 'openshift'; + const unauthorisedMessage = 'Request failed with status code 401'; + // Multi-user mode doesn't support list of registered provers request, + // so we need to check which version of openshift is registered. + if (this.isMultiUser) { + try { + const openShift3token = await this.oAuthUtils.getToken('openshift-v3'); + if (openShift3token) { + return openShift3token; + } + } catch (e) { + // 401 means that the provider is registered but Che is not authorised for it. + oAuthProvider = e.message.indexOf(unauthorisedMessage) > 0 ? 'openshift-v3' : 'openshift-v4'; + } + } + let token; + try { token = await this.oAuthUtils.getToken(oAuthProvider); + } catch (e) { + if (e.message.indexOf(unauthorisedMessage) > 0) { + await this.oAuthUtils.authenticate(oAuthProvider); + token = await this.oAuthUtils.getToken(oAuthProvider); + } } if (token) { return token; diff --git a/extensions/eclipse-che-theia-plugin-ext/src/browser/oauth-utils.ts b/extensions/eclipse-che-theia-plugin-ext/src/browser/oauth-utils.ts index 3f4d1f97b..b41de54a1 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/browser/oauth-utils.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/browser/oauth-utils.ts @@ -7,19 +7,23 @@ * * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ -import { interfaces } from 'inversify'; +import { inject, injectable } from 'inversify'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { CheApiService } from '../common/che-protocol'; +import { Emitter, Event } from '@theia/core/lib/common'; +@injectable() export class OauthUtils { - private readonly envVariableServer: EnvVariablesServer; + private apiUrl: string; private machineToken: string | undefined; - private readonly cheApiService: CheApiService; - - constructor(container: interfaces.Container) { - this.envVariableServer = container.get(EnvVariablesServer); - this.cheApiService = container.get(CheApiService); + private oAuthPopup: Window | undefined; + private userToken: string | undefined; + private readonly onDidReceiveToken: Event; + constructor(@inject(EnvVariablesServer) private readonly envVariableServer: EnvVariablesServer, + @inject(CheApiService) private readonly cheApiService: CheApiService) { + const onDidReceiveTokenEmitter = new Emitter(); + this.onDidReceiveToken = onDidReceiveTokenEmitter.event; this.envVariableServer.getValue('CHE_API').then(variable => { if (variable && variable.value) { this.apiUrl = variable.value; @@ -30,56 +34,73 @@ export class OauthUtils { this.machineToken = variable.value; } }); + window.addEventListener('message', data => { + if (data.data.startsWith('token:') && this.oAuthPopup) { + this.oAuthPopup.close(); + this.userToken = data.data.substring(6, data.data.length); + onDidReceiveTokenEmitter.fire(undefined); + } else if (data.data.startsWith('status:') && this.oAuthPopup) { + this.oAuthPopup.postMessage('token:' + (this.machineToken ? this.machineToken : ''), '*'); + } + }); + } + + private async getUserToken(): Promise { + if (this.userToken) { + return this.userToken; + } else if (this.machineToken && this.machineToken.length > 0) { + const popup = window.open(`${this.apiUrl.substring(0, this.apiUrl.indexOf('/api'))}/_app/oauth.html`, + 'popup', 'toolbar=no, status=no, menubar=no, scrollbars=no, width=10, height=10, visible=none'); + if (popup) { + this.oAuthPopup = popup; + } + return new Promise(async resolve => { + this.onDidReceiveToken(() => resolve(this.userToken)); + }); + } } async getToken(oAuthProvider: string): Promise { - return await this.cheApiService.getOAuthToken(oAuthProvider); + return await this.cheApiService.getOAuthToken(oAuthProvider, await this.getUserToken()); } async getProviders(): Promise { - return await this.cheApiService.getOAuthProviders(); + return await this.cheApiService.getOAuthProviders(await this.getUserToken()); } async isAuthenticated(provider: string): Promise { - const token = await this.cheApiService.getOAuthToken(provider); - return token !== undefined; + try { + await this.cheApiService.getOAuthToken(provider, await this.getUserToken()); + return true; + } catch (e) { + return false; + } + } + + async isRegistered(provider: string): Promise { + try { + await this.cheApiService.getOAuthToken(provider, await this.getUserToken()); + return true; + } catch (e) { + return e.message.indexOf('Request failed with status code 401') > 0; + } } authenticate(oauthProvider: string, scope?: string[]): Promise { return new Promise(async (resolve, reject) => { const redirectUrl = window.location.href; - let url = `${this.apiUrl}/oauth/authenticate?oauth_provider=${oauthProvider}&userId=${await this.cheApiService.getUserId()}`; + let url = `${this.apiUrl.substring(0, this.apiUrl.indexOf('/api'))}/_app/oauth.html?oauth_provider=${oauthProvider}`; if (scope) { for (const s of scope) { url += `&scope=${s}`; } } - if (this.machineToken) { - url += `&token=${this.machineToken}`; - } url += `&redirect_after_login=${redirectUrl}`; - const popupWindow = window.open(url, 'popup'); - const popup_close_handler = async () => { - if (!popupWindow || popupWindow.closed) { - if (popupCloseHandlerIntervalId) { - window.clearInterval(popupCloseHandlerIntervalId); - } - reject(new Error('Authentication failed!')); - } else { - try { - if (redirectUrl === popupWindow.location.href) { - if (popupCloseHandlerIntervalId) { - window.clearInterval(popupCloseHandlerIntervalId); - } - popupWindow.close(); - resolve(); - } - } catch (error) { - } - } - }; - - const popupCloseHandlerIntervalId = window.setInterval(popup_close_handler, 80); + const popup = window.open(url, 'popup'); + if (popup) { + this.oAuthPopup = popup; + } + this.onDidReceiveToken(() => resolve(undefined)); }); } } 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 adf4a8777..584e00d59 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 @@ -82,11 +82,13 @@ export interface CheGithubMain { export interface CheOauth { getProviders(): Promise; isAuthenticated(provider: string): Promise; + isRegistered(provider: string): Promise; } export interface CheOauthMain { $getProviders(): Promise; $isAuthenticated(provider: string): Promise; + $isRegistered(provider: string): Promise; } /** @@ -461,7 +463,7 @@ export interface CheApiService { getFactoryById(factoryId: string): Promise; - getUserId(): Promise; + getUserId(token?: string): Promise; getUserPreferences(): Promise; getUserPreferences(filter: string | undefined): Promise; updateUserPreferences(update: Preferences): Promise; @@ -477,8 +479,8 @@ 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; - getOAuthProviders(): Promise; + getOAuthToken(oAuthProvider: string, token?: string): Promise; + getOAuthProviders(token?: 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 f5e7688dc..b7ff6ec07 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,9 +32,9 @@ export class CheApiServiceImpl implements CheApiService { return process.env.CHE_API_INTERNAL; } - async getUserId(): Promise { + async getUserId(token?: string): Promise { const cheApiClient = await this.getCheApiClient(); - const user = await cheApiClient.getCurrentUser(); + const user = await cheApiClient.getCurrentUser(token); return user.id; } @@ -296,19 +296,15 @@ export class CheApiServiceImpl implements CheApiService { } } - async getOAuthToken(oAuthProvider: string): Promise { + async getOAuthToken(oAuthProvider: string, token?: string): Promise { const cheApiClient = await this.getCheApiClient(); - try { - return await cheApiClient.getOAuthToken(oAuthProvider); - } catch (e) { - return undefined; - } + return cheApiClient.getOAuthToken(oAuthProvider, token); } - async getOAuthProviders(): Promise { + async getOAuthProviders(token?: string): Promise { const cheApiClient = await this.getCheApiClient(); try { - return await cheApiClient.getOAuthProviders(); + return await cheApiClient.getOAuthProviders(token); } catch (e) { return []; } 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 0ff71d02c..a31198ab1 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 @@ -146,6 +146,9 @@ export function createAPIFactory(rpc: RPCProtocol): CheApiFactory { }, isAuthenticated(provider: string): Promise { return cheOauthImpl.isAuthenticated(provider); + }, + isRegistered(provider: string): Promise { + return cheOauthImpl.isRegistered(provider); } }; diff --git a/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-oauth.ts b/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-oauth.ts index 5b56925bd..e84b6c62b 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-oauth.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-oauth.ts @@ -26,4 +26,8 @@ export class CheOauthImpl implements CheOauth { isAuthenticated(provider: string): Promise { return this.oAuthMain.$isAuthenticated(provider); } + + isRegistered(provider: string): Promise { + return this.oAuthMain.$isRegistered(provider); + } } 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 8768a6083..288e9c970 100644 --- a/extensions/eclipse-che-theia-plugin/src/che-proposed.d.ts +++ b/extensions/eclipse-che-theia-plugin/src/che-proposed.d.ts @@ -55,7 +55,17 @@ declare module '@eclipse-che/plugin' { export namespace oAuth { export function getProviders(): Promise; + /** + * Returns {@code true} if the current user is authenticated for given oAuth provider. + * @param provider oAuth provider to Check. + */ export function isAuthenticated(provider: string): Promise; + /** + * Returns {@code true} if the given oAuth provider is registered. + * Use {@link $getProviders} in single-user mode to find the provider in the list. + * @param provider oAuth provider to Check. + */ + export function isRegistered(provider: string): Promise; } export namespace ssh { diff --git a/plugins/ssh-plugin/src/ssh-plugin-backend.ts b/plugins/ssh-plugin/src/ssh-plugin-backend.ts index 2aa9ab2af..c2952a525 100644 --- a/plugins/ssh-plugin/src/ssh-plugin-backend.ts +++ b/plugins/ssh-plugin/src/ssh-plugin-backend.ts @@ -52,11 +52,6 @@ export async function start(): Promise { } // If the remote repository is a GitHub repository, ask to upload a public SSH key. if (out.indexOf('git@github.com') > -1) { - // Currently multi-user che-theia doesn't support GiHub oAuth. - if (isMultiUser()) { - showWarningMessage(keys.length === 0, 'GitHub'); - return; - } switch (command) { case 'clone': { if (await askToGenerateIfEmptyAndUploadKeyToGithub(keys, true)) { @@ -83,8 +78,6 @@ export async function start(): Promise { } }; - const isMultiUser = (): boolean => !!process.env['KEYCLOAK_SERVICE_HOST']; - const showWarningMessage = (showGenerate: boolean, gitProviderName?: string) => theia.window.showWarningMessage(`Permission denied, please ${showGenerate ? 'generate (F1 => ' + GENERATE.label + ') and ' : ''} upload your public SSH key to ${gitProviderName ? gitProviderName : 'the Git provider'} and try again. To get the public key press F1 => ${VIEW.label}`);