From f7f795f58a75d3a04398304115f88ed6debba3bb Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 18 Jan 2018 22:17:48 +0000 Subject: [PATCH] Fixup SSH key auth for project repos --- editor/js/main.js | 15 ++- editor/js/ui/projects/projectSettings.js | 4 +- editor/js/ui/projects/projectUserSettings.js | 22 ++-- editor/js/ui/projects/projects.js | 112 +++++++++++++----- editor/sass/notifications.scss | 2 +- red/api/editor/locales/en-US/editor.json | 4 +- red/api/editor/settings.js | 2 + red/runtime/nodes/flows/index.js | 2 +- .../localfilesystem/projects/Project.js | 17 ++- .../projects/git/authServer.js | 3 +- .../storage/localfilesystem/projects/index.js | 2 +- .../projects/{ssh.js => sshKeygen.js} | 0 .../{ssh_spec.js => sshKeygen_spec.js} | 2 +- 13 files changed, 130 insertions(+), 57 deletions(-) rename red/runtime/storage/localfilesystem/projects/{ssh.js => sshKeygen.js} (100%) rename test/red/runtime/storage/localfilesystem/projects/{ssh_spec.js => sshKeygen_spec.js} (99%) diff --git a/editor/js/main.js b/editor/js/main.js index ef0a0e250..8b1e81f3e 100644 --- a/editor/js/main.js +++ b/editor/js/main.js @@ -122,12 +122,25 @@ timeout: msg.timeout } if (notificationId === "runtime-state") { - if (msg.error === "credentials_load_failed") { + if (msg.error === "missing-types") { + text+=""; + 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")) { options.buttons = [ { text: "Setup credentials", click: function() { + persistentNotifications[notificationId].close(); + delete persistentNotifications[notificationId]; RED.projects.showCredentialsPrompt(); } } diff --git a/editor/js/ui/projects/projectSettings.js b/editor/js/ui/projects/projectSettings.js index 5bf4a3d18..c4736a65c 100644 --- a/editor/js/ui/projects/projectSettings.js +++ b/editor/js/ui/projects/projectSettings.js @@ -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").text("Encryption disabled"); } - credentialSecretResetButton.toggleClass('disabled',!activeProject.settings.credentialsEncrypted); - credentialSecretResetButton.prop('disabled',!activeProject.settings.credentialsEncrypted); + credentialSecretResetButton.toggleClass('disabled',!activeProject.settings.credentialSecretInvalid && !activeProject.settings.credentialsEncrypted); + credentialSecretResetButton.prop('disabled',!activeProject.settings.credentialSecretInvalid && !activeProject.settings.credentialsEncrypted); } checkFiles(); diff --git a/editor/js/ui/projects/projectUserSettings.js b/editor/js/ui/projects/projectUserSettings.js index 6b6db3eaf..2b326b926 100644 --- a/editor/js/ui/projects/projectUserSettings.js +++ b/editor/js/ui/projects/projectUserSettings.js @@ -194,21 +194,21 @@ RED.projects.userSettings = (function() { name: keyNameInput.val() }; - 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]) { + // 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) { spinner.remove(); if (err) { diff --git a/editor/js/ui/projects/projects.js b/editor/js/ui/projects/projects.js index ff7981400..93a39af29 100644 --- a/editor/js/ui/projects/projects.js +++ b/editor/js/ui/projects/projects.js @@ -847,7 +847,13 @@ RED.projects = (function() { validateForm(); }); - row = $('
').hide().appendTo(container); + + var cloneAuthRows = $('
').hide().appendTo(container); + row = $('
').hide().appendTo(cloneAuthRows); + $('
Authentication failed
').appendTo(row); + + // Repo credentials - username/password ---------------- + row = $('
').hide().appendTo(cloneAuthRows); var subrow = $('
').appendTo(row); $('').appendTo(subrow); @@ -857,7 +863,11 @@ RED.projects = (function() { $('').appendTo(subrow); projectRepoPasswordInput = $('').appendTo(subrow); - row = $('
').hide().appendTo(container); + + // ----------------------------------------------------- + + // Repo credentials - key/passphrase ------------------- + row = $('
').hide().appendTo(cloneAuthRows); subrow = $('
').appendTo(row); $('').appendTo(subrow); projectRepoSSHKeySelect = $("').appendTo(subrow); - var sshwarningRow = $('
').hide().appendTo(row); + subrow = $('
').appendTo(cloneAuthRows); + var sshwarningRow = $('
').hide().appendTo(subrow); $('
Before you can clone a repository over ssh you must add an SSH key to access it.
').appendTo(sshwarningRow); subrow = $('
').appendTo(sshwarningRow); $('').appendTo(subrow).click(function(e) { @@ -895,6 +904,8 @@ RED.projects = (function() { $("#user-settings-gitconfig-add-key").click(); },500); }); + // ----------------------------------------------------- + // // Secret - clone // row = $('
').appendTo(container); @@ -989,7 +1000,6 @@ RED.projects = (function() { sendRequest({ url: "projects", type: "POST", - requireCleanWorkspace: true, handleAuthFail: false, responses: { 200: function(data) { @@ -1006,17 +1016,22 @@ RED.projects = (function() { projectRepoInput.addClass("input-error"); }, 'git_auth_failed': function(error) { + $(".projects-dialog-screen-create-row-auth-error").show(); + projectRepoUserInput.addClass("input-error"); projectRepoPasswordInput.addClass("input-error"); // getRepoAuthDetails(req); projectRepoSSHKeySelect.addClass("input-error"); projectRepoPassphrase.addClass("input-error"); - console.log("git auth error",error); }, 'project_empty': function(error) { // This is handled via a runtime notification. dialog.dialog("close"); }, + 'credentials_load_failed': function(error) { + // This is handled via a runtime notification. + dialog.dialog("close"); + }, 'unexpected_error': function(error) { console.log("unexpected_error",error) } @@ -1396,14 +1411,9 @@ RED.projects = (function() { - - function sendRequest(options,body) { - // dialogBody.hide(); - 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; + function requireCleanWorkspace(done) { + if (RED.nodes.dirty()) { + var message = '

You have undeployed changes that will be lost.

Do you want to continue?

'; var cleanNotification = RED.notify(message,{ type:"info", fixed: true, @@ -1415,29 +1425,57 @@ RED.projects = (function() { text: RED._("common.label.cancel"), click: function() { cleanNotification.close(); - if (options.cancel) { - options.cancel(); - } - if (alwaysCallback) { - alwaysCallback(); - } + done(true); } },{ text: 'Continue', click: function() { cleanNotification.close(); - delete options.requireCleanWorkspace; - sendRequest(options,body).always(function() { - if (alwaysCallback) { - alwaysCallback(); - } - - }) + 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; + sendRequest(options,body).then(function() { + if (thenCallback) { + thenCallback(); + } + }).always(function() { + if (alwaysCallback) { + alwaysCallback(); + } + + }) + } + }) + // What follows is a very hacky Promise-like api thats good enough + // for our needs. return { + then: function(done) { + thenCallback = done; + return { 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 { init: init, @@ -1821,10 +1866,15 @@ RED.projects = (function() { RED.notify(RED._("user.errors.notAuthorized"),"error"); return; } - if (!activeProject) { - show('welcome'); + + if (RED.nodes.dirty()) { + return requireCleanWorkspace(function(cancelled) { + if (!cancelled) { + showNewProjectScreen(); + } + }) } else { - show('create') + showNewProjectScreen(); } }, selectProject: function() { diff --git a/editor/sass/notifications.scss b/editor/sass/notifications.scss index 096fa607d..9a66f1683 100644 --- a/editor/sass/notifications.scss +++ b/editor/sass/notifications.scss @@ -36,7 +36,7 @@ } .notification p:first-child { font-size: 1.1em; - font-weight: 500; + font-weight: 400; } .notification a { text-decoration: none; diff --git a/red/api/editor/locales/en-US/editor.json b/red/api/editor/locales/en-US/editor.json index 9390605e2..73a2b3758 100644 --- a/red/api/editor/locales/en-US/editor.json +++ b/red/api/editor/locales/en-US/editor.json @@ -88,9 +88,9 @@ "warnings": { "undeployedChanges": "node has undeployed changes", "nodeActionDisabled": "node actions disabled within subflow", - "missing-types": "Flows stopped due to missing node types. Check logs for details.", + "missing-types": "

Flows stopped due to missing node types.

", "restartRequired": "Node-RED must be restarted to enable upgraded modules", - "credentials_load_failed": "

Flows stopped due to missing or invalid credentialSecret.

", + "credentials_load_failed": "

Flows stopped as the credentials could not be decrypted.

The flow credential file is encrypted, but the project's encryption key is missing or invalid.

", "missing_flow_file": "

Project flow file not found.

The project is not configured with a flow file.

", "project_empty": "

The project is empty.

Do you want to create a default set of project files?
Otherwise, you will have to manually add files to the project outside of the editor.

" }, diff --git a/red/api/editor/settings.js b/red/api/editor/settings.js index 0668f3631..9aeafb1ca 100644 --- a/red/api/editor/settings.js +++ b/red/api/editor/settings.js @@ -17,11 +17,13 @@ var theme = require("../editor/theme"); var util = require('util'); var runtime; var settings; +var log; module.exports = { init: function(_runtime) { runtime = _runtime; settings = runtime.settings; + log = runtime.log; }, runtimeSettings: function(req,res) { var safeSettings = { diff --git a/red/runtime/nodes/flows/index.js b/red/runtime/nodes/flows/index.js index 26c76cd30..5fdb2ef35 100644 --- a/red/runtime/nodes/flows/index.js +++ b/red/runtime/nodes/flows/index.js @@ -254,7 +254,7 @@ function start(type,diff,muteLog) { log.info(log._("nodes.flows.missing-type-install-2")); 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(); } if (!muteLog) { diff --git a/red/runtime/storage/localfilesystem/projects/Project.js b/red/runtime/storage/localfilesystem/projects/Project.js index 46817d397..14547e129 100644 --- a/red/runtime/storage/localfilesystem/projects/Project.js +++ b/red/runtime/storage/localfilesystem/projects/Project.js @@ -32,6 +32,15 @@ var projectsDir; 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) { this.name = name; this.path = fspath.join(projectsDir,name); @@ -645,7 +654,7 @@ Project.prototype.updateRemote = function(user,remote,options) { if (options.auth) { var url = this.remotes[remote].fetch; 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); } @@ -662,7 +671,7 @@ Project.prototype.removeRemote = function(user, remote) { Project.prototype.getFlowFile = function() { - console.log("Project.getFlowFile = ",this.paths.flowFile); + // console.log("Project.getFlowFile = ",this.paths.flowFile); if (this.paths.flowFile) { return fspath.join(this.path,this.paths.flowFile); } else { @@ -674,7 +683,7 @@ Project.prototype.getFlowFileBackup = function() { return getBackupFilename(this.getFlowFile()); } Project.prototype.getCredentialsFile = function() { - console.log("Project.getCredentialsFile = ",this.paths.credentialsFile); + // console.log("Project.getCredentialsFile = ",this.paths.credentialsFile); if (this.paths.credentialsFile) { return fspath.join(this.path,this.paths.credentialsFile); } else { @@ -872,7 +881,7 @@ function createProject(user, metadata) { } else if (originRemote.hasOwnProperty("keyFile") && originRemote.hasOwnProperty("passphrase")) { 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 } ); diff --git a/red/runtime/storage/localfilesystem/projects/git/authServer.js b/red/runtime/storage/localfilesystem/projects/git/authServer.js index 3e04e2c3c..d38ea9fd0 100644 --- a/red/runtime/storage/localfilesystem/projects/git/authServer.js +++ b/red/runtime/storage/localfilesystem/projects/git/authServer.js @@ -45,7 +45,7 @@ var ResponseServer = function(auth) { parts.push(data.substring(0, m)); data = data.substring(m); var line = parts.join(""); - console.log("LINE:",line); + // console.log("LINE:",line); parts = []; if (line==='Username') { connection.end(auth.username); @@ -90,7 +90,6 @@ var ResponseSSHServer = function(auth) { parts.push(data.substring(0, m)); data = data.substring(m); var line = parts.join(""); - console.log("LINE:",line); parts = []; if (line==='The') { // TODO: document these exchanges! diff --git a/red/runtime/storage/localfilesystem/projects/index.js b/red/runtime/storage/localfilesystem/projects/index.js index b4dac444f..86cdfb245 100644 --- a/red/runtime/storage/localfilesystem/projects/index.js +++ b/red/runtime/storage/localfilesystem/projects/index.js @@ -23,7 +23,7 @@ var crypto = require('crypto'); var storageSettings = require("../settings"); var util = require("../util"); var gitTools = require("./git"); -var sshTools = require("./ssh"); +var sshTools = require("./sshKeygen"); var Projects = require("./Project"); diff --git a/red/runtime/storage/localfilesystem/projects/ssh.js b/red/runtime/storage/localfilesystem/projects/sshKeygen.js similarity index 100% rename from red/runtime/storage/localfilesystem/projects/ssh.js rename to red/runtime/storage/localfilesystem/projects/sshKeygen.js diff --git a/test/red/runtime/storage/localfilesystem/projects/ssh_spec.js b/test/red/runtime/storage/localfilesystem/projects/sshKeygen_spec.js similarity index 99% rename from test/red/runtime/storage/localfilesystem/projects/ssh_spec.js rename to test/red/runtime/storage/localfilesystem/projects/sshKeygen_spec.js index c71d83268..0dab247ad 100644 --- a/test/red/runtime/storage/localfilesystem/projects/ssh_spec.js +++ b/test/red/runtime/storage/localfilesystem/projects/sshKeygen_spec.js @@ -19,7 +19,7 @@ var sinon = require("sinon"); var child_process = require('child_process'); 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() {