diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js index ff8b06e14..34d3ba160 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js @@ -333,7 +333,10 @@ RED.palette.editor = (function() { nodeEntry.versionSpan.html(moduleInfo.version+' '+moduleInfo.pending_version).appendTo(nodeEntry.metaRow) nodeEntry.updateButton.text(RED._('palette.editor.updated')).addClass('disabled').css('display', 'inline-block'); } else if (loadedIndex.hasOwnProperty(module)) { - if (semVerCompare(loadedIndex[module].version,moduleInfo.version) > 0) { + if (updateAllowed && + semVerCompare(loadedIndex[module].version,moduleInfo.version) > 0 && + RED.utils.checkModuleAllowed(module,null,updateAllowList,updateDenyList) + ) { nodeEntry.updateButton.show(); nodeEntry.updateButton.text(RED._('palette.editor.update',{version:loadedIndex[module].version})); } else { @@ -484,6 +487,9 @@ RED.palette.editor = (function() { var installAllowList = ['*']; var installDenyList = []; + var updateAllowed = true; + var updateAllowList = ['*']; + var updateDenyList = []; function init() { if (RED.settings.get('externalModules.palette.allowInstall', true) === false) { @@ -498,6 +504,17 @@ RED.palette.editor = (function() { installAllowList = RED.utils.parseModuleList(installAllowList); installDenyList = RED.utils.parseModuleList(installDenyList); + var settingsUpdateAllowList = RED.settings.get("externalModules.palette.allowUpdateList") + var settingsUpdateDenyList = RED.settings.get("externalModules.palette.denyUpdateList") + if (settingsUpdateAllowList || settingsUpdateDenyList) { + updateAllowList = settingsUpdateAllowList; + updateDenyList = settingsUpdateDenyList; + } + updateAllowList = RED.utils.parseModuleList(updateAllowList); + updateDenyList = RED.utils.parseModuleList(updateDenyList); + updateAllowed = RED.settings.get("externalModules.palette.allowUpdate",true); + + createSettingsPane(); RED.userSettings.add({ diff --git a/packages/node_modules/@node-red/registry/lib/installer.js b/packages/node_modules/@node-red/registry/lib/installer.js index e06beeeef..88846bac4 100644 --- a/packages/node_modules/@node-red/registry/lib/installer.js +++ b/packages/node_modules/@node-red/registry/lib/installer.js @@ -40,15 +40,42 @@ let installDenyList = []; let installAllAllowed = true; let installVersionRestricted = false; +let updateAllowed = true; +let updateAllowList = ['*']; +let updateDenyList = []; +let updateAllAllowed = true; + function init(_settings) { settings = _settings; // TODO: This is duplicated in localfilesystem.js // Should it *all* be managed by util? + + installAllowList = ['*']; + installDenyList = []; + installAllAllowed = true; + installVersionRestricted = false; + + updateAllowed = true; + updateAllowList = ['*']; + updateDenyList = []; + updateAllAllowed = true; + if (settings.externalModules && settings.externalModules.palette) { + if (settings.externalModules.palette.allowList || settings.externalModules.palette.denyList) { installAllowList = settings.externalModules.palette.allowList; installDenyList = settings.externalModules.palette.denyList; } + + if (settings.externalModules.palette.hasOwnProperty('allowUpdate')) { + updateAllowed = !!settings.externalModules.palette.allowUpdate; + } + if (settings.externalModules.palette.allowUpdateList || settings.externalModules.palette.denyUpdateList) { + updateAllowList = settings.externalModules.palette.allowUpdateList; + updateDenyList = settings.externalModules.palette.denyUpdateList; + } + + } installAllowList = registryUtil.parseModuleList(installAllowList); installDenyList = registryUtil.parseModuleList(installDenyList); @@ -64,6 +91,10 @@ function init(_settings) { } } + + updateAllowList = registryUtil.parseModuleList(updateAllowList); + updateDenyList = registryUtil.parseModuleList(updateDenyList); + updateAllAllowed = updateAllowed ? updateDenyList.length === 0 : false; } var activePromise = Promise.resolve(); @@ -161,6 +192,15 @@ async function installModule(module,version,url) { isUpgrade = false; } + if (isUpgrade && !updateAllAllowed) { + // Check this module is allowed to be upgraded... + if (!updateAllowed || !registryUtil.checkModuleAllowed(module,null,updateAllowList,updateDenyList)) { + const e = new Error("Update not allowed"); + e.code = "update_not_allowed"; + throw e; + } + } + if (!isUpgrade) { log.info(log._("server.install.installing",{name: module,version: version||"latest"})); } else { diff --git a/test/unit/@node-red/registry/lib/installer_spec.js b/test/unit/@node-red/registry/lib/installer_spec.js index 63075ff24..80b96ecfb 100644 --- a/test/unit/@node-red/registry/lib/installer_spec.js +++ b/test/unit/@node-red/registry/lib/installer_spec.js @@ -63,7 +63,9 @@ describe('nodes/registry/installer', function() { if (typeRegistry.getModuleInfo.restore) { typeRegistry.getModuleInfo.restore(); } - + if (typeRegistry.setModulePendingUpdated.restore) { + typeRegistry.setModulePendingUpdated.restore(); + } if (fs.statSync.restore) { fs.statSync.restore(); } @@ -298,8 +300,11 @@ describe('nodes/registry/installer', function() { hooks.add("preInstall", function(event) { return false }) hooks.add("postInstall", function(event) { receivedEvent = event; }) var nodeInfo = {nodes:{module:"foo",types:["a"]}}; + var addModule = sinon.stub(registry,"addModule").callsFake(function(md) { + return Promise.resolve(nodeInfo); + }); - installer.installModule("this_wont_exist","1.2.3").catch(function(err) { + installer.installModule("this_wont_exist","1.2.3").then(function() { exec.run.called.should.be.false(); should.exist(receivedEvent); done(); @@ -316,6 +321,147 @@ describe('nodes/registry/installer', function() { }).catch(done); }); + describe("allowUpdate lists", function() { + it("rejects when update requested with allowUpdate set to false", function(done) { + installer.init({ externalModules: { palette: { allowUpdate: false } } }) + sinon.stub(typeRegistry,"getModuleInfo").callsFake(function() { + return { + user: true, + version: "0.1.1" + } + }); + installer.installModule("this_wont_exist","0.1.2").catch(function(err) { + err.code.should.be.eql('update_not_allowed'); + done(); + }).catch(done); + }) + it("succeeds when update requested with module not on denyUpdateList", function(done) { + installer.init({ externalModules: { palette: { denyUpdateList: ['this_wont_exist'] } } }) + sinon.stub(typeRegistry,"getModuleInfo").callsFake(function() { + return { + user: true, + version: "0.1.1" + } + }); + + var res = { + code: 0, + stdout:"", + stderr:"" + } + var p = Promise.resolve(res); + p.catch((err)=>{}); + execResponse = p; + + var nodeInfo = {nodes:{module:"this_is_allowed",types:["a"]}}; + + var addModule = sinon.stub(registry,"addModule").callsFake(function(md) { + return Promise.resolve(nodeInfo); + }); + sinon.stub(typeRegistry,"setModulePendingUpdated").callsFake(function() { + return Promise.resolve(nodeInfo); + }); + + installer.installModule("this_is_allowed","0.1.2").then(function() { + done(); + }).catch(done); + }) + it("rejects when update requested with module on denyUpdateList", function(done) { + installer.init({ externalModules: { palette: { denyUpdateList: ['this_wont_exist'] } } }) + sinon.stub(typeRegistry,"getModuleInfo").callsFake(function() { + return { + user: true, + version: "0.1.1" + } + }); + + var res = { + code: 0, + stdout:"", + stderr:"" + } + var p = Promise.resolve(res); + p.catch((err)=>{}); + execResponse = p; + + var nodeInfo = {nodes:{module:"this_is_allowed",types:["a"]}}; + + var addModule = sinon.stub(registry,"addModule").callsFake(function(md) { + return Promise.resolve(nodeInfo); + }); + sinon.stub(typeRegistry,"setModulePendingUpdated").callsFake(function() { + return Promise.resolve(nodeInfo); + }); + + installer.installModule("this_wont_exist","0.1.2").catch(function(err) { + err.code.should.be.eql('update_not_allowed'); + done(); + }).catch(done); + }) + it("succeeds when update requested with module on allowUpdateList", function(done) { + installer.init({ externalModules: { palette: { allowUpdateList: ['this_is_allowed'] } } }) + sinon.stub(typeRegistry,"getModuleInfo").callsFake(function() { + return { + user: true, + version: "0.1.1" + } + }); + + var res = { + code: 0, + stdout:"", + stderr:"" + } + var p = Promise.resolve(res); + p.catch((err)=>{}); + execResponse = p; + + var nodeInfo = {nodes:{module:"this_is_allowed",types:["a"]}}; + + var addModule = sinon.stub(registry,"addModule").callsFake(function(md) { + return Promise.resolve(nodeInfo); + }); + sinon.stub(typeRegistry,"setModulePendingUpdated").callsFake(function() { + return Promise.resolve(nodeInfo); + }); + + installer.installModule("this_is_allowed","0.1.2").then(function() { + done(); + }).catch(done); + }) + it("rejects when update requested with module not on allowUpdateList", function(done) { + installer.init({ externalModules: { palette: { allowUpdateList: ['this_is_allowed'] } } }) + sinon.stub(typeRegistry,"getModuleInfo").callsFake(function() { + return { + user: true, + version: "0.1.1" + } + }); + + var res = { + code: 0, + stdout:"", + stderr:"" + } + var p = Promise.resolve(res); + p.catch((err)=>{}); + execResponse = p; + + var nodeInfo = {nodes:{module:"this_wont_exist",types:["a"]}}; + + var addModule = sinon.stub(registry,"addModule").callsFake(function(md) { + return Promise.resolve(nodeInfo); + }); + sinon.stub(typeRegistry,"setModulePendingUpdated").callsFake(function() { + return Promise.resolve(nodeInfo); + }); + + installer.installModule("this_wont_exist","0.1.2").catch(function(err) { + err.code.should.be.eql('update_not_allowed'); + done(); + }).catch(done); + }) + }); }); describe("uninstalls module", function() { it("rejects invalid module names", function(done) {