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

Fixup SSH key auth for project repos

This commit is contained in:
Nick O'Leary 2018-01-18 22:17:48 +00:00
parent f95b414d22
commit f7f795f58a
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
13 changed files with 130 additions and 57 deletions

View File

@ -122,12 +122,25 @@
timeout: msg.timeout timeout: msg.timeout
} }
if (notificationId === "runtime-state") { if (notificationId === "runtime-state") {
if (msg.error === "credentials_load_failed") { if (msg.error === "missing-types") {
text+="<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
options.buttons = [
{
text: "Close",
click: function() {
persistentNotifications[notificationId].close();
delete persistentNotifications[notificationId];
}
}
]
} else if (msg.error === "credentials_load_failed") {
if (RED.user.hasPermission("projects.write")) { if (RED.user.hasPermission("projects.write")) {
options.buttons = [ options.buttons = [
{ {
text: "Setup credentials", text: "Setup credentials",
click: function() { click: function() {
persistentNotifications[notificationId].close();
delete persistentNotifications[notificationId];
RED.projects.showCredentialsPrompt(); RED.projects.showCredentialsPrompt();
} }
} }

View File

@ -956,8 +956,8 @@ RED.projects.settings = (function() {
credentialStateLabel.find(".user-settings-credentials-state-icon").removeClass().addClass("user-settings-credentials-state-icon fa fa-unlock"); credentialStateLabel.find(".user-settings-credentials-state-icon").removeClass().addClass("user-settings-credentials-state-icon fa fa-unlock");
credentialStateLabel.find(".user-settings-credentials-state").text("Encryption disabled"); credentialStateLabel.find(".user-settings-credentials-state").text("Encryption disabled");
} }
credentialSecretResetButton.toggleClass('disabled',!activeProject.settings.credentialsEncrypted); credentialSecretResetButton.toggleClass('disabled',!activeProject.settings.credentialSecretInvalid && !activeProject.settings.credentialsEncrypted);
credentialSecretResetButton.prop('disabled',!activeProject.settings.credentialsEncrypted); credentialSecretResetButton.prop('disabled',!activeProject.settings.credentialSecretInvalid && !activeProject.settings.credentialsEncrypted);
} }
checkFiles(); checkFiles();

View File

@ -194,21 +194,21 @@ RED.projects.userSettings = (function() {
name: keyNameInput.val() name: keyNameInput.val()
}; };
var selectedButton = bg.find(".selected"); // var selectedButton = bg.find(".selected");
if (selectedButton[0] === addLocalButton[0]) { // if (selectedButton[0] === addLocalButton[0]) {
payload.type = "local"; // payload.type = "local";
payload.publicKeyPath = localPublicKeyPathInput.val(); // payload.publicKeyPath = localPublicKeyPathInput.val();
payload.privateKeyPath = localPrivateKeyPathInput.val(); // payload.privateKeyPath = localPrivateKeyPathInput.val();
} else if (selectedButton[0] === uploadButton[0]) { // } else if (selectedButton[0] === uploadButton[0]) {
payload.type = "upload"; // payload.type = "upload";
payload.publicKey = publicKeyInput.val(); // payload.publicKey = publicKeyInput.val();
payload.privateKey = privateKeyInput.val(); // payload.privateKey = privateKeyInput.val();
} else if (selectedButton[0] === generateButton[0]) { // } else if (selectedButton[0] === generateButton[0]) {
payload.type = "generate"; payload.type = "generate";
payload.comment = gitEmailInput.val(); payload.comment = gitEmailInput.val();
payload.password = passphraseInput.val(); payload.password = passphraseInput.val();
payload.size = 4096; payload.size = 4096;
} // }
var done = function(err) { var done = function(err) {
spinner.remove(); spinner.remove();
if (err) { if (err) {

View File

@ -847,7 +847,13 @@ RED.projects = (function() {
validateForm(); validateForm();
}); });
row = $('<div class="hide form-row projects-dialog-screen-create-row projects-dialog-screen-create-row-clone projects-dialog-screen-create-row-creds"></div>').hide().appendTo(container);
var cloneAuthRows = $('<div class="hide projects-dialog-screen-create-row projects-dialog-screen-create-row-clone"></div>').hide().appendTo(container);
row = $('<div class="form-row projects-dialog-screen-create-row-auth-error"></div>').hide().appendTo(cloneAuthRows);
$('<div><i class="fa fa-warning"></i> Authentication failed</div>').appendTo(row);
// Repo credentials - username/password ----------------
row = $('<div class="hide form-row projects-dialog-screen-create-row-creds"></div>').hide().appendTo(cloneAuthRows);
var subrow = $('<div style="width: calc(50% - 10px); display:inline-block;"></div>').appendTo(row); var subrow = $('<div style="width: calc(50% - 10px); display:inline-block;"></div>').appendTo(row);
$('<label for="projects-dialog-screen-create-project-repo-user">Username</label>').appendTo(subrow); $('<label for="projects-dialog-screen-create-project-repo-user">Username</label>').appendTo(subrow);
@ -857,7 +863,11 @@ RED.projects = (function() {
$('<label for="projects-dialog-screen-create-project-repo-pass">Password</label>').appendTo(subrow); $('<label for="projects-dialog-screen-create-project-repo-pass">Password</label>').appendTo(subrow);
projectRepoPasswordInput = $('<input id="projects-dialog-screen-create-project-repo-pass" type="password"></input>').appendTo(subrow); projectRepoPasswordInput = $('<input id="projects-dialog-screen-create-project-repo-pass" type="password"></input>').appendTo(subrow);
row = $('<div class="form-row projects-dialog-screen-create-row projects-dialog-screen-create-row-sshkey"></div>').hide().appendTo(container);
// -----------------------------------------------------
// Repo credentials - key/passphrase -------------------
row = $('<div class="form-row projects-dialog-screen-create-row projects-dialog-screen-create-row-sshkey"></div>').hide().appendTo(cloneAuthRows);
subrow = $('<div style="width: calc(50% - 10px); display:inline-block;"></div>').appendTo(row); subrow = $('<div style="width: calc(50% - 10px); display:inline-block;"></div>').appendTo(row);
$('<label for="projects-dialog-screen-create-project-repo-passphrase">SSH Key</label>').appendTo(subrow); $('<label for="projects-dialog-screen-create-project-repo-passphrase">SSH Key</label>').appendTo(subrow);
projectRepoSSHKeySelect = $("<select>",{style:"width: 100%"}).appendTo(subrow); projectRepoSSHKeySelect = $("<select>",{style:"width: 100%"}).appendTo(subrow);
@ -878,13 +888,12 @@ RED.projects = (function() {
sshwarningRow.hide(); sshwarningRow.hide();
} }
}); });
subrow = $('<div style="width: calc(50% - 10px); margin-left: 20px; display:inline-block;"></div>').appendTo(row); subrow = $('<div style="width: calc(50% - 10px); margin-left: 20px; display:inline-block;"></div>').appendTo(row);
$('<label for="projects-dialog-screen-create-project-repo-passphrase">Passphrase</label>').appendTo(subrow); $('<label for="projects-dialog-screen-create-project-repo-passphrase">Passphrase</label>').appendTo(subrow);
projectRepoPassphrase = $('<input id="projects-dialog-screen-create-project-repo-passphrase" type="password"></input>').appendTo(subrow); projectRepoPassphrase = $('<input id="projects-dialog-screen-create-project-repo-passphrase" type="password"></input>').appendTo(subrow);
var sshwarningRow = $('<div style="padding: 20px"></div>').hide().appendTo(row); subrow = $('<div class="form-row projects-dialog-screen-create-row projects-dialog-screen-create-row-sshkey"></div>').appendTo(cloneAuthRows);
var sshwarningRow = $('<div class="projects-dialog-screen-create-row-auth-error-no-keys"></div>').hide().appendTo(subrow);
$('<div class="form-row"><i class="fa fa-warning"></i> Before you can clone a repository over ssh you must add an SSH key to access it.</div>').appendTo(sshwarningRow); $('<div class="form-row"><i class="fa fa-warning"></i> Before you can clone a repository over ssh you must add an SSH key to access it.</div>').appendTo(sshwarningRow);
subrow = $('<div style="text-align: center">').appendTo(sshwarningRow); subrow = $('<div style="text-align: center">').appendTo(sshwarningRow);
$('<button class="editor-button">Add an ssh key</button>').appendTo(subrow).click(function(e) { $('<button class="editor-button">Add an ssh key</button>').appendTo(subrow).click(function(e) {
@ -895,6 +904,8 @@ RED.projects = (function() {
$("#user-settings-gitconfig-add-key").click(); $("#user-settings-gitconfig-add-key").click();
},500); },500);
}); });
// -----------------------------------------------------
// // Secret - clone // // Secret - clone
// row = $('<div class="hide form-row projects-dialog-screen-create-row projects-dialog-screen-create-row-clone"></div>').appendTo(container); // row = $('<div class="hide form-row projects-dialog-screen-create-row projects-dialog-screen-create-row-clone"></div>').appendTo(container);
@ -989,7 +1000,6 @@ RED.projects = (function() {
sendRequest({ sendRequest({
url: "projects", url: "projects",
type: "POST", type: "POST",
requireCleanWorkspace: true,
handleAuthFail: false, handleAuthFail: false,
responses: { responses: {
200: function(data) { 200: function(data) {
@ -1006,17 +1016,22 @@ RED.projects = (function() {
projectRepoInput.addClass("input-error"); projectRepoInput.addClass("input-error");
}, },
'git_auth_failed': function(error) { 'git_auth_failed': function(error) {
$(".projects-dialog-screen-create-row-auth-error").show();
projectRepoUserInput.addClass("input-error"); projectRepoUserInput.addClass("input-error");
projectRepoPasswordInput.addClass("input-error"); projectRepoPasswordInput.addClass("input-error");
// getRepoAuthDetails(req); // getRepoAuthDetails(req);
projectRepoSSHKeySelect.addClass("input-error"); projectRepoSSHKeySelect.addClass("input-error");
projectRepoPassphrase.addClass("input-error"); projectRepoPassphrase.addClass("input-error");
console.log("git auth error",error);
}, },
'project_empty': function(error) { 'project_empty': function(error) {
// This is handled via a runtime notification. // This is handled via a runtime notification.
dialog.dialog("close"); dialog.dialog("close");
}, },
'credentials_load_failed': function(error) {
// This is handled via a runtime notification.
dialog.dialog("close");
},
'unexpected_error': function(error) { 'unexpected_error': function(error) {
console.log("unexpected_error",error) console.log("unexpected_error",error)
} }
@ -1396,14 +1411,9 @@ RED.projects = (function() {
function requireCleanWorkspace(done) {
function sendRequest(options,body) { if (RED.nodes.dirty()) {
// dialogBody.hide(); var message = '<p>You have undeployed changes that will be lost.</p><p>Do you want to continue?</p>';
console.log(options.url,body);
if (options.requireCleanWorkspace && RED.nodes.dirty()) {
var message = 'You have undeployed changes that will be lost. Do you want to continue?';
var alwaysCallback;
var cleanNotification = RED.notify(message,{ var cleanNotification = RED.notify(message,{
type:"info", type:"info",
fixed: true, fixed: true,
@ -1415,29 +1425,57 @@ RED.projects = (function() {
text: RED._("common.label.cancel"), text: RED._("common.label.cancel"),
click: function() { click: function() {
cleanNotification.close(); cleanNotification.close();
if (options.cancel) { done(true);
options.cancel();
}
if (alwaysCallback) {
alwaysCallback();
}
} }
},{ },{
text: 'Continue', text: 'Continue',
click: function() { click: function() {
cleanNotification.close(); cleanNotification.close();
done(false);
}
}
]
});
}
}
function sendRequest(options,body) {
// dialogBody.hide();
console.log(options.url,body);
if (options.requireCleanWorkspace && RED.nodes.dirty()) {
var thenCallback;
var alwaysCallback;
requireCleanWorkspace(function(cancelled) {
if (cancelled) {
if (options.cancel) {
options.cancel();
if (alwaysCallback) {
alwaysCallback();
}
}
} else {
delete options.requireCleanWorkspace; delete options.requireCleanWorkspace;
sendRequest(options,body).always(function() { sendRequest(options,body).then(function() {
if (thenCallback) {
thenCallback();
}
}).always(function() {
if (alwaysCallback) { if (alwaysCallback) {
alwaysCallback(); alwaysCallback();
} }
}) })
} }
} })
] // What follows is a very hacky Promise-like api thats good enough
}); // for our needs.
return { return {
then: function(done) {
thenCallback = done;
return { always: function(done) { alwaysCallback = done; }}
},
always: function(done) { alwaysCallback = done; } always: function(done) { alwaysCallback = done; }
} }
} }
@ -1806,6 +1844,13 @@ RED.projects = (function() {
} }
function showNewProjectScreen() {
if (!activeProject) {
show('welcome');
} else {
show('create')
}
}
return { return {
init: init, init: init,
@ -1821,10 +1866,15 @@ RED.projects = (function() {
RED.notify(RED._("user.errors.notAuthorized"),"error"); RED.notify(RED._("user.errors.notAuthorized"),"error");
return; return;
} }
if (!activeProject) {
show('welcome'); if (RED.nodes.dirty()) {
return requireCleanWorkspace(function(cancelled) {
if (!cancelled) {
showNewProjectScreen();
}
})
} else { } else {
show('create') showNewProjectScreen();
} }
}, },
selectProject: function() { selectProject: function() {

View File

@ -36,7 +36,7 @@
} }
.notification p:first-child { .notification p:first-child {
font-size: 1.1em; font-size: 1.1em;
font-weight: 500; font-weight: 400;
} }
.notification a { .notification a {
text-decoration: none; text-decoration: none;

View File

@ -88,9 +88,9 @@
"warnings": { "warnings": {
"undeployedChanges": "node has undeployed changes", "undeployedChanges": "node has undeployed changes",
"nodeActionDisabled": "node actions disabled within subflow", "nodeActionDisabled": "node actions disabled within subflow",
"missing-types": "Flows stopped due to missing node types. Check logs for details.", "missing-types": "<p>Flows stopped due to missing node types.</p>",
"restartRequired": "Node-RED must be restarted to enable upgraded modules", "restartRequired": "Node-RED must be restarted to enable upgraded modules",
"credentials_load_failed": "<p>Flows stopped due to missing or invalid credentialSecret.</p>", "credentials_load_failed": "<p>Flows stopped as the credentials could not be decrypted.</p><p>The flow credential file is encrypted, but the project's encryption key is missing or invalid.</p>",
"missing_flow_file": "<p>Project flow file not found.</p><p>The project is not configured with a flow file.</p>", "missing_flow_file": "<p>Project flow file not found.</p><p>The project is not configured with a flow file.</p>",
"project_empty": "<p>The project is empty.</p><p>Do you want to create a default set of project files?<br/>Otherwise, you will have to manually add files to the project outside of the editor.</p>" "project_empty": "<p>The project is empty.</p><p>Do you want to create a default set of project files?<br/>Otherwise, you will have to manually add files to the project outside of the editor.</p>"
}, },

View File

@ -17,11 +17,13 @@ var theme = require("../editor/theme");
var util = require('util'); var util = require('util');
var runtime; var runtime;
var settings; var settings;
var log;
module.exports = { module.exports = {
init: function(_runtime) { init: function(_runtime) {
runtime = _runtime; runtime = _runtime;
settings = runtime.settings; settings = runtime.settings;
log = runtime.log;
}, },
runtimeSettings: function(req,res) { runtimeSettings: function(req,res) {
var safeSettings = { var safeSettings = {

View File

@ -254,7 +254,7 @@ function start(type,diff,muteLog) {
log.info(log._("nodes.flows.missing-type-install-2")); log.info(log._("nodes.flows.missing-type-install-2"));
log.info(" "+settings.userDir); log.info(" "+settings.userDir);
} }
events.emit("runtime-event",{id:"runtime-state",payload:{type:"warning",text:"notification.warnings.missing-types"},retain:true}); events.emit("runtime-event",{id:"runtime-state",payload:{error:"missing-types", type:"warning",text:"notification.warnings.missing-types",types:activeFlowConfig.missingTypes},retain:true});
return when.resolve(); return when.resolve();
} }
if (!muteLog) { if (!muteLog) {

View File

@ -32,6 +32,15 @@ var projectsDir;
var authCache = require("./git/authCache"); var authCache = require("./git/authCache");
// TODO: DRY - red/api/editor/sshkeys !
function getSSHKeyUsername(userObj) {
var username = '__default';
if ( userObj && userObj.name ) {
username = userObj.name;
}
return username;
}
function Project(name) { function Project(name) {
this.name = name; this.name = name;
this.path = fspath.join(projectsDir,name); this.path = fspath.join(projectsDir,name);
@ -645,7 +654,7 @@ Project.prototype.updateRemote = function(user,remote,options) {
if (options.auth) { if (options.auth) {
var url = this.remotes[remote].fetch; var url = this.remotes[remote].fetch;
if (options.auth.keyFile) { if (options.auth.keyFile) {
options.auth.key_path = sshKeys.getPrivateKeyPath(username, options.auth.keyFile); options.auth.key_path = sshKeys.getPrivateKeyPath(getSSHKeyUsername(user), options.auth.keyFile);
} }
authCache.set(this.name,url,username,options.auth); authCache.set(this.name,url,username,options.auth);
} }
@ -662,7 +671,7 @@ Project.prototype.removeRemote = function(user, remote) {
Project.prototype.getFlowFile = function() { Project.prototype.getFlowFile = function() {
console.log("Project.getFlowFile = ",this.paths.flowFile); // console.log("Project.getFlowFile = ",this.paths.flowFile);
if (this.paths.flowFile) { if (this.paths.flowFile) {
return fspath.join(this.path,this.paths.flowFile); return fspath.join(this.path,this.paths.flowFile);
} else { } else {
@ -674,7 +683,7 @@ Project.prototype.getFlowFileBackup = function() {
return getBackupFilename(this.getFlowFile()); return getBackupFilename(this.getFlowFile());
} }
Project.prototype.getCredentialsFile = function() { Project.prototype.getCredentialsFile = function() {
console.log("Project.getCredentialsFile = ",this.paths.credentialsFile); // console.log("Project.getCredentialsFile = ",this.paths.credentialsFile);
if (this.paths.credentialsFile) { if (this.paths.credentialsFile) {
return fspath.join(this.path,this.paths.credentialsFile); return fspath.join(this.path,this.paths.credentialsFile);
} else { } else {
@ -872,7 +881,7 @@ function createProject(user, metadata) {
} }
else if (originRemote.hasOwnProperty("keyFile") && originRemote.hasOwnProperty("passphrase")) { else if (originRemote.hasOwnProperty("keyFile") && originRemote.hasOwnProperty("passphrase")) {
authCache.set(project,originRemote.url,username,{ // TODO: hardcoded remote name authCache.set(project,originRemote.url,username,{ // TODO: hardcoded remote name
key_path: sshKeys.getPrivateKeyPath(username, originRemote.keyFile), key_path: sshKeys.getPrivateKeyPath(getSSHKeyUsername(user), originRemote.keyFile),
passphrase: originRemote.passphrase passphrase: originRemote.passphrase
} }
); );

View File

@ -45,7 +45,7 @@ var ResponseServer = function(auth) {
parts.push(data.substring(0, m)); parts.push(data.substring(0, m));
data = data.substring(m); data = data.substring(m);
var line = parts.join(""); var line = parts.join("");
console.log("LINE:",line); // console.log("LINE:",line);
parts = []; parts = [];
if (line==='Username') { if (line==='Username') {
connection.end(auth.username); connection.end(auth.username);
@ -90,7 +90,6 @@ var ResponseSSHServer = function(auth) {
parts.push(data.substring(0, m)); parts.push(data.substring(0, m));
data = data.substring(m); data = data.substring(m);
var line = parts.join(""); var line = parts.join("");
console.log("LINE:",line);
parts = []; parts = [];
if (line==='The') { if (line==='The') {
// TODO: document these exchanges! // TODO: document these exchanges!

View File

@ -23,7 +23,7 @@ var crypto = require('crypto');
var storageSettings = require("../settings"); var storageSettings = require("../settings");
var util = require("../util"); var util = require("../util");
var gitTools = require("./git"); var gitTools = require("./git");
var sshTools = require("./ssh"); var sshTools = require("./sshKeygen");
var Projects = require("./Project"); var Projects = require("./Project");

View File

@ -19,7 +19,7 @@ var sinon = require("sinon");
var child_process = require('child_process'); var child_process = require('child_process');
var EventEmitter = require("events"); var EventEmitter = require("events");
var ssh = require("../../../../../../red/runtime/storage/localfilesystem/projects/ssh") var ssh = require("../../../../../../red/runtime/storage/localfilesystem/projects/sshKeygen")
describe("localfilesystem/projects/ssh", function() { describe("localfilesystem/projects/ssh", function() {