diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/index.js b/packages/node_modules/@node-red/editor-api/lib/admin/index.js index ff32111f5..34c47b2cb 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/index.js @@ -50,12 +50,14 @@ module.exports = { // Nodes adminApp.get("/nodes",needsPermission("nodes.read"),nodes.getAll,apiUtil.errorHandler); - if (!settings.editorTheme || !settings.editorTheme.palette || settings.editorTheme.palette.upload !== false) { - const multer = require('multer'); - const upload = multer({ storage: multer.memoryStorage() }); - adminApp.post("/nodes",needsPermission("nodes.write"),upload.single("tarball"),nodes.post,apiUtil.errorHandler); - } else { - adminApp.post("/nodes",needsPermission("nodes.write"),nodes.post,apiUtil.errorHandler); + if (!settings.externalModules || !settings.externalModules.palette || settings.externalModules.palette.allowInstall !== false) { + if (!settings.externalModules || !settings.externalModules.palette || settings.externalModules.palette.allowUpload !== false) { + const multer = require('multer'); + const upload = multer({ storage: multer.memoryStorage() }); + adminApp.post("/nodes",needsPermission("nodes.write"),upload.single("tarball"),nodes.post,apiUtil.errorHandler); + } else { + adminApp.post("/nodes",needsPermission("nodes.write"),nodes.post,apiUtil.errorHandler); + } } adminApp.get(/^\/nodes\/messages/,needsPermission("nodes.read"),nodes.getModuleCatalogs,apiUtil.errorHandler); adminApp.get(/^\/nodes\/((@[^\/]+\/)?[^\/]+\/[^\/]+)\/messages/,needsPermission("nodes.read"),nodes.getModuleCatalog,apiUtil.errorHandler); diff --git a/packages/node_modules/@node-red/registry/lib/installer.js b/packages/node_modules/@node-red/registry/lib/installer.js index f94ebbad4..bde1f0f3d 100644 --- a/packages/node_modules/@node-red/registry/lib/installer.js +++ b/packages/node_modules/@node-red/registry/lib/installer.js @@ -15,26 +15,42 @@ **/ -var path = require("path"); -var os = require("os"); -var fs = require("fs-extra"); -var tar = require("tar"); +const path = require("path"); +const os = require("os"); +const fs = require("fs-extra"); +const tar = require("tar"); -var registry = require("./registry"); -var library = require("./library"); +const registry = require("./registry"); +const registryUtil = require("./util"); +const library = require("./library"); const {exec,log,events} = require("@node-red/util"); -var child_process = require('child_process'); -var npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; -var installerEnabled = false; +const child_process = require('child_process'); +const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; +let installerEnabled = false; -var settings; +let settings; const moduleRe = /^(@[^/@]+?[/])?[^/@]+?$/; const slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/; const pkgurlRe = /^(https?|git(|\+https?|\+ssh|\+file)):\/\//; const localtgzRe = /^([a-zA-Z]:|\/).+tgz$/; +// Default allow/deny lists +let installAllowList = ['*']; +let installDenyList = []; + + function init(_settings) { settings = _settings; + // TODO: This is duplicated in localfilesystem.js + // Should it *all* be managed by util? + 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; + } + } + installAllowList = registryUtil.parseModuleList(installAllowList); + installDenyList = registryUtil.parseModuleList(installDenyList); } var activePromise = Promise.resolve(); @@ -118,6 +134,12 @@ function installModule(module,version,url) { reject(e); return; } + if (!registryUtil.checkModuleAllowed(module,version,installAllowList,installDenyList)) { + const e = new Error("Install not allowed"); + e.code = "install_not_allowed"; + reject(e); + return + } isUpgrade = checkExistingModule(module,version); } catch(err) { return reject(err); @@ -215,6 +237,10 @@ async function getExistingPackageVersion(moduleName) { } async function installTarball(tarball) { + if (settings.externalModules && settings.externalModules.palette && settings.externalModules.palette.allowUpload === false) { + throw new Error("Module upload disabled") + } + // Check this tarball contains a valid node-red module. // Get its module name/version const moduleInfo = await getTarballModuleInfo(tarball); diff --git a/packages/node_modules/@node-red/registry/lib/localfilesystem.js b/packages/node_modules/@node-red/registry/lib/localfilesystem.js index 7aea3f57b..25e423571 100644 --- a/packages/node_modules/@node-red/registry/lib/localfilesystem.js +++ b/packages/node_modules/@node-red/registry/lib/localfilesystem.js @@ -14,10 +14,16 @@ * limitations under the License. **/ -var fs = require("fs"); -var path = require("path"); -var log = require("@node-red/util").log; -var i18n = require("@node-red/util").i18n; +const fs = require("fs"); +const path = require("path"); +const log = require("@node-red/util").log; +const i18n = require("@node-red/util").i18n; +const registryUtil = require("./util"); + +// Default allow/deny lists +let loadAllowList = ['*']; +let loadDenyList = []; + var settings; var disableNodePathScan = false; @@ -25,6 +31,16 @@ var iconFileExtensions = [".png", ".gif", ".svg"]; function init(_settings) { settings = _settings; + // TODO: This is duplicated in installer.js + // Should it *all* be managed by util? + if (settings.externalModules && settings.externalModules.palette) { + if (settings.externalModules.palette.allowList || settings.externalModules.palette.denyList) { + loadAllowList = settings.externalModules.palette.allowList; + loadDenyList = settings.externalModules.palette.denyList; + } + } + loadAllowList = registryUtil.parseModuleList(loadAllowList); + loadDenyList = registryUtil.parseModuleList(loadDenyList); } function isIncluded(name) { @@ -137,8 +153,12 @@ function scanDirForNodesModules(dir,moduleName) { try { var pkg = require(pkgfn); if (pkg['node-red']) { - var moduleDir = path.join(dir,fn); - results.push({dir:moduleDir,package:pkg}); + if (!registryUtil.checkModuleAllowed(pkg.name,pkg.version,loadAllowList,loadDenyList)) { + log.debug("! Module: "+pkg.name+" "+pkg.version+ " *ignored due to denyList*"); + } else { + var moduleDir = path.join(dir,fn); + results.push({dir:moduleDir,package:pkg}); + } } } catch(err) { if (err.code != "MODULE_NOT_FOUND") { @@ -308,8 +328,7 @@ function getNodeFiles(disableNodePathScan) { } else { result = false; } - log.debug("Module: "+mod.package.name+" "+mod.package.version+(result?"":" *ignored due to local copy*")); - log.debug(" "+mod.dir); + log.debug((result?"":"! ")+"Module: "+mod.package.name+" "+mod.package.version+" "+mod.dir+(result?"":" *ignored due to local copy*")); return result; }); diff --git a/packages/node_modules/@node-red/registry/lib/util.js b/packages/node_modules/@node-red/registry/lib/util.js index dbb6c6fc7..6e7609fd5 100644 --- a/packages/node_modules/@node-red/registry/lib/util.js +++ b/packages/node_modules/@node-red/registry/lib/util.js @@ -15,6 +15,7 @@ **/ const path = require("path"); +const semver = require("semver"); const {events,i18n,log} = require("@node-red/util"); var runtime; @@ -104,9 +105,78 @@ function createNodeApi(node) { return red; } + +function checkAgainstList(module,version,list) { + for (let i=0;i deniedRule.wildcardPos + } else { + // First wildcard in same position. + // Go with the longer matching rule. This isn't going to be 100% + // right, but we are deep into edge cases at this point. + return allowedRule.module.toString().length > deniedRule.module.toString().length + } + return false; +} + +function parseModuleList(list) { + list = list || ["*"]; + return list.map(rule => { + let m = /^(.+?)(?:@(.*))?$/.exec(rule); + let wildcardPos = m[1].indexOf("*"); + wildcardPos = wildcardPos===-1?Infinity:wildcardPos; + + return { + module: new RegExp("^"+m[1].replace(/\*/g,".*")+"$"), + version: m[2], + wildcardPos: wildcardPos + } + }) +} + + module.exports = { init: function(_runtime) { runtime = _runtime; }, - createNodeApi: createNodeApi + createNodeApi: createNodeApi, + parseModuleList: parseModuleList, + checkModuleAllowed: checkModuleAllowed } diff --git a/packages/node_modules/@node-red/runtime/lib/api/nodes.js b/packages/node_modules/@node-red/runtime/lib/api/nodes.js index 9ac83c814..ba4f9874b 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/nodes.js +++ b/packages/node_modules/@node-red/runtime/lib/api/nodes.js @@ -164,7 +164,7 @@ var api = module.exports = { throw err; } if (opts.tarball) { - if (runtime.settings.editorTheme && runtime.settings.editorTheme.palette && runtime.settings.editorTheme.palette.upload === false) { + if (runtime.settings.externalModules && runtime.settings.externalModules.palette && runtime.settings.externalModules.palette.upload === false) { runtime.log.audit({event: "nodes.install",tarball:opts.tarball.file,error:"invalid_request"}, opts.req); var err = new Error("Invalid request"); err.code = "invalid_request"; diff --git a/test/unit/@node-red/registry/lib/util_spec.js b/test/unit/@node-red/registry/lib/util_spec.js index a82519d11..d2384e5dc 100644 --- a/test/unit/@node-red/registry/lib/util_spec.js +++ b/test/unit/@node-red/registry/lib/util_spec.js @@ -14,7 +14,54 @@ * limitations under the License. **/ +const should = require("should"); +const NR_TEST_UTILS = require("nr-test-utils"); +const registryUtil = NR_TEST_UTILS.require("@node-red/registry/lib/util"); + describe("red/nodes/registry/util",function() { - it.skip("NEEDS TESTS"); + describe("createNodeApi", function() { + it.skip("needs tests"); + }); + describe("checkModuleAllowed", function() { + function checkList(module, version, allowList, denyList) { + return registryUtil.checkModuleAllowed( + module, + version, + registryUtil.parseModuleList(allowList), + registryUtil.parseModuleList(denyList) + ) + } + + it("allows module with no allow/deny list provided", function() { + checkList("abc","1.2.3",[],[]).should.be.true(); + }) + it("defaults allow to * when only deny list is provided", function() { + checkList("abc","1.2.3",["*"],["def"]).should.be.true(); + checkList("def","1.2.3",["*"],["def"]).should.be.false(); + }) + it("uses most specific matching rule", function() { + checkList("abc","1.2.3",["ab*"],["a*"]).should.be.true(); + checkList("def","1.2.3",["d*"],["de*"]).should.be.false(); + }) + it("checks version string using semver rules", function() { + // Deny + checkList("abc","1.2.3",["abc@1.2.2"],["*"]).should.be.false(); + checkList("abc","1.2.3",["abc@1.2.4"],["*"]).should.be.false(); + checkList("abc","1.2.3",["abc@>1.2.3"],["*"]).should.be.false(); + checkList("abc","1.2.3",["abc@>=1.2.3"],["abc"]).should.be.false(); + + + checkList("node-red-contrib-foo","1.2.3",["*"],["*contrib*"]).should.be.false(); + + + // Allow + checkList("abc","1.2.3",["abc@1.2.3"],["*"]).should.be.true(); + checkList("abc","1.2.3",["abc@<1.2.4"],["*"]).should.be.true(); + checkList("abc","1.2.3",["abc"],["abc@>1.2.3"]).should.be.true(); + checkList("abc","1.2.3",["abc"],["abc@<1.2.3||>1.2.3"]).should.be.true(); + checkList("node-red-contrib-foo","1.2.3",["*contrib*"],["*"]).should.be.true(); + }) + + }) });