Skip to content
This repository has been archived by the owner on Apr 4, 2023. It is now read-only.

Commit

Permalink
Store SSH private key on the file system from SSH plugin (#360)
Browse files Browse the repository at this point in the history
Signed-off-by: Igor Vinokur <ivinokur@redhat.com>
  • Loading branch information
vinokurig authored Jul 22, 2019
1 parent 61ed6e9 commit 94223d4
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 79 deletions.
3 changes: 3 additions & 0 deletions plugins/ssh-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
"files": [
"src"
],
"dependencies": {
"fs-extra": "7.0.1"
},
"devDependencies": {
"@eclipse-che/api": "latest",
"@eclipse-che/plugin": "0.0.1",
Expand Down
158 changes: 88 additions & 70 deletions plugins/ssh-plugin/src/ssh-plugin-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,18 @@
**********************************************************************/

import * as theia from '@theia/plugin';
import { RemoteSshKeyManager, SshKeyManager, CheService } from './node/ssh-key-manager';
import { RemoteSshKeyManager, SshKeyManager } from './node/ssh-key-manager';
import { che as cheApi } from '@eclipse-che/api';
import { resolve } from 'path';
import { pathExists, unlink, ensureFile, chmod, readFile, writeFile, appendFile } from 'fs-extra';
import * as os from 'os';

export async function start() {
const sshKeyManager = new RemoteSshKeyManager();
const GENERATE_FOR_HOST: theia.CommandDescription = {
id: 'ssh:generate_for_host',
label: 'SSH: generate key pair for particular host...'
};
const GENERATE: theia.CommandDescription = {
id: 'ssh:generate',
label: 'SSH: generate key pair...'
Expand All @@ -31,6 +38,9 @@ export async function start() {
label: 'SSH: view public key...'
};

theia.commands.registerCommand(GENERATE_FOR_HOST, () => {
generateKeyPairForHost(sshKeyManager);
});
theia.commands.registerCommand(GENERATE, () => {
generateKeyPair(sshKeyManager);
});
Expand All @@ -45,97 +55,105 @@ export async function start() {
});
}

const getSshServiceName = async function (): Promise<string> {
/**
* Known Che services which can use the SSH key pairs.
*/
const services: CheService[] = [
{
name: 'vcs',
displayName: 'VCS',
description: 'SSH keys used by Che VCS plugins'
},
{
name: 'machine',
displayName: 'Workspace Containers',
description: 'SSH keys injected into all Workspace Containers'
}
];
const getHostName = async () => {
const hostName = await theia.window.showInputBox({ placeHolder: 'Please provide a Host name e.g. github.com' });
return hostName ? hostName : '';
};

const option: theia.QuickPickOptions = {
matchOnDescription: true,
matchOnDetail: true,
canPickMany: false,
placeHolder: 'Select object:'
};
const sshServiceValue = await theia.window.showQuickPick<theia.QuickPickItem>(services.map(service =>
({
label: service.displayName,
description: service.description,
detail: service.name,
name: service.name
})), option);
if (sshServiceValue) {
return Promise.resolve(sshServiceValue.label);
const getKeyFilePath = (name: string) => resolve(os.homedir(), '.ssh', name.replace(new RegExp('\\.'), '_'));

const updateConfig = async (hostName: string) => {
const configFile = resolve(os.homedir(), '.ssh', 'config');
await ensureFile(configFile);
await chmod(configFile, '644');
const keyConfig = `\nHost ${hostName.startsWith('default-') ? '*' : hostName}\nIdentityFile ${getKeyFilePath(hostName)}\n`;
const configContentBuffer = await readFile(configFile);
if (configContentBuffer.indexOf(keyConfig) >= 0) {
const newConfigContent = configContentBuffer.toString().replace(keyConfig, '');
await writeFile(configFile, newConfigContent);
} else {
return Promise.resolve('');
await appendFile(configFile, keyConfig);
}
};

const writeKey = async (name: string, key: string) => {
const keyFile = getKeyFilePath(name);
await appendFile(keyFile, key);
await chmod(keyFile, '600');
};

const generateKeyPair = async (sshkeyManager: SshKeyManager) => {
const keyName = `default-${Date.now()}`;
const key = await sshkeyManager.generate('vcs', keyName);
await updateConfig(keyName);
await writeKey(keyName, key.privateKey);
const viewAction = 'View';
const action = await theia.window.showInformationMessage('Key pair successfully generated, do you want to view the public key?', viewAction);
if (action === viewAction && key.privateKey) {
const document = await theia.workspace.openTextDocument({ content: key.publicKey });
await theia.window.showTextDocument(document);
}
};

const generateKeyPair = async function (sshkeyManager: SshKeyManager): Promise<void> {
const keyName = await theia.window.showInputBox({ placeHolder: 'Please provide a key pair name' });
const key = await sshkeyManager.generate(await getSshServiceName(), keyName ? keyName : '');
const generateKeyPairForHost = async (sshkeyManager: SshKeyManager) => {
const hostName = await getHostName();
const key = await sshkeyManager.generate('vcs', hostName);
await updateConfig(hostName);
await writeKey(hostName, key.privateKey);
const viewAction = 'View';
const action = await theia.window.showInformationMessage('Do you want to view the generated private key?', viewAction);
const action = await theia.window.showInformationMessage(`Key pair for ${hostName} successfully generated, do you want to view the public key?`, viewAction);
if (action === viewAction && key.privateKey) {
theia.workspace.openTextDocument({ content: key.privateKey });
const document = await theia.workspace.openTextDocument({ content: key.publicKey });
await theia.window.showTextDocument(document);
}
};

const createKeyPair = async function (sshkeyManager: SshKeyManager): Promise<void> {
const keyName = await theia.window.showInputBox({ placeHolder: 'Please provide a key pair name' });
const createKeyPair = async (sshkeyManager: SshKeyManager) => {
const hostName = await getHostName();
const publicKey = await theia.window.showInputBox({ placeHolder: 'Enter public key' });
const privateKey = await theia.window.showInputBox({ placeHolder: 'Enter private key' });

await sshkeyManager
.create({ name: keyName ? keyName : '', service: await getSshServiceName(), publicKey: publicKey })
.then(() => {
theia.window.showInformationMessage('Key "' + `${keyName}` + '" successfully created');
})
.catch(error => {
theia.window.showErrorMessage(error);
});
try {
await sshkeyManager.create({ name: hostName, service: 'vcs', publicKey: publicKey, privateKey });
theia.window.showInformationMessage(`Key pair for ${hostName} successfully created`);
await updateConfig(hostName);
await writeKey(hostName, privateKey);
} catch (error) {
theia.window.showErrorMessage(error);
}
};

const deleteKeyPair = async function (sshkeyManager: SshKeyManager): Promise<void> {
const sshServiceName = await getSshServiceName();
const keys: cheApi.ssh.SshPair[] = await sshkeyManager.getAll(sshServiceName);
const deleteKeyPair = async (sshkeyManager: SshKeyManager) => {
const keys: cheApi.ssh.SshPair[] = await sshkeyManager.getAll('vcs');
const keyResp = await theia.window.showQuickPick<theia.QuickPickItem>(keys.map(key =>
({ label: key.name ? key.name : '' })), {});
const keyName = keyResp ? keyResp.label : '';
await sshkeyManager
.delete(sshServiceName, keyName)
.then(() => {
theia.window.showInformationMessage('Key "' + `${keyName}` + '" successfully deleted');
})
.catch(error => {
theia.window.showErrorMessage(error);
});

try {
await sshkeyManager.delete('vcs', keyName);
const keyFile = getKeyFilePath(keyName);
if (await pathExists(keyFile)) {
await unlink(keyFile);
await updateConfig(keyName);
}
theia.window.showInformationMessage(`Key ${keyName} successfully deleted`);
} catch (error) {
theia.window.showErrorMessage(error);
}
};

const viewPublicKey = async function (sshkeyManager: SshKeyManager): Promise<void> {
const sshServiceName = await getSshServiceName();
const keys: cheApi.ssh.SshPair[] = await sshkeyManager.getAll(sshServiceName);
const viewPublicKey = async (sshkeyManager: SshKeyManager) => {
const keys: cheApi.ssh.SshPair[] = await sshkeyManager.getAll('vcs');
const keyResp = await theia.window.showQuickPick<theia.QuickPickItem>(keys.map(key =>
({ label: key.name ? key.name : '' })), {});
const keyName = keyResp ? keyResp.label : '';
await sshkeyManager
.get(sshServiceName, keyName)
.then(key => {
theia.workspace.openTextDocument({ content: key.publicKey });
})
.catch(error => {
theia.window.showErrorMessage(error);
});
try {
const key = await sshkeyManager.get('vcs', keyName);
const document = await theia.workspace.openTextDocument({ content: key.publicKey });
theia.window.showTextDocument(document);
} catch (error) {
theia.window.showErrorMessage(error);
}
};

export function stop() {
Expand Down
18 changes: 9 additions & 9 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4621,6 +4621,15 @@ fs-extra@7.0.0:
jsonfile "^4.0.0"
universalify "^0.1.0"

fs-extra@7.0.1, fs-extra@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"

fs-extra@^0.30.0:
version "0.30.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0"
Expand All @@ -4632,15 +4641,6 @@ fs-extra@^0.30.0:
path-is-absolute "^1.0.0"
rimraf "^2.2.8"

fs-extra@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"

fs-minipass@^1.2.5:
version "1.2.6"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07"
Expand Down

0 comments on commit 94223d4

Please sign in to comment.