Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions code/extensions/che-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@
"supported": true
}
},
"contributes": {
"commands": [
{
"command": "che-api.test-github-proxy",
"title": "Test GitHub Proxy (axios)"
}
]
},
"main": "./out/extension.js",
"scripts": {
"compile": "gulp compile-extension:che-api",
Expand Down
10 changes: 0 additions & 10 deletions code/extensions/che-api/src/api/github-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,6 @@ export const GithubService = Symbol('GithubService');
export interface GithubService {
getToken(): Promise<string>;

/**
* Persists the given token to the corresponding secret, the in-memory value will be updated as well.
* A new secret will be created if there is no secret yet.
* Note: The existing token will be owerriten by the given one.
*/
persistDeviceAuthToken(token: string): Promise<void>;

/* Removes Device Authentication secret, extracts a token from another source (.git-credentials/credentials file or git-credentials secret) */
removeDeviceAuthToken(): Promise<void>;

getUser(): Promise<GithubUser>;
getTokenScopes(token: string): Promise<string[]>;
}
11 changes: 11 additions & 0 deletions code/extensions/che-api/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,16 @@ export async function activate(_extensionContext: vscode.ExtensionContext): Prom

await container.get(K8SServiceImpl).ensureKubernetesServiceHostWhitelisted();

_extensionContext.subscriptions.push(
vscode.commands.registerCommand('che-api.test-github-proxy', async () => {
try {
const user = await githubService.getUser();
vscode.window.showInformationMessage(`Success! GitHub user: ${user.login}`);
} catch (e: any) {
vscode.window.showErrorMessage(`Failed to get GitHub user: ${e.message}`);
}
})
);

return api;
}
119 changes: 9 additions & 110 deletions code/extensions/che-api/src/impl/github-service-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,22 @@

/* eslint-disable header/header */

import * as k8s from '@kubernetes/client-node';
import { AxiosInstance } from 'axios';
import * as fs from 'fs-extra';
import { inject, injectable } from 'inversify';
import * as path from 'path';

import { GithubService, GithubUser } from '../api/github-service';
import { Logger } from '../logger';
import { K8SServiceImpl } from './k8s-service-impl';
import { base64Decode, base64Encode, createLabelsSelector, randomString } from './utils';

const GIT_CREDENTIALS_PATH = path.resolve('/.git-credentials', 'credentials');
const GIT_CREDENTIAL_LABEL = {
'controller.devfile.io/git-credential': 'true'
};
const DEVICE_AUTHENTICATION_LABEL = {
'che.eclipse.org/device-authentication': 'true'
}
const DEVICE_AUTHENTICATION_LABEL_SELECTOR: string = createLabelsSelector(DEVICE_AUTHENTICATION_LABEL);
const SCM_URL_ATTRIBUTE = 'che.eclipse.org/scm-url';
const GITHUB_URL = 'https://github.com';
const GIT_CREDENTIALS_LABEL_SELECTOR: string = createLabelsSelector(GIT_CREDENTIAL_LABEL);

@injectable()
export class GithubServiceImpl implements GithubService {
private token: string | undefined;

constructor(
@inject(Logger) private logger: Logger,
@inject(K8SServiceImpl) private readonly k8sService: K8SServiceImpl,
@inject(Symbol.for('AxiosInstance')) private readonly axiosInstance: AxiosInstance
) {
this.initializeToken();
Expand Down Expand Up @@ -72,57 +58,17 @@ export class GithubServiceImpl implements GithubService {
return result.headers['x-oauth-scopes'].split(', ');
}

async persistDeviceAuthToken(token: string): Promise<void> {
this.token = token;
this.logger.info(`Github Service: adding token to the device-authentication secret...`);

const deviceAuthSecrets = await this.k8sService.getSecret(DEVICE_AUTHENTICATION_LABEL_SELECTOR);
if (deviceAuthSecrets.length < 1) {
this.logger.info(`Github Service: device-authentication secret not found, creating a new secret...`);

const namespace = this.k8sService.getDevWorkspaceNamespace();
const newSecret = toDeviceAuthSecret(token, namespace);
await this.k8sService.createNamespacedSecret(newSecret);
private async initializeToken(): Promise<void> {
this.logger.info('Github Service: extracting token...');

this.logger.info(`Github Service: device-authentication secret was created successfully!`);
if (process.env.GITHUB_TEST_TOKEN) {
this.token = process.env.GITHUB_TEST_TOKEN;
this.logger.info('Github Service: using token from GITHUB_TEST_TOKEN env variable');
return;
} else {
this.logger.info('Github Service: NO token from GITHUB_TEST_TOKEN env variable');
}

const deviceAuthSecret = deviceAuthSecrets[0];
this.logger.info(`Github Service: updating exsting device-authentication secret...`);

const data = {
token: base64Encode(`${token}`)
};

const updatedSecret = { ...deviceAuthSecret, data };
const name = deviceAuthSecret.metadata?.name || `device-authentication-secret-${randomString(5).toLowerCase()}`;
this.k8sService.replaceNamespacedSecret(name, updatedSecret);

this.logger.info(`Github Service: device-authentication secret was updated successfully!`);
}

async removeDeviceAuthToken(): Promise<void> {
this.logger.info(`Github Service: got request for removing a device-authentication secret`);
const deviceAuthSecrets = await this.k8sService.getSecret(DEVICE_AUTHENTICATION_LABEL_SELECTOR);
if (deviceAuthSecrets.length < 1) {
this.logger.warn('Github Service: device-authentication secret not found');
throw new Error('device-authentication secret not found');
}

for (const secret of deviceAuthSecrets) {
this.logger.info(`Github Service: removing device-authentication secret with ${secret.metadata?.name} name...`);
await this.k8sService.deleteNamespacedSecret(secret);
this.logger.info(`Github Service: device-authentication secret with ${secret.metadata?.name} name was deleted successfully!`);
}

// another token should be used by the Github Service after removing the Device Authentication token
this.initializeToken();
}

private async initializeToken(): Promise<void> {
this.logger.info('Github Service: extracting token...');

const deviceAuthToken = await this.getDeviceAuthToken();
if (deviceAuthToken) {
this.token = deviceAuthToken;
Expand All @@ -136,19 +82,11 @@ export class GithubServiceImpl implements GithubService {
this.logger.info('Github Service: git-credential token is used');
return;
}
this.token = await this.getTokenFromSecret();
}

/* Extracts a token from the device-authentication secret */
private async getDeviceAuthToken(): Promise<string | undefined> {
const deviceAuthSecrets = await this.k8sService.getSecret(DEVICE_AUTHENTICATION_LABEL_SELECTOR);
this.logger.info(`Github Service: found ${deviceAuthSecrets.length} device-authentication secrets`);
if (deviceAuthSecrets.length > 0) {
const decodedToken = base64Decode(deviceAuthSecrets[0].data!.token);
return decodedToken;
} else {
return undefined;
}
private async getDeviceAuthToken(): Promise<undefined> {
return undefined;
}

/* Extracts tokens from the .git-credentials/credentials file */
Expand All @@ -169,43 +107,4 @@ export class GithubServiceImpl implements GithubService {
this.logger.info(`Github Service: found ${tokens.length} tokens in the ${GIT_CREDENTIALS_PATH} file`);
return tokens;
}

/* Extracts token from the git-credential secret */
private async getTokenFromSecret(): Promise<string | undefined> {
this.logger.info(`Github Service: looking for the corresponding git-credentials secret to get token...`);

const gitCredentialSecrets = await this.k8sService.getSecret(GIT_CREDENTIALS_LABEL_SELECTOR);
if (gitCredentialSecrets.length === 0) {
this.logger.warn('Github Service: token is not found');
return undefined;
}

const githubSecrets = gitCredentialSecrets.filter(secret => secret.metadata?.annotations?.[SCM_URL_ATTRIBUTE] === GITHUB_URL);
this.logger.info(`Github Service: found ${githubSecrets.length} github secrets`);

const credentials = githubSecrets.length > 0 ? githubSecrets[0].data!.credentials : gitCredentialSecrets[0].data!.credentials;
const decodedCredentials = base64Decode(credentials);
const decodedToken = decodedCredentials.substring(decodedCredentials.lastIndexOf(':') + 1, decodedCredentials.indexOf('@'));
this.logger.info('Github Service: a token from the git-credential secret is used');

return decodedToken;
}
}

function toDeviceAuthSecret(token: string, namespace: string): k8s.V1Secret {
return {
apiVersion: 'v1',
kind: 'Secret',
metadata: {
name: `device-authentication-secret-${randomString(5).toLowerCase()}`,
namespace,
labels: {
'che.eclipse.org/device-authentication': 'true'
}
},
type: 'Opaque',
data: {
token: base64Encode(`${token}`)
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,22 @@
import { inject, injectable } from 'inversify';
import * as vscode from 'vscode';
import { ExtensionContext } from './extension-context';
import { GitHubAuthProvider, GithubService } from './github';
import { GitHubAuthProvider } from './github';
import { CHANNEL_NAME, Logger } from './logger';

@injectable()
export class DeviceAuthentication {
constructor(
@inject(Logger) private logger: Logger,
@inject(ExtensionContext) private extensionContext: ExtensionContext,
@inject(GitHubAuthProvider) private gitHubAuthProvider: GitHubAuthProvider,
@inject(Symbol.for('GithubServiceInstance')) private githubService: GithubService
@inject(GitHubAuthProvider) private gitHubAuthProvider: GitHubAuthProvider
) {
this.extensionContext.getContext().subscriptions.push(
vscode.commands.registerCommand('github-authentication.device-code-flow.authentication', async () => this.trigger()),
);
this.logger.info('Device Authentication command has been registered');

this.extensionContext.getContext().subscriptions.push(
vscode.commands.registerCommand('github-authentication.device-code-flow.remove-token', async () => this.removeDeviceAuthToken()),
);

this.logger.info('Remove Device Authentication Token command has been registered');
}

Expand All @@ -58,7 +55,6 @@ export class DeviceAuthentication {
this.logger.info(`Device Authentication: token for scopes: ${scopes} has been generated successfully`);

try {
await this.githubService.persistDeviceAuthToken(token);
await this.gitHubAuthProvider.createSession([scopes]);
this.onTokenGenerated(scopes);

Expand All @@ -73,18 +69,6 @@ export class DeviceAuthentication {
}
}

async removeDeviceAuthToken(): Promise<void> {
try {
await this.githubService.removeDeviceAuthToken();
const message = 'The token was deleted successfully. Some operations may require Github Sign Out => Sign In to use another token.'
vscode.window.showInformationMessage(message);
} catch (error) {
const message = `Can not remove Device Authentication token: ${error.message}`;
vscode.window.showErrorMessage(message);
}

}

private async onTokenGenerated(scopes: string): Promise<void> {
const message = `A new session has been created for ${scopes} scopes. Please reload window to apply it.`
const reloadNow = vscode.l10n.t('Reload Now');
Expand Down
2 changes: 0 additions & 2 deletions code/extensions/che-github-authentication/src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ export interface GithubUser {

export interface GithubService {
getToken(): Promise<string>;
persistDeviceAuthToken(token: string): Promise<void>;
removeDeviceAuthToken(): Promise<void>;
getUser(): Promise<GithubUser>;
getTokenScopes(token: string): Promise<string[]>;
}
Expand Down
Loading