From 923893e16066bc2b7df9de62b98fb3191320ae9b Mon Sep 17 00:00:00 2001 From: Hideki Nakamura Date: Thu, 7 Dec 2017 23:11:24 +0900 Subject: [PATCH] Add SSH key management API --- package.json | 98 ++-- red/api/editor/index.js | 4 + red/api/editor/sshkeys.js | 125 ++++++ red/runtime/storage/index.js | 3 + red/runtime/storage/localfilesystem/index.js | 5 +- .../storage/localfilesystem/sshkeys.js | 152 +++++++ test/red/api/editor/sshkeys_spec.js | 377 ++++++++++++++++ .../storage/localfilesystem/sshkeys_spec.js | 422 ++++++++++++++++++ 8 files changed, 1140 insertions(+), 46 deletions(-) create mode 100644 red/api/editor/sshkeys.js create mode 100644 red/runtime/storage/localfilesystem/sshkeys.js create mode 100644 test/red/api/editor/sshkeys_spec.js create mode 100644 test/red/runtime/storage/localfilesystem/sshkeys_spec.js diff --git a/package.json b/package.json index 044d485de..4e18bad23 100644 --- a/package.json +++ b/package.json @@ -1,92 +1,100 @@ { - "name" : "node-red", - "version" : "0.17.5", - "description" : "A visual tool for wiring the Internet of Things", - "homepage" : "http://nodered.org", - "license" : "Apache-2.0", - "repository" : { - "type":"git", - "url":"https://github.com/node-red/node-red.git" + "name": "node-red", + "version": "0.17.5", + "description": "A visual tool for wiring the Internet of Things", + "homepage": "http://nodered.org", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/node-red/node-red.git" }, - "main" : "red/red.js", - "scripts" : { + "main": "red/red.js", + "scripts": { "start": "node red.js", "test": "grunt", "build": "grunt build" }, - "bin" : { + "bin": { "node-red": "./red.js", "node-red-pi": "bin/node-red-pi" }, "contributors": [ - {"name": "Nick O'Leary"}, - {"name": "Dave Conway-Jones"} + { + "name": "Nick O'Leary" + }, + { + "name": "Dave Conway-Jones" + } ], "keywords": [ - "editor", "messaging", "iot", "flow" + "editor", + "messaging", + "iot", + "flow" ], "dependencies": { "basic-auth": "1.1.0", "bcryptjs": "2.4.3", "body-parser": "1.17.2", - "cheerio":"0.22.0", + "cheerio": "0.22.0", "clone": "2.1.1", "cookie": "0.3.1", "cookie-parser": "1.4.3", - "cors":"2.8.3", - "cron":"1.2.1", + "cors": "2.8.3", + "cron": "1.2.1", "express": "4.15.3", "express-session": "1.15.2", - "follow-redirects":"1.2.4", + "follow-redirects": "1.2.4", "fs-extra": "4.0.2", - "fs.notify":"0.0.4", - "hash-sum":"1.0.2", - "i18next":"1.10.6", - "is-utf8":"0.2.1", + "fs.notify": "0.0.4", + "hash-sum": "1.0.2", + "i18next": "1.10.6", + "is-utf8": "0.2.1", "js-yaml": "3.8.4", - "json-stringify-safe":"5.0.1", - "jsonata":"1.2.6", + "json-stringify-safe": "5.0.1", + "jsonata": "1.2.6", "media-typer": "0.3.0", "mqtt": "2.9.0", "multer": "1.3.0", "mustache": "2.3.0", + "node-red-node-email": "0.1.*", + "node-red-node-feedparser": "0.1.*", + "node-red-node-rbe": "0.1.*", + "node-red-node-twitter": "0.1.*", "nopt": "3.0.6", - "oauth2orize":"1.8.0", - "on-headers":"1.0.1", - "passport":"0.3.2", - "passport-http-bearer":"1.0.1", - "passport-oauth2-client-password":"0.1.2", - "raw-body":"2.2.0", + "oauth2orize": "1.8.0", + "on-headers": "1.0.1", + "passport": "0.3.2", + "passport-http-bearer": "1.0.1", + "passport-oauth2-client-password": "0.1.2", + "raw-body": "2.2.0", "semver": "5.3.0", - "sentiment":"2.1.0", - "uglify-js":"3.0.20", + "sentiment": "2.1.0", + "ssh-keygen": "^0.4.1", + "uglify-js": "3.0.20", "when": "3.7.8", "ws": "1.1.1", - "xml2js":"0.4.17", - "node-red-node-feedparser":"0.1.*", - "node-red-node-email":"0.1.*", - "node-red-node-twitter":"0.1.*", - "node-red-node-rbe":"0.1.*" + "xml2js": "0.4.17" }, "optionalDependencies": { - "bcrypt":"~1.0.1" + "bcrypt": "~1.0.1" }, "devDependencies": { "grunt": "~1.0.1", "grunt-chmod": "~1.1.1", "grunt-cli": "~1.2.0", - "grunt-concurrent":"~2.3.1", - "grunt-contrib-clean":"~1.1.0", + "grunt-concurrent": "~2.3.1", + "grunt-contrib-clean": "~1.1.0", "grunt-contrib-compress": "~1.4.0", - "grunt-contrib-concat":"~1.0.1", + "grunt-contrib-concat": "~1.0.1", "grunt-contrib-copy": "~1.0.0", "grunt-contrib-jshint": "~1.1.0", "grunt-contrib-uglify": "~3.0.1", - "grunt-contrib-watch":"~1.0.0", - "grunt-jsonlint":"~1.1.0", + "grunt-contrib-watch": "~1.0.0", + "grunt-jsonlint": "~1.1.0", "grunt-mocha-istanbul": "5.0.2", - "grunt-nodemon":"~0.4.2", - "grunt-sass":"~1.2.1", + "grunt-nodemon": "~0.4.2", + "grunt-sass": "~1.2.1", "grunt-simple-mocha": "~0.4.1", "istanbul": "0.4.5", "mocha": "~3.4.2", diff --git a/red/api/editor/index.js b/red/api/editor/index.js index b9f4d539a..21cc29b9e 100644 --- a/red/api/editor/index.js +++ b/red/api/editor/index.js @@ -97,6 +97,10 @@ module.exports = { // User Settings editorApp.post("/settings/user",needsPermission("settings.write"),info.updateUserSettings,apiUtil.errorHandler); + // SSH keys + var sshkeys = require("./sshkeys"); + sshkeys.init(runtime); + editorApp.use("/settings/user/keys",sshkeys.app()); return editorApp; } diff --git a/red/api/editor/sshkeys.js b/red/api/editor/sshkeys.js new file mode 100644 index 000000000..5a9506555 --- /dev/null +++ b/red/api/editor/sshkeys.js @@ -0,0 +1,125 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var express = require("express"); +var os = require("os"); +var runtime; +var settings; +var needsPermission = require("../auth").needsPermission; + +function getUsername(userObj) { + var username = os.hostname(); + if ( userObj && userObj.name ) { + username = userObj.name; + } + return username; +} + +module.exports = { + init: function(_runtime) { + runtime = _runtime; + settings = runtime.settings; + }, + app: function() { + var app = express(); + + // SSH keys + + // List all SSH keys + app.get("/", needsPermission("settings.read"), function(req,res) { + var username = getUsername(req.user); + runtime.storage.sshkeys.listSSHKeys(username) + .then(function(list) { + res.json({ + keys: list + }); + }) + .catch(function(err) { + // console.log(err.stack); + if (err.code) { + res.status(400).json({error:err.code, message: err.message}); + } else { + res.status(400).json({error:"unexpected_error", message:err.toString()}); + } + }); + }); + + // Get SSH key detail + app.get("/:id", needsPermission("settings.read"), function(req,res) { + var username = getUsername(req.user); + console.log('username:', username); + runtime.storage.sshkeys.getSSHKey(username, req.params.id) + .then(function(data) { + res.json({ + publickey: data + }); + }) + .catch(function(err) { + console.log(err.stack); + if (err.code) { + res.status(400).json({error:err.code, message: err.message}); + } else { + res.status(400).json({error:"unexpected_error", message:err.toString()}); + } + }); + }); + + // Generate a SSH key + app.post("/", needsPermission("settings.write"), function(req,res) { + var username = getUsername(req.user); + console.log('req.body:', req.body); + if ( req.body && req.body.name ) { + runtime.storage.sshkeys.generateSSHKey(username, "", req.body.name, req.body) + .then(function(name) { + console.log('generate key --- success name:', name); + res.json({ + name: name + }); + }) + .catch(function(err) { + console.log(err.stack); + if (err.code) { + res.status(400).json({error:err.code, message: err.message}); + } else { + res.status(400).json({error:"unexpected_error", message:err.toString()}); + } + }); + } + else { + res.status(400).json({error:"unexpected_error", message:"You need to have body or body.name"}); + } + }); + + // Delete a SSH key + app.delete("/:id", needsPermission("settings.write"), function(req,res) { + var username = getUsername(req.user); + runtime.storage.sshkeys.deleteSSHKey(username, req.params.id) + .then(function(ret) { + res.status(204).end(); + }) + .catch(function(err) { + console.log(err.stack); + if (err.code) { + res.status(400).json({error:err.code, message: err.message}); + } else { + res.status(400).json({error:"unexpected_error", message:err.toString()}); + } + }); + }); + + return app; + } +} diff --git a/red/runtime/storage/index.js b/red/runtime/storage/index.js index f60862fe7..f9aba118f 100644 --- a/red/runtime/storage/index.js +++ b/red/runtime/storage/index.js @@ -57,6 +57,9 @@ var storageModuleInterface = { if (storageModule.projects) { storageModuleInterface.projects = storageModule.projects; } + if (storageModule.sshkeys) { + storageModuleInterface.sshkeys = storageModule.sshkeys; + } return storageModule.init(runtime.settings,runtime); }, getFlows: function() { diff --git a/red/runtime/storage/localfilesystem/index.js b/red/runtime/storage/localfilesystem/index.js index 2a57c7323..bcd8969dd 100644 --- a/red/runtime/storage/localfilesystem/index.js +++ b/red/runtime/storage/localfilesystem/index.js @@ -24,6 +24,7 @@ var library = require("./library"); var sessions = require("./sessions"); var runtimeSettings = require("./settings"); var projects = require("./projects"); +var sshkeys = require("./sshkeys"); var initialFlowLoadComplete = false; var settings; @@ -60,6 +61,7 @@ var localfilesystem = { runtimeSettings.init(settings); promises.push(library.init(settings)); promises.push(projects.init(settings, runtime)); + promises.push(sshkeys.init(settings, runtime)); var packageFile = fspath.join(settings.userDir,"package.json"); var packagePromise = when.resolve(); @@ -94,7 +96,8 @@ var localfilesystem = { saveSessions: sessions.saveSessions, getLibraryEntry: library.getLibraryEntry, saveLibraryEntry: library.saveLibraryEntry, - projects: projects + projects: projects, + sshkeys: sshkeys }; module.exports = localfilesystem; diff --git a/red/runtime/storage/localfilesystem/sshkeys.js b/red/runtime/storage/localfilesystem/sshkeys.js new file mode 100644 index 000000000..abb92a3cd --- /dev/null +++ b/red/runtime/storage/localfilesystem/sshkeys.js @@ -0,0 +1,152 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var fs = require('fs-extra'); +var when = require('when'); +var fspath = require("path"); +var keygen = require('ssh-keygen'); + +var settings; +var runtime; +var log; +var sshkeyDir; + +function init(_settings, _runtime) { + settings = _settings; + runtime = _runtime; + log = runtime.log; + sshkeyDir = fspath.join(settings.userDir, "projects", ".sshkeys"); + // console.log('sshkeys.init()'); + return createSSHKeyDirectory(); +} + +function createSSHKeyDirectory() { + return fs.ensureDir(sshkeyDir); +} + +function listSSHKeys(username) { + var startStr = username + '_'; + // console.log('sshkeyDir:', sshkeyDir); + return fs.readdir(sshkeyDir).then(function(fns) { + var ret = fns.sort() + .filter(function(fn) { + var fullPath = fspath.join(sshkeyDir,fn); + if (fn[0] != ".") { + var stats = fs.lstatSync(fullPath); + if (stats.isFile()) { + return fn.startsWith(startStr); + } + } + return false; + }) + .map(function(filename) { + return filename.substr(startStr.length); + }) + .reduce(function(prev, current) { + var parsePath = fspath.parse(current); + if ( parsePath ) { + if ( parsePath.ext !== '.pub' ) { + // Private Keys + prev.keyFiles.push(parsePath.base); + } + else if ( parsePath.ext === '.pub' && (prev.keyFiles.some(function(elem){ return elem === parsePath.name; }))) { + prev.privateKeyFiles.push(parsePath.name); + } + } + return prev; + }, { keyFiles: [], privateKeyFiles: [] }); + return ret.privateKeyFiles.map(function(filename) { + return { + name: filename + }; + }); + }); +} + +function getSSHKey(username, name) { + return checkSSHKeyFileAndGetPublicKeyFileName(username, name) + .then(function(publicSSHKeyPath) { + return fs.readFile(publicSSHKeyPath, 'utf-8'); + }); +} + +function generateSSHKey(username, email, name, data) { + var sshKeyFileBasename = username + '_' + name; + var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename); + return generateSSHKeyPair(privateKeyFilePath, email, data.password, data.size) + .then(function() { + return name; + }); +} + +function deleteSSHKey(username, name) { + return checkSSHKeyFileAndGetPublicKeyFileName(username, name) + .then(function() { + return deleteSSHKeyFiles(username, name); + }); +} + +function checkSSHKeyFileAndGetPublicKeyFileName(username, name) { + var sshKeyFileBasename = username + '_' + name; + var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename); + var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub'); + return when.all([ + fs.access(privateKeyFilePath, (fs.constants || fs).R_OK), + fs.access(publicKeyFilePath , (fs.constants || fs).R_OK) + ]) + .then(function() { + return when.resolve(publicKeyFilePath); + }); +} + +function deleteSSHKeyFiles(username, name) { + var sshKeyFileBasename = username + '_' + name; + var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename); + var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub'); + return when.all([ + fs.remove(privateKeyFilePath), + fs.remove(publicKeyFilePath) + ]) + .then(function(retArray) { + return when.resolve(true); + }); +} + +function generateSSHKeyPair(privateKeyPath, comment, password, size) { + return when.promise(function(resolve, reject) { + keygen({ + location: privateKeyPath, + comment: comment, + password: password, + size: size + }, function(err, out) { + if ( err ) { + reject(err); + } + else { + resolve(); + } + }); + }); +} + +module.exports = { + init: init, + listSSHKeys: listSSHKeys, + getSSHKey: getSSHKey, + generateSSHKey: generateSSHKey, + deleteSSHKey: deleteSSHKey +}; diff --git a/test/red/api/editor/sshkeys_spec.js b/test/red/api/editor/sshkeys_spec.js new file mode 100644 index 000000000..68f8784be --- /dev/null +++ b/test/red/api/editor/sshkeys_spec.js @@ -0,0 +1,377 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var should = require("should"); +var sinon = require("sinon"); +var request = require("supertest"); +var express = require("express"); +var editorApi = require("../../../../red/api/editor"); +var comms = require("../../../../red/api/editor/comms"); +var info = require("../../../../red/api/editor/settings"); +var auth = require("../../../../red/api/auth"); +var sshkeys = require("../../../../red/api/editor/sshkeys"); +var when = require("when"); +var bodyParser = require("body-parser"); +var fs = require("fs-extra"); +var fspath = require("path"); + + +describe("api/editor/sshkeys", function() { + var app; + var mockList = [ + 'library','theme','locales','credentials','comms' + ] + var isStarted = true; + var errors = []; + var session_data = {}; + // before(function() { + // mockList.forEach(function(m) { + // sinon.stub(require("../../../../../red/api/editor/"+m),"init",function(){}); + // }); + // sinon.stub(require("../../../../../red/api/editor/theme"),"app",function(){ return express()}); + // }); + // after(function() { + // mockList.forEach(function(m) { + // require("../../../../../red/api/editor/"+m).init.restore(); + // }) + // require("../../../../../red/api/editor/theme").app.restore(); + // }); + var mockRuntime = { + settings:{ + httpNodeRoot: true, + httpAdminRoot: true, + disableEditor: false, + exportNodeSettings:function(){}, + // adminAuth: { + // default: { + // permissions: ['*'] + // } + // }, + storage: { + getSessions: function(){ + return when.resolve(session_data); + }, + setSessions: function(_session) { + session_data = _session; + return when.resolve(); + } + }, + log:{audit:function(){},error:function(msg){errors.push(msg)}} + }, + storage: { + sshkeys: { + init: function(){}, + listSSHKeys: function(){}, + getSSHKey: function(){}, + generateSSHKey: function(){}, + deleteSSHKey: function(){}, + } + }, + events:{on:function(){},removeListener:function(){}}, + isStarted: function() { return isStarted; }, + nodes: {paletteEditorEnabled: function() { return false }} + }; + + before(function() { + auth.init(mockRuntime); + app = express(); + app.use(bodyParser.json()); + app.use(editorApi.init({},mockRuntime)); + }); + after(function() { + // fs.removeSync() + }) + + beforeEach(function() { + sinon.stub(mockRuntime.storage.sshkeys, "listSSHKeys"); + sinon.stub(mockRuntime.storage.sshkeys, "getSSHKey"); + sinon.stub(mockRuntime.storage.sshkeys, "generateSSHKey"); + sinon.stub(mockRuntime.storage.sshkeys, "deleteSSHKey"); + }) + afterEach(function() { + mockRuntime.storage.sshkeys.listSSHKeys.restore(); + mockRuntime.storage.sshkeys.getSSHKey.restore(); + mockRuntime.storage.sshkeys.generateSSHKey.restore(); + mockRuntime.storage.sshkeys.deleteSSHKey.restore(); + }) + + it('GET /settings/user/keys --- return empty list', function(done) { + mockRuntime.storage.sshkeys.listSSHKeys.returns(Promise.resolve([])); + request(app) + .get("/settings/user/keys") + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property('keys'); + res.body.keys.should.be.empty(); + done(); + }); + }); + + it('GET /settings/user/keys --- return normal list', function(done) { + var fileList = [ + 'test_key01', + 'test_key02' + ]; + var retList = fileList.map(function(elem) { + return { + name: elem + }; + }); + mockRuntime.storage.sshkeys.listSSHKeys.returns(Promise.resolve(retList)); + request(app) + .get("/settings/user/keys") + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property('keys'); + for (var item of retList) { + res.body.keys.should.containEql(item); + } + done(); + }); + }); + + it('GET /settings/user/keys --- return Error', function(done) { + var errInstance = new Error("Messages....."); + errInstance.code = "test_code"; + mockRuntime.storage.sshkeys.listSSHKeys.returns(Promise.reject(errInstance)); + request(app) + .get("/settings/user/keys") + .expect(400) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property('error'); + res.body.error.should.be.equal(errInstance.code); + res.body.should.have.property('message'); + res.body.message.should.be.equal(errInstance.message); + done(); + }); + }); + + it('GET /settings/user/keys --- return Unexpected Error', function(done) { + var errInstance = new Error("Messages....."); + // errInstance.code = "test_code"; + mockRuntime.storage.sshkeys.listSSHKeys.returns(Promise.reject(errInstance)); + request(app) + .get("/settings/user/keys") + .expect(400) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property('error'); + res.body.error.should.be.equal("unexpected_error"); + res.body.should.have.property('message'); + res.body.message.should.be.equal(errInstance.toString()); + done(); + }); + }); + + it('GET /settings/user/keys/ --- return content', function(done) { + var key_file_name = "test_key"; + var fileContent = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQD3a+sgtgzSbbliWxmOq5p6+H/mE+0gjWfLWrkIVmHENd1mifV4uCmIHAR2NfuadUYMQ3+bQ90kpmmEKTMYPsyentsKpHQZxTzG7wOCAIpJnbPTHDMxEJhVTaAwEjbVyMSIzTTPfnhoavWIBu0+uMgKDDlBm+RjlgkFlyhXyCN6UwFrIUUMH6Gw+eQHLiooKIl8ce7uDxIlt+9b7hFCU+sQ3kvuse239DZluu6+8buMWqJvrEHgzS9adRFKku8nSPAEPYn85vDi7OgVAcLQufknNgs47KHBAx9h04LeSrFJ/P5J1b//ItRpMOIme+O9d1BR46puzhvUaCHLdvO9czj+OmW+dIm+QIk6lZIOOMnppG72kZxtLfeKT16ur+2FbwAdL9ItBp4BI/YTlBPoa5mLMxpuWfmX1qHntvtGc9wEwS1P7YFfmF3XiK5apxalzrn0Qlr5UmDNbVIqJb1OlbC0w03Z0oktti1xT+R2DGOLWM4lBbpXDHV1BhQ7oYOvbUD8Cnof55lTP0WHHsOHlQc/BGDti1XA9aBX/OzVyzBUYEf0pkimsD0RYo6aqt7QwehJYdlz9x1NBguBffT0s4NhNb9IWr+ASnFPvNl2sw4XH/8U0J0q8ZkMpKkbLM1Zdp1Fv00GF0f5UNRokai6uM3w/ccantJ3WvZ6GtctqytWrw== \n"; + mockRuntime.storage.sshkeys.getSSHKey.returns(Promise.resolve(fileContent)); + request(app) + .get("/settings/user/keys/" + key_file_name) + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + mockRuntime.storage.sshkeys.getSSHKey.called.should.be.true(); + res.body.should.be.deepEqual({ publickey: fileContent }); + done(); + }); + }); + + it('GET /settings/user/keys/ --- return Error', function(done) { + var key_file_name = "test_key"; + var errInstance = new Error("Messages....."); + errInstance.code = "test_code"; + mockRuntime.storage.sshkeys.getSSHKey.returns(Promise.reject(errInstance)); + request(app) + .get("/settings/user/keys/" + key_file_name) + .expect(400) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property('error'); + res.body.error.should.be.equal(errInstance.code); + res.body.should.have.property('message'); + res.body.message.should.be.equal(errInstance.message); + done(); + }); + }); + + it('GET /settings/user/keys/ --- return Unexpected Error', function(done) { + var key_file_name = "test_key"; + var errInstance = new Error("Messages....."); + // errInstance.code = "test_code"; + mockRuntime.storage.sshkeys.getSSHKey.returns(Promise.reject(errInstance)); + request(app) + .get("/settings/user/keys/" + key_file_name) + .expect(400) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property('error'); + res.body.error.should.be.equal("unexpected_error"); + res.body.should.have.property('message'); + res.body.message.should.be.equal(errInstance.toString()); + done(); + }); + }); + + it('POST /settings/user/keys --- success', function(done) { + var key_file_name = "test_key"; + mockRuntime.storage.sshkeys.generateSSHKey.returns(Promise.resolve(key_file_name)); + request(app) + .post("/settings/user/keys") + .send({ name: key_file_name }) + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + done(); + }); + }); + + it('POST /settings/user/keys --- return parameter error', function(done) { + var key_file_name = "test_key"; + mockRuntime.storage.sshkeys.generateSSHKey.returns(Promise.resolve(key_file_name)); + request(app) + .post("/settings/user/keys") + // .send({ name: key_file_name }) + .expect(400) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property('error'); + res.body.error.should.be.equal("unexpected_error"); + res.body.should.have.property('message'); + res.body.message.should.be.equal("You need to have body or body.name"); + done(); + }); + }); + + it('POST /settings/user/keys --- return Error', function(done) { + var key_file_name = "test_key"; + var errInstance = new Error("Messages....."); + errInstance.code = "test_code"; + mockRuntime.storage.sshkeys.generateSSHKey.returns(Promise.reject(errInstance)); + request(app) + .post("/settings/user/keys") + .send({ name: key_file_name }) + .expect(400) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property('error'); + res.body.error.should.be.equal("test_code"); + res.body.should.have.property('message'); + res.body.message.should.be.equal(errInstance.message); + done(); + }); + }); + + it('POST /settings/user/keys --- return Unexpected error', function(done) { + var key_file_name = "test_key"; + var errInstance = new Error("Messages....."); + // errInstance.code = "test_code"; + mockRuntime.storage.sshkeys.generateSSHKey.returns(Promise.reject(errInstance)); + request(app) + .post("/settings/user/keys") + .send({ name: key_file_name }) + .expect(400) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property('error'); + res.body.error.should.be.equal("unexpected_error"); + res.body.should.have.property('message'); + res.body.message.should.be.equal(errInstance.toString()); + done(); + }); + }); + + it('DELETE /settings/user/keys/ --- success', function(done) { + var key_file_name = "test_key"; + mockRuntime.storage.sshkeys.deleteSSHKey.returns(Promise.resolve(true)); + request(app) + .delete("/settings/user/keys/" + key_file_name) + .expect(204) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.be.true(); + done(); + }); + }); + + it('DELETE /settings/user/keys/ --- return Error', function(done) { + var key_file_name = "test_key"; + var errInstance = new Error("Messages....."); + errInstance.code = "test_code"; + mockRuntime.storage.sshkeys.deleteSSHKey.returns(Promise.reject(errInstance)); + request(app) + .delete("/settings/user/keys/" + key_file_name) + .expect(400) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property('error'); + res.body.error.should.be.equal("test_code"); + res.body.should.have.property('message'); + res.body.message.should.be.equal(errInstance.message); + done(); + }); + }); + + it('DELETE /settings/user/keys/ --- return Unexpected Error', function(done) { + var key_file_name = "test_key"; + var errInstance = new Error("Messages....."); + // errInstance.code = "test_code"; + mockRuntime.storage.sshkeys.deleteSSHKey.returns(Promise.reject(errInstance)); + request(app) + .delete("/settings/user/keys/" + key_file_name) + .expect(400) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property('error'); + res.body.error.should.be.equal("unexpected_error"); + res.body.should.have.property('message'); + res.body.message.should.be.equal(errInstance.toString()); + done(); + }); + }); +}); diff --git a/test/red/runtime/storage/localfilesystem/sshkeys_spec.js b/test/red/runtime/storage/localfilesystem/sshkeys_spec.js new file mode 100644 index 000000000..8f6619414 --- /dev/null +++ b/test/red/runtime/storage/localfilesystem/sshkeys_spec.js @@ -0,0 +1,422 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +var should = require("should"); +var fs = require('fs-extra'); +var path = require('path'); + +var localfilesystem = require("../../../../../red/runtime/storage/localfilesystem"); +var sshkeys = require("../../../../../red/runtime/storage/localfilesystem/sshkeys"); + +describe("storage/localfilesystem/sshkeys", function() { + var userDir = path.join(__dirname,".testSSHKeyUserHome"); + var mockSettings = { + userDir: userDir + }; + var mockRuntime = { + log:{ + _:function() { return "placeholder message"}, + info: function() { } + } + }; + beforeEach(function(done) { + fs.remove(userDir,function(err) { + fs.mkdir(userDir,done); + }); + }); + afterEach(function(done) { + fs.remove(userDir,done); + }); + + it('should create sshkey directory when sshkey initializes', function(done) { + var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + var ret = fs.existsSync(sshkeyDirPath); + fs.existsSync(sshkeyDirPath).should.be.true(); + done(); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }); + + it('should get sshkey empty list if there is no sshkey file', function(done) { + var username = 'test'; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + sshkeys.listSSHKeys(username).then(function(retObj) { + console.log('retObj:', retObj); + retObj.should.be.instanceOf(Array).and.have.lengthOf(0); + done(); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }); + + it('should get sshkey list', function(done) { + var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); + var username = 'test'; + var filenameList = ['test-key01', 'test-key02']; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + for(var filename of filenameList) { + fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8"); + fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),"","utf8"); + } + sshkeys.listSSHKeys(username).then(function(retObj) { + retObj.should.be.instanceOf(Array).and.have.lengthOf(filenameList.length); + for(var filename of filenameList) { + retObj.should.containEql({ name: filename }); + } + done(); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }); + + it('should not get sshkey file if there is only private key', function(done) { + var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); + var username = 'test'; + var filenameList = ['test-key01', 'test-key02']; + var onlyPrivateKeyFilenameList = ['test-key03', 'test-key04']; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + for(var filename of filenameList) { + fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8"); + fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),"","utf8"); + } + for(var filename of onlyPrivateKeyFilenameList) { + fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8"); + } + sshkeys.listSSHKeys(username).then(function(retObj) { + retObj.should.be.instanceOf(Array).and.have.lengthOf(filenameList.length); + for(var filename of filenameList) { + retObj.should.containEql({ name: filename }); + } + for(var filename of onlyPrivateKeyFilenameList) { + retObj.should.not.containEql({ name: filename }); + } + done(); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }); + + it('should not get sshkey file if there is only public key', function(done) { + var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); + var username = 'test'; + var filenameList = ['test-key01', 'test-key02']; + var directoryList = ['test-key03', '.test-key04']; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + for(var filename of filenameList) { + fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8"); + fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),"","utf8"); + } + for(var filename of directoryList) { + fs.ensureDirSync(path.join(sshkeyDirPath,filename)); + } + sshkeys.listSSHKeys(username).then(function(retObj) { + retObj.should.be.instanceOf(Array).and.have.lengthOf(filenameList.length); + for(var filename of filenameList) { + retObj.should.containEql({ name: filename }); + } + for(var directoryname of directoryList) { + retObj.should.not.containEql({ name: directoryname }); + } + done(); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }); + + it('should get sshkey list that does not have directory', function(done) { + var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); + var username = 'test'; + var otherUsername = 'other'; + var filenameList = ['test-key01', 'test-key02']; + var otherUserFilenameList = ['test-key03', 'test-key04']; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + for(var filename of filenameList) { + fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8"); + fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),"","utf8"); + } + for(var filename of otherUserFilenameList) { + fs.writeFileSync(path.join(sshkeyDirPath,otherUsername+"_"+filename),"","utf8"); + fs.writeFileSync(path.join(sshkeyDirPath,otherUsername+"_"+filename+".pub"),"","utf8"); + } + sshkeys.listSSHKeys(username).then(function(retObj) { + retObj.should.be.instanceOf(Array).and.have.lengthOf(filenameList.length); + for(var filename of filenameList) { + retObj.should.containEql({ name: filename }); + } + for(var filename of otherUserFilenameList) { + retObj.should.not.containEql({ name: filename }); + } + done(); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }); + + it('should get sshkey list that have keys of specified user', function(done) { + var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); + var username = 'test'; + var otherUsername = 'other'; + var filenameList = ['test-key01', 'test-key02']; + var otherUserFilenameList = ['test-key03', 'test-key04']; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + for(var filename of filenameList) { + fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8"); + fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),"","utf8"); + } + for(var filename of otherUserFilenameList) { + fs.writeFileSync(path.join(sshkeyDirPath,otherUsername+"_"+filename),"","utf8"); + fs.writeFileSync(path.join(sshkeyDirPath,otherUsername+"_"+filename+".pub"),"","utf8"); + } + sshkeys.listSSHKeys(username).then(function(retObj) { + retObj.should.be.instanceOf(Array).and.have.lengthOf(filenameList.length); + for(var filename of filenameList) { + retObj.should.containEql({ name: filename }); + } + for(var filename of otherUserFilenameList) { + retObj.should.not.containEql({ name: filename }); + } + done(); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }); + + it('should generate sshkey file with empty data', function(done) { + var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); + var username = 'test'; + var email = 'test@test.com'; + var filename = 'test-key01'; + var data = {}; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + sshkeys.generateSSHKey(username, email, filename, data).then(function(retObj) { + retObj.should.be.equal(filename); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.true(); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.pub')).should.be.true(); + done(); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }); + + it('should generate sshkey file with password data', function(done) { + var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); + var username = 'test'; + var email = 'test@test.com'; + var filename = 'test-key01'; + var data = { + password: 'testtest' + }; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + sshkeys.generateSSHKey(username, email, filename, data).then(function(retObj) { + retObj.should.be.equal(filename); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.true(); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.pub')).should.be.true(); + done(); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }); + + it('should generate sshkey file with size data', function(done) { + var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); + var username = 'test'; + var email = 'test@test.com'; + var filename = 'test-key01'; + var data = { + size: 4096 + }; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + sshkeys.generateSSHKey(username, email, filename, data).then(function(retObj) { + retObj.should.be.equal(filename); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.true(); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.pub')).should.be.true(); + done(); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }); + + it('should generate sshkey file with password & size data', function(done) { + this.timeout(5000); + var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); + var username = 'test'; + var email = 'test@test.com'; + var filename = 'test-key01'; + var data = { + password: 'testtest', + size: 4096 + }; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + sshkeys.generateSSHKey(username, email, filename, data).then(function(retObj) { + retObj.should.be.equal(filename); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.true(); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.pub')).should.be.true(); + done(); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }); + + it('should not generate sshkey file with illegal size data', function(done) { + this.timeout(5000); + var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); + var username = 'test'; + var email = 'test@test.com'; + var filename = 'test-key01'; + var data = { + size: 3333 + }; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + sshkeys.generateSSHKey(username, email, filename, data).then(function(retObj) { + retObj.should.be.equal(filename); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.true(); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.pub')).should.be.true(); + done(); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }); + + it('should get sshkey file content', function(done) { + var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); + var username = 'test'; + var filename = 'test-key01'; + var fileContent = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQD3a+sgtgzSbbliWxmOq5p6+H/mE+0gjWfLWrkIVmHENd1mifV4uCmIHAR2NfuadUYMQ3+bQ90kpmmEKTMYPsyentsKpHQZxTzG7wOCAIpJnbPTHDMxEJhVTaAwEjbVyMSIzTTPfnhoavWIBu0+uMgKDDlBm+RjlgkFlyhXyCN6UwFrIUUMH6Gw+eQHLiooKIl8ce7uDxIlt+9b7hFCU+sQ3kvuse239DZluu6+8buMWqJvrEHgzS9adRFKku8nSPAEPYn85vDi7OgVAcLQufknNgs47KHBAx9h04LeSrFJ/P5J1b//ItRpMOIme+O9d1BR46puzhvUaCHLdvO9czj+OmW+dIm+QIk6lZIOOMnppG72kZxtLfeKT16ur+2FbwAdL9ItBp4BI/YTlBPoa5mLMxpuWfmX1qHntvtGc9wEwS1P7YFfmF3XiK5apxalzrn0Qlr5UmDNbVIqJb1OlbC0w03Z0oktti1xT+R2DGOLWM4lBbpXDHV1BhQ7oYOvbUD8Cnof55lTP0WHHsOHlQc/BGDti1XA9aBX/OzVyzBUYEf0pkimsD0RYo6aqt7QwehJYdlz9x1NBguBffT0s4NhNb9IWr+ASnFPvNl2sw4XH/8U0J0q8ZkMpKkbLM1Zdp1Fv00GF0f5UNRokai6uM3w/ccantJ3WvZ6GtctqytWrw== \n"; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8"); + fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),fileContent,"utf8"); + sshkeys.getSSHKey(username, filename).then(function(retObj) { + retObj.should.be.equal(fileContent); + done(); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }); + + it('should delete sshkey files', function(done) { + var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); + var username = 'test'; + var filename = 'test-key01'; + var fileContent = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQD3a+sgtgzSbbliWxmOq5p6+H/mE+0gjWfLWrkIVmHENd1mifV4uCmIHAR2NfuadUYMQ3+bQ90kpmmEKTMYPsyentsKpHQZxTzG7wOCAIpJnbPTHDMxEJhVTaAwEjbVyMSIzTTPfnhoavWIBu0+uMgKDDlBm+RjlgkFlyhXyCN6UwFrIUUMH6Gw+eQHLiooKIl8ce7uDxIlt+9b7hFCU+sQ3kvuse239DZluu6+8buMWqJvrEHgzS9adRFKku8nSPAEPYn85vDi7OgVAcLQufknNgs47KHBAx9h04LeSrFJ/P5J1b//ItRpMOIme+O9d1BR46puzhvUaCHLdvO9czj+OmW+dIm+QIk6lZIOOMnppG72kZxtLfeKT16ur+2FbwAdL9ItBp4BI/YTlBPoa5mLMxpuWfmX1qHntvtGc9wEwS1P7YFfmF3XiK5apxalzrn0Qlr5UmDNbVIqJb1OlbC0w03Z0oktti1xT+R2DGOLWM4lBbpXDHV1BhQ7oYOvbUD8Cnof55lTP0WHHsOHlQc/BGDti1XA9aBX/OzVyzBUYEf0pkimsD0RYo6aqt7QwehJYdlz9x1NBguBffT0s4NhNb9IWr+ASnFPvNl2sw4XH/8U0J0q8ZkMpKkbLM1Zdp1Fv00GF0f5UNRokai6uM3w/ccantJ3WvZ6GtctqytWrw== \n"; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8"); + fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),fileContent,"utf8"); + sshkeys.deleteSSHKey(username, filename).then(function(retObj) { + retObj.should.be.true(); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.false(); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.pub')).should.be.false(); + done(); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }).catch(function(err) { + done(err); + }); + }); +});