Skip to content

Commit

Permalink
GH-3397: Implemented the HTTP-based authentication for Git in Electron.
Browse files Browse the repository at this point in the history
If a Git operation (fetch, pull, merge, ...) requires authentication,
the Theia backend will ask the frontend for the username and password.

User credentials are neither stored nor reused.

In electron, if a Git operation fails due to authentication, we
report it back to the user. This PR does not change the behavior of the
browser-based application.

Closes: #3397

Signed-off-by: Akos Kitta <[email protected]>
  • Loading branch information
Akos Kitta committed Nov 19, 2018
1 parent 68f9192 commit 67fabac
Show file tree
Hide file tree
Showing 22 changed files with 937 additions and 59 deletions.
7 changes: 7 additions & 0 deletions packages/core/src/browser/quick-open/quick-open-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export namespace QuickOpenOptions {
*/
readonly showItemsWithoutHighlight: boolean;

/**
* `true` if the quick open widget provides a way for the user to securely enter a password.
* Otherwise, `false`.
*/
readonly password: boolean;

selectIndex(lookfor: string): number;

onClose(canceled: boolean): void;
Expand All @@ -52,6 +58,7 @@ export namespace QuickOpenOptions {
skipPrefix: 0,

showItemsWithoutHighlight: false,
password: false,

onClose: () => { /* no-op*/ },

Expand Down
12 changes: 11 additions & 1 deletion packages/git/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
"@theia/workspace": "^0.3.16",
"@types/diff": "^3.2.2",
"@types/fs-extra": "^4.0.2",
"@types/p-queue": "^2.3.1",
"diff": "^3.4.0",
"dugite-extra": "0.1.7",
"dugite-extra": "0.1.9",
"find-git-repositories": "^0.1.0",
"fs-extra": "^4.0.2",
"moment": "^2.21.0",
"octicons": "^7.1.0",
"p-queue": "^2.4.2",
"ts-md5": "^1.2.2"
},
"publishConfig": {
Expand All @@ -26,6 +28,14 @@
{
"frontend": "lib/browser/git-frontend-module",
"backend": "lib/node/git-backend-module"
},
{
"backend": "lib/node/env/git-env-module",
"backendElectron": "lib/electron-node/env/electron-git-env-module"
},
{
"frontend": "lib/browser/prompt/git-prompt-module",
"frontendElectron": "lib/electron-browser/prompt/electron-git-prompt-module"
}
],
"keywords": [
Expand Down
2 changes: 2 additions & 0 deletions packages/git/src/browser/git-view-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { GitWidget } from './git-widget';
import { GitRepositoryTracker } from './git-repository-tracker';
import { GitQuickOpenService } from './git-quick-open-service';
import { GitSyncService } from './git-sync-service';
import { GitPrompt } from '../common/git-prompt';

export const GIT_WIDGET_FACTORY_ID = 'git';

Expand Down Expand Up @@ -95,6 +96,7 @@ export class GitViewContribution extends AbstractViewContribution<GitWidget> imp
@inject(GitQuickOpenService) protected readonly quickOpenService: GitQuickOpenService;
@inject(GitRepositoryTracker) protected readonly repositoryTracker: GitRepositoryTracker;
@inject(GitSyncService) protected readonly syncService: GitSyncService;
@inject(GitPrompt) protected readonly prompt: GitPrompt;

constructor() {
super({
Expand Down
29 changes: 29 additions & 0 deletions packages/git/src/browser/prompt/git-prompt-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/********************************************************************************
* Copyright (C) 2018 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { ContainerModule, interfaces } from 'inversify';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import { GitPrompt, GitPromptServer, GitPromptServerProxy, GitPromptServerImpl } from '../../common/git-prompt';

export default new ContainerModule(bind => {
bind(GitPrompt).toSelf();
bindPromptServer(bind);
});

export function bindPromptServer(bind: interfaces.Bind): void {
bind(GitPromptServer).to(GitPromptServerImpl).inSingletonScope();
bind(GitPromptServerProxy).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, GitPrompt.WS_PATH)).inSingletonScope();
}
174 changes: 174 additions & 0 deletions packages/git/src/common/git-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/********************************************************************************
* Copyright (C) 2018 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { inject, injectable, postConstruct } from 'inversify';
import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';

export const GitPromptServer = Symbol('GitPromptServer');
export interface GitPromptServer extends JsonRpcServer<GitPromptClient> {
}

export const GitPromptServerProxy = Symbol('GitPromptServerProxy');
export interface GitPromptServerProxy extends JsonRpcProxy<GitPromptServer> {
}

@injectable()
export class GitPrompt implements GitPromptClient, Disposable {

@inject(GitPromptServer)
protected readonly server: GitPromptServer;

protected readonly toDispose = new DisposableCollection();

@postConstruct()
protected init(): void {
this.server.setClient(this);
}

dispose(): void {
this.toDispose.dispose();
}

async ask(question: GitPrompt.Question): Promise<GitPrompt.Answer> {
return GitPrompt.Failure.create('Interactive Git prompt is not supported in the browser.');
}

}

export namespace GitPrompt {

/**
* Unique WS endpoint path for the Git prompt service.
*/
export const WS_PATH = 'services/git-prompt';

export interface Question {
readonly text: string;
readonly details?: string;
readonly password?: boolean;
}

export interface Answer {
readonly type: Answer.Type;
}

export interface Success {
readonly type: Answer.Type.SUCCESS;
readonly result: string | boolean;
}

export namespace Success {

export function is(answer: Answer): answer is Success {
return answer.type === Answer.Type.SUCCESS
&& 'result' in answer
&& ((typeof (answer as Success).result) === 'string' || (typeof (answer as Success).result) === 'boolean');
}

export function create(result: string | boolean): Success {
return {
type: Answer.Type.SUCCESS,
result
};
}

}

export interface Cancel extends Answer {
readonly type: Answer.Type.CANCEL;
}

export namespace Cancel {

export function is(answer: Answer): answer is Cancel {
return answer.type === Answer.Type.CANCEL;
}

export function create(): Cancel {
return {
type: Answer.Type.CANCEL
};
}

}

export interface Failure extends Answer {
readonly type: Answer.Type.FAILURE;
readonly error: string | Error;
}

export namespace Failure {

export function is(answer: Answer): answer is Failure {
return answer.type === Answer.Type.FAILURE
&& 'error' in answer
&& ((typeof (answer as Failure).error) === 'string' || (answer as Failure).error instanceof Error);
}

export function create(error: string | Error): Failure {
return {
type: Answer.Type.FAILURE,
error
};
}

}

export namespace Answer {

export enum Type {

SUCCESS,
CANCEL,
FAILURE

}

}

}

export const GitPromptClient = Symbol('GitPromptClient');
export interface GitPromptClient {

ask(question: GitPrompt.Question): Promise<GitPrompt.Answer>;

// TODO: implement `confirm` with boolean return type.
// TODO: implement `select` with possible answers.

}

/**
* Note: This implementation is not reconnecting.
* Git prompting is not supported in the browser. In electron, there's no need to reconnect.
*/
@injectable()
export class GitPromptServerImpl implements GitPromptServer {

@inject(GitPromptServerProxy)
protected readonly proxy: GitPromptServerProxy;

setClient(client: GitPromptClient): void {
this.proxy.setClient(client);
}

dispose(): void {
this.proxy.dispose();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/********************************************************************************
* Copyright (C) 2018 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { ContainerModule } from 'inversify';
import { GitPrompt } from '../../common/git-prompt';
import { bindPromptServer } from '../../browser/prompt/git-prompt-module';
import { GitQuickOpenPrompt } from './git-quick-open-prompt';

export default new ContainerModule(bind => {
bind(GitQuickOpenPrompt).toSelf().inSingletonScope();
bind(GitPrompt).toService(GitQuickOpenPrompt);
bindPromptServer(bind);
});
72 changes: 72 additions & 0 deletions packages/git/src/electron-browser/prompt/git-quick-open-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/********************************************************************************
* Copyright (C) 2018 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { inject, injectable } from 'inversify';
import * as PQueue from 'p-queue';
import { QuickOpenItem, QuickOpenMode } from '@theia/core/lib/browser/quick-open/quick-open-model';
import { QuickOpenService } from '@theia/core/lib/browser/quick-open/quick-open-service';
import { GitPrompt } from '../../common/git-prompt';

@injectable()
export class GitQuickOpenPrompt extends GitPrompt {

@inject(QuickOpenService)
protected readonly quickOpenService: QuickOpenService;

protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });

async ask(question: GitPrompt.Question): Promise<GitPrompt.Answer> {
return this.queue.add(() => {
const { details, text, password } = question;
return new Promise<GitPrompt.Answer>(resolve => {
const model = {
onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void {
acceptor([
new QuickOpenItem({
label: details,
run: (mode: QuickOpenMode): boolean => {
if (mode !== QuickOpenMode.OPEN) {
return false;
}
resolve(GitPrompt.Success.create(lookFor));
return true;
}
})
]);
}
};
const options = {
onClose: (canceled: boolean): void => {
if (canceled) {
resolve(GitPrompt.Cancel.create());
}
},
placeholder: text,
password
};
this.quickOpenService.open(model, options);
});
});
}

dispose(): void {
if (!this.queue.isPaused) {
this.queue.pause();
}
this.queue.clear();
}

}
Loading

0 comments on commit 67fabac

Please sign in to comment.