Skip to content

Commit

Permalink
Support flexible auth user (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeanp413 authored Oct 9, 2023
1 parent 19d1779 commit 49fbaf6
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 731 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@
"@gitpod/gitpod-protocol": "main-gha",
"@gitpod/local-app-api-grpcweb": "main-gha",
"@gitpod/public-api": "main-gha",
"@gitpod/supervisor-api-grpc": "main-gha",
"@gitpod/supervisor-api-grpcweb": "main-gha",
"@improbable-eng/grpc-web-node-http-transport": "^0.14.0",
"@microsoft/dev-tunnels-ssh": "^3.11.24",
"@microsoft/dev-tunnels-ssh-keys": "^3.11.24",
Expand Down
51 changes: 9 additions & 42 deletions src/local-ssh/ipc/extensionServiceServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ExtensionServiceDefinition, ExtensionServiceImplementation, GetWorkspaceAuthInfoRequest, GetWorkspaceAuthInfoResponse, PingRequest, SendErrorReportRequest, SendLocalSSHUserFlowStatusRequest } from '../../proto/typescript/ipc/v1/ipc';
import { ExtensionServiceDefinition, ExtensionServiceImplementation, GetWorkspaceAuthInfoRequest, GetWorkspaceAuthInfoResponse, PingRequest } from '../../proto/typescript/ipc/v1/ipc';
import { Disposable } from '../../common/dispose';
import { ILogService } from '../../services/logService';
import { ISessionService } from '../../services/sessionService';
import { CallContext, ServerError, Status } from 'nice-grpc-common';
import { IHostService } from '../../services/hostService';
import { Server, createChannel, createClient, createServer } from 'nice-grpc';
import { ITelemetryService, UserFlowTelemetryProperties } from '../../common/telemetry';
import { ITelemetryService } from '../../common/telemetry';
import { Configuration } from '../../configuration';
import { timeout } from '../../common/async';
import { BrowserHeaders } from 'browser-headers';
import { ControlServiceClient, ServiceError } from '@gitpod/supervisor-api-grpcweb/lib/control_pb_service';
import { NodeHttpTransport } from '@improbable-eng/grpc-web-node-http-transport';
import { CreateSSHKeyPairRequest } from '@gitpod/supervisor-api-grpcweb/lib/control_pb';
import { CreateSSHKeyPairRequest, CreateSSHKeyPairResponse } from '@gitpod/supervisor-api-grpcweb/lib/control_pb';
import * as ssh2 from 'ssh2';
import { ParsedKey } from 'ssh2-streams';
import { WrapError } from '../../common/utils';
Expand Down Expand Up @@ -66,15 +66,15 @@ class ExtensionServiceImpl implements ExtensionServiceImplementation {
private async getWorkspaceSSHKey(ownerToken: string, workspaceUrl: string, signal: AbortSignal) {
const url = new URL(workspaceUrl);
url.pathname = '/_supervisor/v1';
const privateKey = await wrapSupervisorAPIError(() => new Promise<string>((resolve, reject) => {
const { privateKey, userName } = await wrapSupervisorAPIError(() => new Promise<CreateSSHKeyPairResponse.AsObject>((resolve, reject) => {
const metadata = new BrowserHeaders();
metadata.append('x-gitpod-owner-token', ownerToken);
const client = new ControlServiceClient(url.toString(), { transport: NodeHttpTransport() });
client.createSSHKeyPair(new CreateSSHKeyPairRequest(), metadata, (err, resp) => {
if (err) {
return reject(err);
}
resolve(resp!.toObject().privateKey);
resolve(resp!.toObject());
});
}), { signal });

Expand All @@ -83,7 +83,7 @@ class ExtensionServiceImpl implements ExtensionServiceImplementation {
throw new Error('Error while parsing workspace SSH private key');
}

return (parsedResult as ParsedKey).getPrivatePEM();
return { sshkey: (parsedResult as ParsedKey).getPrivatePEM(), username: userName };
}

async getWorkspaceAuthInfo(request: GetWorkspaceAuthInfoRequest, _context: CallContext): Promise<GetWorkspaceAuthInfoResponse> {
Expand Down Expand Up @@ -126,6 +126,7 @@ class ExtensionServiceImpl implements ExtensionServiceImplementation {
let ownerToken = '';
let workspaceHost = '';
let sshkey = '';
let username = '';
if (wsData.phase === 'running') {
ownerToken = await this.sessionService.getAPI().getOwnerToken(actualWorkspaceId, _context.signal);

Expand All @@ -137,7 +138,7 @@ class ExtensionServiceImpl implements ExtensionServiceImplementation {
actualWorkspaceUrl = actualWorkspaceUrl.replace(actualWorkspaceId, workspaceId);
}

sshkey = await this.getWorkspaceSSHKey(ownerToken, actualWorkspaceUrl, _context.signal);
({ sshkey, username } = await this.getWorkspaceSSHKey(ownerToken, actualWorkspaceUrl, _context.signal));
}

return {
Expand All @@ -149,6 +150,7 @@ class ExtensionServiceImpl implements ExtensionServiceImplementation {
ownerToken,
sshkey,
phase: wsData.phase,
username,
};
} catch (e) {
let code = Status.INTERNAL;
Expand All @@ -169,41 +171,6 @@ class ExtensionServiceImpl implements ExtensionServiceImplementation {
}
}

// TODO remove from protocol, don't pass sensitive info back and forth, only once for auth, daemon should do telemetry directly
async sendLocalSSHUserFlowStatus(request: SendLocalSSHUserFlowStatusRequest, _context: CallContext): Promise<{}> {
if (!request.flowStatus || request.flowStatus === '') {
return {};
}
const flow: UserFlowTelemetryProperties = {
flow: 'local_ssh',
workspaceId: request.workspaceId,
instanceId: request.instanceId,
daemonVersion: request.daemonVersion,
userId: request.userId,
gitpodHost: request.gitpodHost,
failureCode: request.flowFailureCode,
};
this.telemetryService.sendUserFlowStatus(request.flowStatus, flow);
return {};
}

// TODO remove from protocol, don't pass sensitive info back and forth, only once for auth, daemon should do telemetry directly
// local ssh daemon should be own component in reporting?
async sendErrorReport(request: SendErrorReportRequest, _context: CallContext): Promise<{}> {
const err = new Error(request.errorMessage);
err.name = `${request.errorName}[local-ssh]`;
err.stack = request.errorStack;
this.telemetryService.sendTelemetryException(err, {
gitpodHost: request.gitpodHost,
workspaceId: request.workspaceId,
instanceId: request.instanceId,
daemonVersion: request.daemonVersion,
extensionVersion: request.extensionVersion,
userId: request.userId,
});
return {};
}

async ping(_request: PingRequest, _context: CallContext): Promise<{}> {
return {};
}
Expand Down
2 changes: 1 addition & 1 deletion src/local-ssh/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ class WebSocketSSHProxy {

await session.connect(stream);

const ok = await session.authenticate({ username: 'gitpod', publicKeys: [await importKey(workspaceInfo.sshkey)] });
const ok = await session.authenticate({ username: workspaceInfo.username || 'gitpod', publicKeys: [await importKey(workspaceInfo.sshkey)] });
if (!ok) {
throw new FailedToProxyError('TUNNEL.AuthenticateSSHKeyFailed');
}
Expand Down
65 changes: 1 addition & 64 deletions src/proto/ipc/v1/ipc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,76 +4,12 @@ package ipc.v1;

service ExtensionService {
rpc GetWorkspaceAuthInfo (GetWorkspaceAuthInfoRequest) returns (GetWorkspaceAuthInfoResponse) {}
rpc SendLocalSSHUserFlowStatus (SendLocalSSHUserFlowStatusRequest) returns (SendLocalSSHUserFlowStatusResponse) {}
rpc SendErrorReport (SendErrorReportRequest) returns (SendErrorReportResponse) {}
rpc Ping (PingRequest) returns (PingResponse) {}
}

message PingRequest {}
message PingResponse {}

message SendErrorReportRequest {
string workspace_id = 1;
string instance_id = 2;
string error_name = 3;
string error_message = 4;
string error_stack = 5;
string daemon_version = 6;
string extension_version = 7;
string gitpod_host = 8;
string user_id = 9;
}

message SendErrorReportResponse {}

message SendLocalSSHUserFlowStatusRequest {

enum ConnType {
CONN_TYPE_UNSPECIFIED = 0;
CONN_TYPE_SSH = 1;
CONN_TYPE_TUNNEL = 2;
}

enum Status {
STATUS_UNSPECIFIED = 0;
STATUS_SUCCESS = 1;
STATUS_FAILURE = 2;
}

enum Code {
CODE_UNSPECIFIED = 0;
// CODE_NO_WORKSPACE_AUTO_INFO is used if failed to get workspace auto info
CODE_NO_WORKSPACE_AUTO_INFO = 1;
// CODE_TUNNEL_CANNOT_CREATE_WEBSOCKET is used if failed to create websocket to supervisor
CODE_TUNNEL_CANNOT_CREATE_WEBSOCKET = 2;
// CODE_TUNNEL_FAILED_FORWARD_SSH_PORT is used if failed to forward ssh port in supervisor
CODE_TUNNEL_FAILED_FORWARD_SSH_PORT = 3;
// CODE_TUNNEL_NO_PRIVATEKEY when failed to create private key in supervisor
CODE_TUNNEL_NO_PRIVATEKEY = 4;
// CODE_TUNNEL_NO_ESTABLISHED_CONNECTION is used if the tunnel is not established
CODE_TUNNEL_NO_ESTABLISHED_CONNECTION = 5;
// CODE_SSH_CANNOT_CONNECT is used if failed to direct connect to ssh gateway
CODE_SSH_CANNOT_CONNECT = 6;
}

Status status = 1;
string workspace_id = 2;
string instance_id = 3;
Code failure_code = 5;
// DEPRECATED string failure_reason = 6
reserved 6;
string daemon_version = 7;
// DEPRECATED string extension_version = 8
reserved 8;
ConnType conn_type = 9;
string gitpod_host = 10;
string user_id = 11;
string flow_status = 12;
string flow_failure_code = 13;
}

message SendLocalSSHUserFlowStatusResponse {}

message GetWorkspaceAuthInfoRequest {
string workspace_id = 1;
string gitpod_host = 2;
Expand All @@ -88,4 +24,5 @@ message GetWorkspaceAuthInfoResponse {
string user_id = 6;
string sshkey = 7;
string phase = 8;
string username = 9;
}
Loading

0 comments on commit 49fbaf6

Please sign in to comment.