Skip to content

Commit 2af00d8

Browse files
authored
Support variable scopes (#17)
1 parent 4025455 commit 2af00d8

File tree

3 files changed

+87
-32
lines changed

3 files changed

+87
-32
lines changed

src/authentication.ts

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import * as vscode from 'vscode';
77
import { v4 as uuid } from 'uuid';
8+
import fetch from 'node-fetch';
89
import Keychain from './common/keychain';
910
import GitpodServer from './gitpodServer';
1011
import Log from './common/logger';
@@ -30,6 +31,8 @@ export default class GitpodAuthenticationProvider extends Disposable implements
3031
private _gitpodServer: GitpodServer;
3132
private _keychain: Keychain;
3233

34+
private _serviceUrl: string;
35+
3336
private _sessionsPromise: Promise<vscode.AuthenticationSession[]>;
3437

3538
constructor(private readonly context: vscode.ExtensionContext, logger: Log, telemetry: TelemetryReporter) {
@@ -39,17 +42,19 @@ export default class GitpodAuthenticationProvider extends Disposable implements
3942
this._telemetry = telemetry;
4043

4144
const gitpodHost = vscode.workspace.getConfiguration('gitpod').get<string>('host')!;
42-
const serviceUrl = new URL(gitpodHost);
43-
this._gitpodServer = new GitpodServer(serviceUrl.toString(), this._logger);
44-
this._keychain = new Keychain(this.context, `gitpod.auth.${serviceUrl.hostname}`, this._logger);
45+
const gitpodHostUrl = new URL(gitpodHost);
46+
this._serviceUrl = gitpodHostUrl.toString().replace(/\/$/, '');
47+
this._gitpodServer = new GitpodServer(this._serviceUrl, this._logger);
48+
this._keychain = new Keychain(this.context, `gitpod.auth.${gitpodHostUrl.hostname}`, this._logger);
4549
this._logger.info(`Started authentication provider for ${gitpodHost}`);
4650
this._register(vscode.workspace.onDidChangeConfiguration(e => {
4751
if (e.affectsConfiguration('gitpod.host')) {
4852
const gitpodHost = vscode.workspace.getConfiguration('gitpod').get<string>('host')!;
49-
const serviceUrl = new URL(gitpodHost);
53+
const gitpodHostUrl = new URL(gitpodHost);
54+
this._serviceUrl = gitpodHostUrl.toString().replace(/\/$/, '');
5055
this._gitpodServer.dispose();
51-
this._gitpodServer = new GitpodServer(serviceUrl.toString(), this._logger);
52-
this._keychain = new Keychain(this.context, `gitpod.auth.${serviceUrl.hostname}`, this._logger);
56+
this._gitpodServer = new GitpodServer(this._serviceUrl, this._logger);
57+
this._keychain = new Keychain(this.context, `gitpod.auth.${gitpodHostUrl.hostname}`, this._logger);
5358
this._logger.info(`Started authentication provider for ${gitpodHost}`);
5459

5560
this.checkForUpdates();
@@ -70,16 +75,40 @@ export default class GitpodAuthenticationProvider extends Disposable implements
7075
async getSessions(scopes?: string[]): Promise<vscode.AuthenticationSession[]> {
7176
// For the Gitpod scope list, order doesn't matter so we immediately sort the scopes
7277
const sortedScopes = scopes?.sort() || [];
73-
this._logger.info(`Getting sessions for ${sortedScopes.length ? sortedScopes.join(',') : 'all scopes'}...`);
78+
const validScopes = await this.fetchValidScopes();
79+
const sortedFilteredScopes = sortedScopes.filter(s => !validScopes || validScopes.includes(s));
80+
this._logger.info(`Getting sessions for ${sortedScopes.length ? sortedScopes.join(',') : 'all scopes'}${sortedScopes.length !== sortedFilteredScopes.length ? `, but valid scopes are ${sortedFilteredScopes.join(',')}` : ''}...`);
81+
if (sortedScopes.length !== sortedFilteredScopes.length) {
82+
this._logger.warn(`But valid scopes are ${sortedFilteredScopes.join(',')}, returning session with only valid scopes...`);
83+
}
7484
const sessions = await this._sessionsPromise;
75-
const finalSessions = sortedScopes.length
76-
? sessions.filter(session => arrayEquals([...session.scopes].sort(), sortedScopes))
85+
const finalSessions = sortedFilteredScopes.length
86+
? sessions.filter(session => arrayEquals([...session.scopes].sort(), sortedFilteredScopes))
7787
: sessions;
7888

79-
this._logger.info(`Got ${finalSessions.length} sessions for ${sortedScopes?.join(',') ?? 'all scopes'}...`);
89+
this._logger.info(`Got ${finalSessions.length} sessions for ${sortedFilteredScopes?.join(',') ?? 'all scopes'}...`);
8090
return finalSessions;
8191
}
8292

93+
private _validScopes: string[] | undefined;
94+
private async fetchValidScopes(): Promise<string[] | undefined> {
95+
if (this._validScopes) {
96+
return this._validScopes;
97+
}
98+
99+
const endpoint = `${this._serviceUrl}/api/oauth/inspect?client=${vscode.env.uriScheme}-gitpod`;
100+
try {
101+
const resp = await fetch(endpoint, { timeout: 1500 });
102+
if (resp.ok) {
103+
this._validScopes = await resp.json();
104+
return this._validScopes;
105+
}
106+
} catch (e) {
107+
this._logger.error(`Error fetching endpoint ${endpoint}`, e);
108+
}
109+
return undefined;
110+
}
111+
83112
private async checkForUpdates() {
84113
const previousSessions = await this._sessionsPromise;
85114
this._sessionsPromise = this.readSessions();
@@ -187,18 +216,23 @@ export default class GitpodAuthenticationProvider extends Disposable implements
187216
try {
188217
// For the Gitpod scope list, order doesn't matter so we immediately sort the scopes
189218
const sortedScopes = scopes.sort();
219+
const validScopes = await this.fetchValidScopes();
220+
const sortedFilteredScopes = sortedScopes.filter(s => !validScopes || validScopes.includes(s));
221+
if (sortedScopes.length !== sortedFilteredScopes.length) {
222+
this._logger.warn(`Creating session with only valid scopes ${sortedFilteredScopes.join(',')}, original scopes were ${sortedScopes.join(',')}`);
223+
}
190224

191225
this._telemetry.sendRawTelemetryEvent('gitpod_desktop_auth', {
192226
kind: 'login',
193-
scopes: JSON.stringify(sortedScopes),
227+
scopes: JSON.stringify(sortedFilteredScopes),
194228
});
195229

196-
const scopeString = sortedScopes.join(' ');
230+
const scopeString = sortedFilteredScopes.join(' ');
197231
const token = await this._gitpodServer.login(scopeString);
198-
const session = await this.tokenToSession(token, sortedScopes);
232+
const session = await this.tokenToSession(token, sortedFilteredScopes);
199233

200234
const sessions = await this._sessionsPromise;
201-
const sessionIndex = sessions.findIndex(s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedScopes));
235+
const sessionIndex = sessions.findIndex(s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedFilteredScopes));
202236
if (sessionIndex > -1) {
203237
sessions.splice(sessionIndex, 1, session);
204238
} else {

src/featureSupport.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Copyright (c) Gitpod. All rights reserved.
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
5+
import * as vscode from 'vscode';
56
import * as semver from 'semver';
67
import fetch from 'node-fetch';
78
import Log from './common/logger';
@@ -50,9 +51,9 @@ async function getOrFetchVersionInfo(serviceUrl: string, logger: Log) {
5051
return cacheGitpodVersion;
5152
}
5253

54+
const versionEndPoint = `${serviceUrl}/api/version`;
5355
let gitpodRawVersion: string | undefined;
5456
try {
55-
const versionEndPoint = `${serviceUrl}/api/version`;
5657
gitpodRawVersion = await retry(async () => {
5758
const resp = await fetch(versionEndPoint, { timeout: 1500 });
5859
if (!resp.ok) {
@@ -61,11 +62,11 @@ async function getOrFetchVersionInfo(serviceUrl: string, logger: Log) {
6162
return resp.text();
6263
}, 1000, 3);
6364
} catch (e) {
64-
logger.error(`Error while fetching ${serviceUrl}`, e);
65+
logger.error(`Error while fetching ${versionEndPoint}`, e);
6566
}
6667

6768
if (!gitpodRawVersion) {
68-
logger.info(`Failed to fetch version from ${serviceUrl}, some feature will be disabled`);
69+
logger.info(`Failed to fetch version from ${versionEndPoint}, some feature will be disabled`);
6970
return {
7071
host: serviceUrl,
7172
version: GitpodVersion.Min,
@@ -98,3 +99,22 @@ export function isFeatureSupported(gitpodVersion: GitpodVersion, feature: Featur
9899
return semver.gte(gitpodVersion.version, '2022.7.0'); // Don't use leading zeros
99100
}
100101
}
102+
103+
export async function isOauthInspectSupported(gitpodHost: string,) {
104+
const serviceUrl = new URL(gitpodHost).toString().replace(/\/$/, '');
105+
const endpoint = `${serviceUrl}/api/oauth/inspect?client=${vscode.env.uriScheme}-gitpod`;
106+
try {
107+
const resp = await fetch(endpoint, { timeout: 1500 });
108+
if (resp.ok) {
109+
return true;
110+
}
111+
} catch {
112+
}
113+
114+
return false;
115+
}
116+
117+
export enum ScopeFeature {
118+
SSHPublicKeys = 'function:getSSHPublicKeys',
119+
LocalHeartbeat = 'function:sendHeartBeat'
120+
}

src/remoteConnector.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import TelemetryReporter from './telemetryReporter';
2525
import { addHostToHostFile, checkNewHostInHostkeys } from './ssh/hostfile';
2626
import { DEFAULT_IDENTITY_FILES } from './ssh/identityFiles';
2727
import { HeartbeatManager } from './heartbeat';
28-
import { getGitpodVersion, GitpodVersion, isFeatureSupported } from './featureSupport';
28+
import { getGitpodVersion, GitpodVersion, isFeatureSupported, isOauthInspectSupported, ScopeFeature } from './featureSupport';
2929
import SSHConfiguration from './ssh/sshConfig';
3030
import { isWindows } from './common/platform';
3131
import { untildify } from './common/files';
@@ -523,14 +523,13 @@ export default class RemoteConnector extends Disposable {
523523
return preferredIdentityKeys;
524524
}
525525

526-
private async getWorkspaceSSHDestination(accessToken: string, { workspaceId, gitpodHost }: SSHConnectionParams): Promise<{ destination: string; password?: string }> {
526+
private async getWorkspaceSSHDestination(session: vscode.AuthenticationSession, { workspaceId, gitpodHost }: SSHConnectionParams): Promise<{ destination: string; password?: string }> {
527527
const serviceUrl = new URL(gitpodHost);
528-
const gitpodVersion = await getGitpodVersion(gitpodHost, this.logger);
529-
530-
const [workspaceInfo, ownerToken, registeredSSHKeys] = await withServerApi(accessToken, serviceUrl.toString(), service => Promise.all([
528+
const sshKeysSupported = session.scopes.includes(ScopeFeature.SSHPublicKeys);
529+
const [workspaceInfo, ownerToken, registeredSSHKeys] = await withServerApi(session.accessToken, serviceUrl.toString(), service => Promise.all([
531530
service.server.getWorkspace(workspaceId),
532531
service.server.getOwnerToken(workspaceId),
533-
isFeatureSupported(gitpodVersion, 'SSHPublicKeys') ? service.server.getSSHPublicKeys() : undefined
532+
sshKeysSupported ? service.server.getSSHPublicKeys() : undefined
534533
]), this.logger);
535534

536535
if (workspaceInfo.latestInstance?.status?.phase !== 'running') {
@@ -612,7 +611,8 @@ export default class RemoteConnector extends Disposable {
612611
if (identityKeys.length) {
613612
sshDestInfo.user = `${workspaceId}#${ownerToken}`;
614613
}
615-
this.logger.warn(`Registered SSH public keys not supported in ${gitpodHost}, using version ${gitpodVersion.version}`);
614+
const gitpodVersion = await getGitpodVersion(gitpodHost, this.logger);
615+
this.logger.warn(`Registered SSH public keys not supported in ${gitpodHost}, using version ${gitpodVersion.raw}`);
616616
}
617617

618618
return {
@@ -699,11 +699,11 @@ export default class RemoteConnector extends Disposable {
699699
return true;
700700
}
701701

702-
private async showSSHPasswordModal(password: string, sshParams: SSHConnectionParams) {
702+
private async showSSHPasswordModal(password: string, session: vscode.AuthenticationSession,sshParams: SSHConnectionParams) {
703703
const maskedPassword = '•'.repeat(password.length - 3) + password.substring(password.length - 3);
704704

705+
const sshKeysSupported = session.scopes.includes(ScopeFeature.SSHPublicKeys);
705706
const gitpodVersion = await getGitpodVersion(sshParams.gitpodHost, this.logger);
706-
const sshKeysSupported = isFeatureSupported(gitpodVersion, 'SSHPublicKeys');
707707

708708
const copy: vscode.MessageItem = { title: 'Copy' };
709709
const configureSSH: vscode.MessageItem = { title: 'Configure SSH' };
@@ -752,10 +752,10 @@ export default class RemoteConnector extends Disposable {
752752

753753
const gitpodVersion = await getGitpodVersion(gitpodHost, this.logger);
754754
const sessionScopes = ['function:getWorkspace', 'function:getOwnerToken', 'function:getLoggedInUser', 'resource:default'];
755-
if (isFeatureSupported(gitpodVersion, 'SSHPublicKeys') /* && isFeatureSupported('', 'sendHeartBeat') */) {
755+
if (await isOauthInspectSupported(gitpodHost) || isFeatureSupported(gitpodVersion, 'SSHPublicKeys') /* && isFeatureSupported('', 'sendHeartBeat') */) {
756756
sessionScopes.push('function:getSSHPublicKeys', 'function:sendHeartBeat');
757757
} else {
758-
this.logger.warn(`function:getSSHPublicKeys and function:sendHeartBeat session scopes not supported in ${gitpodHost}, using version ${gitpodVersion.version}`);
758+
this.logger.warn(`function:getSSHPublicKeys and function:sendHeartBeat session scopes not supported in ${gitpodHost}, using version ${gitpodVersion.raw}`);
759759
}
760760

761761
return vscode.authentication.getSession(
@@ -797,11 +797,11 @@ export default class RemoteConnector extends Disposable {
797797
try {
798798
this.telemetry.sendRawTelemetryEvent('vscode_desktop_ssh', { kind: 'gateway', status: 'connecting', ...params, gitpodVersion: gitpodVersion.raw, userOverride, openSSHVersion });
799799

800-
const { destination, password } = await this.getWorkspaceSSHDestination(session.accessToken, params);
800+
const { destination, password } = await this.getWorkspaceSSHDestination(session, params);
801801
sshDestination = destination;
802802

803803
if (password) {
804-
await this.showSSHPasswordModal(password, params);
804+
await this.showSSHPasswordModal(password, session, params);
805805
}
806806

807807
this.telemetry.sendRawTelemetryEvent('vscode_desktop_ssh', { kind: 'gateway', status: 'connected', ...params, gitpodVersion: gitpodVersion.raw, auth: password ? 'password' : 'key', userOverride, openSSHVersion });
@@ -1039,11 +1039,12 @@ export default class RemoteConnector extends Disposable {
10391039

10401040
await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`, { ...connectionInfo, isFirstConnection: false });
10411041

1042+
const heartbeatSupported = session.scopes.includes(ScopeFeature.LocalHeartbeat);
10421043
const gitpodVersion = await getGitpodVersion(connectionInfo.gitpodHost, this.logger);
1043-
if (isFeatureSupported(gitpodVersion, 'localHeartbeat')) {
1044+
if (heartbeatSupported) {
10441045
this.startHeartBeat(session.accessToken, connectionInfo, gitpodVersion);
10451046
} else {
1046-
this.logger.warn(`Local heatbeat not supported in ${connectionInfo.gitpodHost}, using version ${gitpodVersion.version}`);
1047+
this.logger.warn(`Local heartbeat not supported in ${connectionInfo.gitpodHost}, using version ${gitpodVersion.raw}`);
10471048
}
10481049

10491050
const syncExtensions = vscode.workspace.getConfiguration('gitpod').get<boolean>('remote.syncExtensions')!;

0 commit comments

Comments
 (0)