From d6b5494625f3eb67a6c6b3e2a0d9636125c89db1 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 13 Feb 2020 16:44:48 +0000 Subject: [PATCH] Allow credentials to be provided as part of /flows api --- .../@node-red/runtime/lib/api/flows.js | 4 ++- .../runtime/lib/nodes/flows/index.js | 36 +++++++++++++------ .../@node-red/runtime/lib/api/flows_spec.js | 20 ++++++++--- .../runtime/lib/nodes/flows/index_spec.js | 36 ++++++++++++++++++- 4 files changed, 80 insertions(+), 16 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/api/flows.js b/packages/node_modules/@node-red/runtime/lib/api/flows.js index 1e6e72dc7..e58531190 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/flows.js +++ b/packages/node_modules/@node-red/runtime/lib/api/flows.js @@ -57,6 +57,8 @@ var api = module.exports = { * Sets the current flow configuration * @param {Object} opts * @param {User} opts.user - the user calling the api + * @param {Object} opts.flows - the flow configuration: `{flows: [..], credentials: {}}` + * @param {Object} opts.deploymentType - the type of deployment - "full", "nodes", "flows", "reload" * @param {Object} opts.req - the request to log (optional) * @return {Promise} - the active flow configuration * @memberof @node-red/runtime_flows @@ -83,7 +85,7 @@ var api = module.exports = { return reject(err); } } - apiPromise = runtime.nodes.setFlows(flows.flows,deploymentType); + apiPromise = runtime.nodes.setFlows(flows.flows,flows.credentials,deploymentType); } apiPromise.then(function(flowId) { return resolve({rev:flowId}); diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/flows/index.js b/packages/node_modules/@node-red/runtime/lib/nodes/flows/index.js index 981ed7173..16166fbc1 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/flows/index.js @@ -106,15 +106,20 @@ function load(forceStart) { // This is a force reload from the API - disable safeMode delete settings.safeMode; } - return setFlows(null,"load",false,forceStart); + return setFlows(null,null,"load",false,forceStart); } /* * _config - new node array configuration + * _credentials - new credentials configuration (optional) * type - full/nodes/flows/load (default full) * muteLog - don't emit the standard log messages (used for individual flow api) */ -function setFlows(_config,type,muteLog,forceStart) { +function setFlows(_config,_credentials,type,muteLog,forceStart) { + if (typeof _credentials === "string") { + type = _credentials; + _credentials = null; + } type = type||"full"; if (settings.safeMode) { if (type !== "load") { @@ -155,16 +160,27 @@ function setFlows(_config,type,muteLog,forceStart) { delete newFlowConfig.allNodes[id].credentials; } } + var credsDirty; - // Allow the credential store to remove anything no longer needed - credentials.clean(config); + if (_credentials) { + // A full set of credentials have been provided. Use those instead + configSavePromise = credentials.load(_credentials); + credsDirty = true; + } else { + // Allow the credential store to remove anything no longer needed + credentials.clean(config); - // Remember whether credentials need saving or not - var credsDirty = credentials.dirty(); + // Remember whether credentials need saving or not + var credsDirty = credentials.dirty(); + + configSavePromise = Promise.resolve(); + } // Get the latest credentials and ask storage to save them (if needed) // as well as the new flow configuration. - configSavePromise = credentials.export().then(function(creds) { + configSavePromise = configSavePromise.then(function() { + return credentials.export() + }).then(function(creds) { var saveConfig = { flows: config, credentialsDirty:credsDirty, @@ -515,7 +531,7 @@ function addFlow(flow) { var newConfig = clone(activeConfig.flows); newConfig = newConfig.concat(nodes); - return setFlows(newConfig,'flows',true).then(function() { + return setFlows(newConfig,null,'flows',true).then(function() { log.info(log._("nodes.flows.added-flow",{label:(flow.label?flow.label+" ":"")+"["+flow.id+"]"})); return flow.id; }); @@ -646,7 +662,7 @@ function updateFlow(id,newFlow) { } newConfig = newConfig.concat(nodes); - return setFlows(newConfig,'flows',true).then(function() { + return setFlows(newConfig,null,'flows',true).then(function() { log.info(log._("nodes.flows.updated-flow",{label:(label?label+" ":"")+"["+id+"]"})); }) } @@ -668,7 +684,7 @@ function removeFlow(id) { return node.z !== id && node.id !== id; }); - return setFlows(newConfig,'flows',true).then(function() { + return setFlows(newConfig,null,'flows',true).then(function() { log.info(log._("nodes.flows.removed-flow",{label:(flow.label?flow.label+" ":"")+"["+flow.id+"]"})); }); } diff --git a/test/unit/@node-red/runtime/lib/api/flows_spec.js b/test/unit/@node-red/runtime/lib/api/flows_spec.js index a7c85efa2..dafbbc69b 100644 --- a/test/unit/@node-red/runtime/lib/api/flows_spec.js +++ b/test/unit/@node-red/runtime/lib/api/flows_spec.js @@ -53,7 +53,7 @@ describe("runtime-api/flows", function() { var loadFlows; var reloadError = false; beforeEach(function() { - setFlows = sinon.spy(function(flows,type) { + setFlows = sinon.spy(function(flows,credentials,type) { if (flows[0] === "error") { var err = new Error("error"); err.code = "error"; @@ -91,7 +91,19 @@ describe("runtime-api/flows", function() { result.should.eql({rev:"newRev"}); setFlows.called.should.be.true(); setFlows.lastCall.args[0].should.eql([4,5,6]); - setFlows.lastCall.args[1].should.eql("full"); + setFlows.lastCall.args[2].should.eql("full"); + done(); + }).catch(done); + }); + it("includes credentials when part of the request", function(done) { + flows.setFlows({ + flows: {flows:[4,5,6], credentials: {$:"creds"}}, + }).then(function(result) { + result.should.eql({rev:"newRev"}); + setFlows.called.should.be.true(); + setFlows.lastCall.args[0].should.eql([4,5,6]); + setFlows.lastCall.args[1].should.eql({$:"creds"}); + setFlows.lastCall.args[2].should.eql("full"); done(); }).catch(done); }); @@ -103,7 +115,7 @@ describe("runtime-api/flows", function() { result.should.eql({rev:"newRev"}); setFlows.called.should.be.true(); setFlows.lastCall.args[0].should.eql([4,5,6]); - setFlows.lastCall.args[1].should.eql("nodes"); + setFlows.lastCall.args[2].should.eql("nodes"); done(); }).catch(done); }); @@ -125,7 +137,7 @@ describe("runtime-api/flows", function() { result.should.eql({rev:"newRev"}); setFlows.called.should.be.true(); setFlows.lastCall.args[0].should.eql([4,5,6]); - setFlows.lastCall.args[1].should.eql("nodes"); + setFlows.lastCall.args[2].should.eql("nodes"); done(); }).catch(done); }); diff --git a/test/unit/@node-red/runtime/lib/nodes/flows/index_spec.js b/test/unit/@node-red/runtime/lib/nodes/flows/index_spec.js index a37d88f78..8865c5d3f 100644 --- a/test/unit/@node-red/runtime/lib/nodes/flows/index_spec.js +++ b/test/unit/@node-red/runtime/lib/nodes/flows/index_spec.js @@ -67,7 +67,10 @@ describe('flows/index', function() { }); return when.resolve(); }); - credentialsLoad = sinon.stub(credentials,"load",function() { + credentialsLoad = sinon.stub(credentials,"load",function(creds) { + if (creds && creds.hasOwnProperty("$") && creds['$'] === "fail") { + return when.reject("creds error"); + } return when.resolve(); }); flowCreate = sinon.stub(Flow,"create",function(parent, global, flow) { @@ -177,6 +180,23 @@ describe('flows/index', function() { }); }); + it('sets the full flow including credentials', function(done) { + var originalConfig = [ + {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, + {id:"t1",type:"tab"} + ]; + var credentials = {"t1-1":{"a":1}}; + + flows.init({log:mockLog, settings:{},storage:storage}); + flows.setFlows(originalConfig,credentials).then(function() { + credentialsClean.called.should.be.false(); + credentialsLoad.called.should.be.true(); + credentialsLoad.lastCall.args[0].should.eql(credentials); + flows.getFlows().flows.should.eql(originalConfig); + done(); + }); + }); + it('updates existing flows with partial deployment - nodes', function(done) { var originalConfig = [ {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, @@ -235,6 +255,20 @@ describe('flows/index', function() { }); }); + it('returns error if it cannot decrypt credentials', function(done) { + var originalConfig = [ + {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, + {id:"t1",type:"tab"} + ]; + var credentials = {"$":"fail"}; + + flows.init({log:mockLog, settings:{},storage:storage}); + flows.setFlows(originalConfig,credentials).then(function() { + done("Unexpected success when credentials couldn't be decrypted") + }).catch(function(err) { + done(); + }); + }); }); describe('#load', function() {