1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Allow a user to pick existing sshkeys from ~/.ssh

This commit is contained in:
Nick O'Leary 2018-01-10 17:37:41 +00:00
parent 00a396014b
commit 6516e0dfd2
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
8 changed files with 268 additions and 131 deletions

View File

@ -44,24 +44,32 @@ RED.projects.userSettings = (function() {
function createSSHKeySection(pane) { function createSSHKeySection(pane) {
var container = $('<div class="user-settings-section"></div>').appendTo(pane); var container = $('<div class="user-settings-section"></div>').appendTo(pane);
var popover; var popover;
var title = $('<h4></h4>').text("SSH Keys").appendTo(container); var title = $('<h3></h3>').text("SSH Keys").appendTo(container);
var subtitle = $('<div style="color:#aaa;"></div>').appendTo(container).text("Allows you to create secure connections to remote git repositories.");
var addKeyButton = $('<button class="editor-button editor-button-small" style="float: right; margin-right: 10px;">generate key</button>') var addKeyButton = $('<button class="editor-button editor-button-small" style="float: right; margin-right: 10px;">add key</button>')
.appendTo(title) .appendTo(subtitle)
.click(function(evt) { .click(function(evt) {
addKeyButton.attr('disabled',true); addKeyButton.attr('disabled',true);
saveButton.attr('disabled',true);
// bg.children().removeClass("selected");
// addLocalButton.click();
addKeyDialog.slideDown(200); addKeyDialog.slideDown(200);
keyNameInput.focus(); keyNameInput.focus();
saveButton.attr('disabled',true);
}); });
var validateForm = function() { var validateForm = function() {
var validName = /^[a-zA-Z0-9\-_]+$/.test(keyNameInput.val()); var valid = /^[a-zA-Z0-9\-_]+$/.test(keyNameInput.val());
keyNameInput.toggleClass('input-error',keyNameInputChanged&&!valid);
// var selectedButton = bg.find(".selected");
// if (selectedButton[0] === addLocalButton[0]) {
// valid = valid && localPublicKeyPathInput.val().length > 0 && localPrivateKeyPathInput.val().length > 0;
// } else if (selectedButton[0] === uploadButton[0]) {
// valid = valid && publicKeyInput.val().length > 0 && privateKeyInput.val().length > 0;
// } else if (selectedButton[0] === generateButton[0]) {
var passphrase = passphraseInput.val(); var passphrase = passphraseInput.val();
var validPassphrase = passphrase.length === 0 || passphrase.length >= 8; var validPassphrase = passphrase.length === 0 || passphrase.length >= 8;
saveButton.attr('disabled',!validName || !validPassphrase);
keyNameInput.toggleClass('input-error',keyNameInputChanged&&!validName);
passphraseInput.toggleClass('input-error',!validPassphrase); passphraseInput.toggleClass('input-error',!validPassphrase);
if (!validPassphrase) { if (!validPassphrase) {
passphraseInputSubLabel.text("Passphrase too short"); passphraseInputSubLabel.text("Passphrase too short");
@ -70,6 +78,10 @@ RED.projects.userSettings = (function() {
} else { } else {
passphraseInputSubLabel.text(""); passphraseInputSubLabel.text("");
} }
valid = valid && validPassphrase;
// }
saveButton.attr('disabled',!valid);
if (popover) { if (popover) {
popover.close(); popover.close();
@ -79,27 +91,88 @@ RED.projects.userSettings = (function() {
var row = $('<div class="user-settings-row"></div>').appendTo(container); var row = $('<div class="user-settings-row"></div>').appendTo(container);
var addKeyDialog = $('<div class="projects-dialog-list-dialog"></div>').hide().appendTo(row); var addKeyDialog = $('<div class="projects-dialog-list-dialog"></div>').hide().appendTo(row);
$('<div class="projects-dialog-list-dialog-header">').text('Generate SSH Key').appendTo(addKeyDialog); $('<div class="projects-dialog-list-dialog-header">').text('Add SSH Key').appendTo(addKeyDialog);
var addKeyDialogBody = $('<div>').appendTo(addKeyDialog); var addKeyDialogBody = $('<div>').appendTo(addKeyDialog);
row = $('<div class="user-settings-row"></div>').appendTo(addKeyDialogBody);
$('<div style="color:#aaa;"></div>').appendTo(row).text("Generate a new public/private key pair");
// var bg = $('<div></div>',{class:"button-group", style:"text-align: center"}).appendTo(row);
// var addLocalButton = $('<button class="editor-button toggle selected">use local key</button>').appendTo(bg);
// var uploadButton = $('<button class="editor-button toggle">upload key</button>').appendTo(bg);
// var generateButton = $('<button class="editor-button toggle">generate key</button>').appendTo(bg);
// bg.children().click(function(e) {
// e.preventDefault();
// if ($(this).hasClass("selected")) {
// return;
// }
// bg.children().removeClass("selected");
// $(this).addClass("selected");
// if (this === addLocalButton[0]) {
// addLocalKeyPane.show();
// generateKeyPane.hide();
// uploadKeyPane.hide();
// } else if (this === uploadButton[0]) {
// addLocalKeyPane.hide();
// generateKeyPane.hide();
// uploadKeyPane.show();
// } else if (this === generateButton[0]){
// addLocalKeyPane.hide();
// generateKeyPane.show();
// uploadKeyPane.hide();
// }
// validateForm();
// })
row = $('<div class="user-settings-row"></div>').appendTo(addKeyDialogBody); row = $('<div class="user-settings-row"></div>').appendTo(addKeyDialogBody);
$('<label for=""></label>').text('Name').appendTo(row); $('<label for=""></label>').text('Name').appendTo(row);
var keyNameInputChanged = false;
var keyNameInput = $('<input type="text">').appendTo(row).on("change keyup paste",function() { var keyNameInput = $('<input type="text">').appendTo(row).on("change keyup paste",function() {
keyNameInputChanged = true; keyNameInputChanged = true;
validateForm(); validateForm();
}); });
var keyNameInputChanged = false;
$('<label class="projects-edit-form-sublabel"><small>Must contain only A-Z 0-9 _ -</small></label>').appendTo(row).find("small"); $('<label class="projects-edit-form-sublabel"><small>Must contain only A-Z 0-9 _ -</small></label>').appendTo(row).find("small");
row = $('<div class="user-settings-row"></div>').appendTo(addKeyDialogBody); var generateKeyPane = $('<div>').appendTo(addKeyDialogBody);
row = $('<div class="user-settings-row"></div>').appendTo(generateKeyPane);
$('<label for=""></label>').text('Passphrase').appendTo(row); $('<label for=""></label>').text('Passphrase').appendTo(row);
passphraseInput = $('<input type="password">').appendTo(row).on("change keyup paste",validateForm); var passphraseInput = $('<input type="password">').appendTo(row).on("change keyup paste",validateForm);
var passphraseInputSubLabel = $('<label class="projects-edit-form-sublabel"><small>Optional</small></label>').appendTo(row).find("small"); var passphraseInputSubLabel = $('<label class="projects-edit-form-sublabel"><small>Optional</small></label>').appendTo(row).find("small");
// var addLocalKeyPane = $('<div>').hide().appendTo(addKeyDialogBody);
// row = $('<div class="user-settings-row"></div>').appendTo(addLocalKeyPane);
// $('<label for=""></label>').text('Public key').appendTo(row);
// var localPublicKeyPathInput = $('<input type="text">').appendTo(row).on("change keyup paste",validateForm);
// $('<label class="projects-edit-form-sublabel"><small>Public key file path, for example: ~/.ssh/id_rsa.pub</small></label>').appendTo(row).find("small");
// row = $('<div class="user-settings-row"></div>').appendTo(addLocalKeyPane);
// $('<label for=""></label>').text('Private key').appendTo(row);
// var localPrivateKeyPathInput = $('<input type="text">').appendTo(row).on("change keyup paste",validateForm);
// $('<label class="projects-edit-form-sublabel"><small>Private key file path, for example: ~/.ssh/id_rsa</small></label>').appendTo(row).find("small");
//
// var uploadKeyPane = $('<div>').hide().appendTo(addKeyDialogBody);
// row = $('<div class="user-settings-row"></div>').appendTo(uploadKeyPane);
// $('<label for=""></label>').text('Public key').appendTo(row);
// var publicKeyInput = $('<textarea>').appendTo(row).on("change keyup paste",validateForm);
// $('<label class="projects-edit-form-sublabel"><small>Paste in public key contents, for example: ~/.ssh/id_rsa.pub</small></label>').appendTo(row).find("small");
// row = $('<div class="user-settings-row"></div>').appendTo(uploadKeyPane);
// $('<label for=""></label>').text('Private key').appendTo(row);
// var privateKeyInput = $('<textarea>').appendTo(row).on("change keyup paste",validateForm);
// $('<label class="projects-edit-form-sublabel"><small>Paste in private key contents, for example: ~/.ssh/id_rsa</small></label>').appendTo(row).find("small");
var hideEditForm = function() { var hideEditForm = function() {
addKeyButton.attr('disabled',false); addKeyButton.attr('disabled',false);
addKeyDialog.hide(); addKeyDialog.hide();
keyNameInput.val(""); keyNameInput.val("");
keyNameInputChanged = false;
passphraseInput.val(""); passphraseInput.val("");
// localPublicKeyPathInput.val("");
// localPrivateKeyPathInput.val("");
// publicKeyInput.val("");
// privateKeyInput.val("");
if (popover) { if (popover) {
popover.close(); popover.close();
popover = null; popover = null;
@ -118,11 +191,24 @@ RED.projects.userSettings = (function() {
evt.preventDefault(); evt.preventDefault();
var spinner = utils.addSpinnerOverlay(addKeyDialog).addClass('projects-dialog-spinner-contain'); var spinner = utils.addSpinnerOverlay(addKeyDialog).addClass('projects-dialog-spinner-contain');
var payload = { var payload = {
name: keyNameInput.val(), name: keyNameInput.val()
comment: gitEmailInput.val(),
password: passphraseInput.val(),
size: 4096
}; };
var selectedButton = bg.find(".selected");
if (selectedButton[0] === addLocalButton[0]) {
payload.type = "local";
payload.publicKeyPath = localPublicKeyPathInput.val();
payload.privateKeyPath = localPrivateKeyPathInput.val();
} else if (selectedButton[0] === uploadButton[0]) {
payload.type = "upload";
payload.publicKey = publicKeyInput.val();
payload.privateKey = privateKeyInput.val();
} else if (selectedButton[0] === generateButton[0]) {
payload.type = "generate";
payload.comment = gitEmailInput.val();
payload.password = passphraseInput.val();
payload.size = 4096;
}
var done = function(err) { var done = function(err) {
spinner.remove(); spinner.remove();
if (err) { if (err) {
@ -181,6 +267,7 @@ RED.projects.userSettings = (function() {
$('<button class="editor-button editor-button-small">Copy to clipboard</button>') $('<button class="editor-button editor-button-small">Copy to clipboard</button>')
.appendTo(formButtons) .appendTo(formButtons)
.click(function(evt) { .click(function(evt) {
evt.stopPropagation();
evt.preventDefault(); evt.preventDefault();
document.getSelection().selectAllChildren(keyBox[0]); document.getSelection().selectAllChildren(keyBox[0]);
var ret = document.execCommand('copy'); var ret = document.execCommand('copy');
@ -189,7 +276,7 @@ RED.projects.userSettings = (function() {
return row; return row;
} }
var keyList = $('<ol>').appendTo(row).editableList({ var keyList = $('<ol class="projects-dialog-ssh-key-list">').appendTo(row).editableList({
height: 'auto', height: 'auto',
addButton: false, addButton: false,
scrollOnAdd: false, scrollOnAdd: false,
@ -210,9 +297,7 @@ RED.projects.userSettings = (function() {
var tools = $('<span class="button-row entry-tools">').appendTo(container); var tools = $('<span class="button-row entry-tools">').appendTo(container);
var expandedRow; var expandedRow;
$('<button class="editor-button editor-button-small"><i class="fa fa-eye"></i></button>') row.click(function(e) {
.appendTo(tools)
.click(function(e) {
if (expandedRow) { if (expandedRow) {
expandedRow.slideUp(200,function() { expandedRow.slideUp(200,function() {
expandedRow.remove(); expandedRow.remove();
@ -222,9 +307,11 @@ RED.projects.userSettings = (function() {
expandedRow = expandKey(container,entry); expandedRow = expandKey(container,entry);
} }
}) })
if (!entry.system) {
$('<button class="editor-button editor-button-small"><i class="fa fa-trash"></i></button>') $('<button class="editor-button editor-button-small"><i class="fa fa-trash"></i></button>')
.appendTo(tools) .appendTo(tools)
.click(function(e) { .click(function(e) {
e.stopPropagation();
var spinner = utils.addSpinnerOverlay(row).addClass('projects-dialog-spinner-contain'); var spinner = utils.addSpinnerOverlay(row).addClass('projects-dialog-spinner-contain');
var notification = RED.notify("Are you sure you want to delete the SSH key '"+entry.name+"'? This cannot be undone.", { var notification = RED.notify("Are you sure you want to delete the SSH key '"+entry.name+"'? This cannot be undone.", {
type: 'warning', type: 'warning',
@ -270,6 +357,7 @@ RED.projects.userSettings = (function() {
] ]
}); });
}); });
}
if (entry.expand) { if (entry.expand) {
expandedRow = expandKey(container,entry); expandedRow = expandKey(container,entry);
} }
@ -288,7 +376,11 @@ RED.projects.userSettings = (function() {
key.expand = true; key.expand = true;
} }
keyList.editableList('addItem',key); keyList.editableList('addItem',key);
}) });
if (keyList.editableList('length') === 0) {
keyList.editableList('addItem',emptyItem);
}
} }
}) })
} }

View File

@ -1232,7 +1232,7 @@ RED.projects = (function() {
//data-i18n="[placeholder]menu.label.searchInput" //data-i18n="[placeholder]menu.label.searchInput"
delay: 200, delay: 200,
change: function() { change: function() {
filterTerm = $(this).val(); filterTerm = $(this).val().toLowerCase();
list.editableList('filter'); list.editableList('filter');
if (selectedListItem && !selectedListItem.is(":visible")) { if (selectedListItem && !selectedListItem.is(":visible")) {
selectedListItem.children().children().removeClass('selected'); selectedListItem.children().children().removeClass('selected');
@ -1241,6 +1241,12 @@ RED.projects = (function() {
if (options.select) { if (options.select) {
options.select(selectedListItem.children().data('data')); options.select(selectedListItem.children().data('data'));
} }
} else {
selectedListItem = list.children(":visible").first();
selectedListItem.children().children().addClass('selected');
if (options.select) {
options.select(selectedListItem.children().data('data'));
}
} }
ensureSelectedIsVisible(); ensureSelectedIsVisible();
} }

View File

@ -780,6 +780,15 @@ div.projects-dialog-ssh-public-key {
} }
} }
.projects-dialog-ssh-key-list {
li {
cursor: pointer;
}
li:hover {
background: #f3f3f3;
}
}
.projects-dialog-list { .projects-dialog-list {
position: relative; position: relative;
.red-ui-editableList-container { .red-ui-editableList-container {

View File

@ -73,7 +73,11 @@
display: table; display: table;
clear: both; clear: both;
} }
.uneditable-input, input { .uneditable-input, input, textarea {
width: calc(100% - 150px); width: calc(100% - 150px);
} }
textarea {
resize: none;
height: 10em;
}
} }

View File

@ -84,7 +84,7 @@ module.exports = {
app.post("/", needsPermission("settings.write"), function(req,res) { app.post("/", needsPermission("settings.write"), function(req,res) {
var username = getUsername(req.user); var username = getUsername(req.user);
// console.log('req.body:', req.body); // console.log('req.body:', req.body);
if ( req.body && req.body.name ) { if ( req.body && req.body.name && /^[a-zA-Z0-9\-_]+$/.test(req.body.name)) {
runtime.storage.sshkeys.generateSSHKey(username, req.body) runtime.storage.sshkeys.generateSSHKey(username, req.body)
.then(function(name) { .then(function(name) {
// console.log('generate key --- success name:', name); // console.log('generate key --- success name:', name);

View File

@ -949,7 +949,9 @@ function getProject(name) {
function listProjects() { function listProjects() {
return fs.readdir(projectsDir).then(function(fns) { return fs.readdir(projectsDir).then(function(fns) {
var dirs = []; var dirs = [];
fns.sort().filter(function(fn) { fns.sort(function(A,B) {
return A.toLowerCase().localeCompare(B.toLowerCase());
}).filter(function(fn) {
var fullPath = fspath.join(projectsDir,fn); var fullPath = fspath.join(projectsDir,fn);
if (fn[0] != ".") { if (fn[0] != ".") {
var stats = fs.lstatSync(fullPath); var stats = fs.lstatSync(fullPath);

View File

@ -23,12 +23,14 @@ var settings;
var runtime; var runtime;
var log; var log;
var sshkeyDir; var sshkeyDir;
var userSSHKeyDir;
function init(_settings, _runtime) { function init(_settings, _runtime) {
settings = _settings; settings = _settings;
runtime = _runtime; runtime = _runtime;
log = runtime.log; log = runtime.log;
sshkeyDir = fspath.join(settings.userDir, "projects", ".sshkeys"); sshkeyDir = fspath.join(settings.userDir, "projects", ".sshkeys");
userSSHKeyDir = fspath.join(process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH, ".ssh");
// console.log('sshkeys.init()'); // console.log('sshkeys.init()');
return createSSHKeyDirectory(); return createSSHKeyDirectory();
} }
@ -38,12 +40,23 @@ function createSSHKeyDirectory() {
} }
function listSSHKeys(username) { function listSSHKeys(username) {
var startStr = username + '_'; return listSSHKeysInDir(sshkeyDir,username + '_').then(function(customKeys) {
// console.log('sshkeyDir:', sshkeyDir); return listSSHKeysInDir(userSSHKeyDir).then(function(existingKeys) {
return fs.readdir(sshkeyDir).then(function(fns) { existingKeys.forEach(function(k){
k.system = true;
customKeys.push(k);
})
return customKeys;
});
});
}
function listSSHKeysInDir(dir,startStr) {
startStr = startStr || "";
return fs.readdir(dir).then(function(fns) {
var ret = fns.sort() var ret = fns.sort()
.filter(function(fn) { .filter(function(fn) {
var fullPath = fspath.join(sshkeyDir,fn); var fullPath = fspath.join(dir,fn);
if (fn.length > 2 || fn[0] != ".") { if (fn.length > 2 || fn[0] != ".") {
var stats = fs.lstatSync(fullPath); var stats = fs.lstatSync(fullPath);
if (stats.isFile()) { if (stats.isFile()) {
@ -73,6 +86,10 @@ function listSSHKeys(username) {
name: filename name: filename
}; };
}); });
}).then(function(result) {
return result;
}).catch(function() {
return []
}); });
} }
@ -81,7 +98,13 @@ function getSSHKey(username, name) {
.then(function(publicSSHKeyPath) { .then(function(publicSSHKeyPath) {
return fs.readFile(publicSSHKeyPath, 'utf-8'); return fs.readFile(publicSSHKeyPath, 'utf-8');
}).catch(function() { }).catch(function() {
return null; var privateKeyPath = fspath.join(userSSHKeyDir,name);
var publicKeyPath = privateKeyPath+".pub";
return checkFilePairExist(privateKeyPath,publicKeyPath).then(function() {
return fs.readFile(publicKeyPath, 'utf-8');
}).catch(function() {
return null
});
}); });
} }
@ -116,10 +139,7 @@ function checkExistSSHKeyFiles(username, name) {
var sshKeyFileBasename = username + '_' + name; var sshKeyFileBasename = username + '_' + name;
var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename); var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename);
var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub'); var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub');
return Promise.all([ return checkFilePairExist(privateKeyFilePath,publicKeyFilePath)
fs.access(privateKeyFilePath, (fs.constants || fs).R_OK),
fs.access(publicKeyFilePath , (fs.constants || fs).R_OK)
])
.then(function() { .then(function() {
return true; return true;
}) })
@ -132,13 +152,16 @@ function checkSSHKeyFileAndGetPublicKeyFileName(username, name) {
var sshKeyFileBasename = username + '_' + name; var sshKeyFileBasename = username + '_' + name;
var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename); var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename);
var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub'); var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub');
return checkFilePairExist(privateKeyFilePath,publicKeyFilePath).then(function() {
return publicKeyFilePath;
});
}
function checkFilePairExist(privateKeyFilePath,publicKeyFilePath) {
return Promise.all([ return Promise.all([
fs.access(privateKeyFilePath, (fs.constants || fs).R_OK), fs.access(privateKeyFilePath, (fs.constants || fs).R_OK),
fs.access(publicKeyFilePath , (fs.constants || fs).R_OK) fs.access(publicKeyFilePath , (fs.constants || fs).R_OK)
]) ])
.then(function() {
return publicKeyFilePath;
});
} }
function deleteSSHKeyFiles(username, name) { function deleteSSHKeyFiles(username, name) {
@ -149,9 +172,6 @@ function deleteSSHKeyFiles(username, name) {
fs.remove(privateKeyFilePath), fs.remove(privateKeyFilePath),
fs.remove(publicKeyFilePath) fs.remove(publicKeyFilePath)
]) ])
.then(function() {
return true;
});
} }
function generateSSHKeyPair(name, privateKeyPath, comment, password, size) { function generateSSHKeyPair(name, privateKeyPath, comment, password, size) {

View File

@ -33,12 +33,17 @@ describe("storage/localfilesystem/sshkeys", function() {
trace: function() { } trace: function() { }
} }
}; };
var oldHOME;
beforeEach(function(done) { beforeEach(function(done) {
oldHOME = process.env.HOME;
process.env.HOME = "/tmp/doesnt/exist";
fs.remove(userDir,function(err) { fs.remove(userDir,function(err) {
fs.mkdir(userDir,done); fs.mkdir(userDir,done);
}); });
}); });
afterEach(function(done) { afterEach(function(done) {
process.env.HOME = oldHOME;
fs.remove(userDir,done); fs.remove(userDir,done);
}); });
@ -463,8 +468,7 @@ describe("storage/localfilesystem/sshkeys", function() {
sshkeys.init(mockSettings, mockRuntime).then(function() { sshkeys.init(mockSettings, mockRuntime).then(function() {
fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8"); fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8");
fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),fileContent,"utf8"); fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),fileContent,"utf8");
sshkeys.deleteSSHKey(username, filename).then(function(retObj) { sshkeys.deleteSSHKey(username, filename).then(function() {
retObj.should.be.true();
fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.false(); fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.false();
fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.pub')).should.be.false(); fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.pub')).should.be.false();
done(); done();