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

Allow credentials to be provided as part of /flows api

This commit is contained in:
Nick O'Leary 2020-02-13 16:44:48 +00:00
parent 634a51635c
commit d6b5494625
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
4 changed files with 80 additions and 16 deletions

View File

@ -57,6 +57,8 @@ var api = module.exports = {
* Sets the current flow configuration * Sets the current flow configuration
* @param {Object} opts * @param {Object} opts
* @param {User} opts.user - the user calling the api * @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) * @param {Object} opts.req - the request to log (optional)
* @return {Promise<Flows>} - the active flow configuration * @return {Promise<Flows>} - the active flow configuration
* @memberof @node-red/runtime_flows * @memberof @node-red/runtime_flows
@ -83,7 +85,7 @@ var api = module.exports = {
return reject(err); return reject(err);
} }
} }
apiPromise = runtime.nodes.setFlows(flows.flows,deploymentType); apiPromise = runtime.nodes.setFlows(flows.flows,flows.credentials,deploymentType);
} }
apiPromise.then(function(flowId) { apiPromise.then(function(flowId) {
return resolve({rev:flowId}); return resolve({rev:flowId});

View File

@ -106,15 +106,20 @@ function load(forceStart) {
// This is a force reload from the API - disable safeMode // This is a force reload from the API - disable safeMode
delete settings.safeMode; delete settings.safeMode;
} }
return setFlows(null,"load",false,forceStart); return setFlows(null,null,"load",false,forceStart);
} }
/* /*
* _config - new node array configuration * _config - new node array configuration
* _credentials - new credentials configuration (optional)
* type - full/nodes/flows/load (default full) * type - full/nodes/flows/load (default full)
* muteLog - don't emit the standard log messages (used for individual flow api) * 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"; type = type||"full";
if (settings.safeMode) { if (settings.safeMode) {
if (type !== "load") { if (type !== "load") {
@ -155,16 +160,27 @@ function setFlows(_config,type,muteLog,forceStart) {
delete newFlowConfig.allNodes[id].credentials; delete newFlowConfig.allNodes[id].credentials;
} }
} }
var credsDirty;
// Allow the credential store to remove anything no longer needed if (_credentials) {
credentials.clean(config); // 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 // Remember whether credentials need saving or not
var credsDirty = credentials.dirty(); var credsDirty = credentials.dirty();
configSavePromise = Promise.resolve();
}
// Get the latest credentials and ask storage to save them (if needed) // Get the latest credentials and ask storage to save them (if needed)
// as well as the new flow configuration. // 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 = { var saveConfig = {
flows: config, flows: config,
credentialsDirty:credsDirty, credentialsDirty:credsDirty,
@ -515,7 +531,7 @@ function addFlow(flow) {
var newConfig = clone(activeConfig.flows); var newConfig = clone(activeConfig.flows);
newConfig = newConfig.concat(nodes); 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+"]"})); log.info(log._("nodes.flows.added-flow",{label:(flow.label?flow.label+" ":"")+"["+flow.id+"]"}));
return flow.id; return flow.id;
}); });
@ -646,7 +662,7 @@ function updateFlow(id,newFlow) {
} }
newConfig = newConfig.concat(nodes); 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+"]"})); 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 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+"]"})); log.info(log._("nodes.flows.removed-flow",{label:(flow.label?flow.label+" ":"")+"["+flow.id+"]"}));
}); });
} }

View File

@ -53,7 +53,7 @@ describe("runtime-api/flows", function() {
var loadFlows; var loadFlows;
var reloadError = false; var reloadError = false;
beforeEach(function() { beforeEach(function() {
setFlows = sinon.spy(function(flows,type) { setFlows = sinon.spy(function(flows,credentials,type) {
if (flows[0] === "error") { if (flows[0] === "error") {
var err = new Error("error"); var err = new Error("error");
err.code = "error"; err.code = "error";
@ -91,7 +91,19 @@ describe("runtime-api/flows", function() {
result.should.eql({rev:"newRev"}); result.should.eql({rev:"newRev"});
setFlows.called.should.be.true(); setFlows.called.should.be.true();
setFlows.lastCall.args[0].should.eql([4,5,6]); 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(); done();
}).catch(done); }).catch(done);
}); });
@ -103,7 +115,7 @@ describe("runtime-api/flows", function() {
result.should.eql({rev:"newRev"}); result.should.eql({rev:"newRev"});
setFlows.called.should.be.true(); setFlows.called.should.be.true();
setFlows.lastCall.args[0].should.eql([4,5,6]); setFlows.lastCall.args[0].should.eql([4,5,6]);
setFlows.lastCall.args[1].should.eql("nodes"); setFlows.lastCall.args[2].should.eql("nodes");
done(); done();
}).catch(done); }).catch(done);
}); });
@ -125,7 +137,7 @@ describe("runtime-api/flows", function() {
result.should.eql({rev:"newRev"}); result.should.eql({rev:"newRev"});
setFlows.called.should.be.true(); setFlows.called.should.be.true();
setFlows.lastCall.args[0].should.eql([4,5,6]); setFlows.lastCall.args[0].should.eql([4,5,6]);
setFlows.lastCall.args[1].should.eql("nodes"); setFlows.lastCall.args[2].should.eql("nodes");
done(); done();
}).catch(done); }).catch(done);
}); });

View File

@ -67,7 +67,10 @@ describe('flows/index', function() {
}); });
return when.resolve(); 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(); return when.resolve();
}); });
flowCreate = sinon.stub(Flow,"create",function(parent, global, flow) { 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) { it('updates existing flows with partial deployment - nodes', function(done) {
var originalConfig = [ var originalConfig = [
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, {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() { describe('#load', function() {