From fbf7ee50eb6b01d8ad3d41299bf5ed34c0dae7e1 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 28 Jan 2015 22:41:13 +0000 Subject: [PATCH] Increase unit test coverage of auth code --- public/red/user.js | 2 +- red/api/auth/clients.js | 1 + red/api/auth/index.js | 16 +- red/api/auth/permissions.js | 4 +- red/api/auth/strategies.js | 53 +++-- red/api/auth/tokens.js | 44 ---- red/api/auth/tokens/index.js | 81 +++++++ red/api/auth/tokens/localfilesystem.js | 72 +++++++ red/api/auth/users.js | 118 ++++++----- red/api/index.js | 26 +-- red/comms.js | 61 +++--- red/storage/index.js | 3 + red/storage/localfilesystem.js | 1 - test/red/api/auth/clients_spec.js | 47 +++++ test/red/api/auth/index_spec.js | 38 +++- test/red/api/auth/permissions_spec.js | 61 ++++++ test/red/api/auth/strategies_spec.js | 197 +++++++++++++++++- test/red/api/auth/tokens/index_spec.js | 168 +++++++++++++++ .../api/auth/tokens/localfilesystem_spec.js | 96 +++++++++ test/red/api/auth/tokens_spec.js | 0 test/red/api/auth/users_spec.js | 185 ++++++++++++++++ test/red/comms_spec.js | 155 +++++++++++++- 22 files changed, 1251 insertions(+), 178 deletions(-) delete mode 100644 red/api/auth/tokens.js create mode 100644 red/api/auth/tokens/index.js create mode 100644 red/api/auth/tokens/localfilesystem.js create mode 100644 test/red/api/auth/tokens/index_spec.js create mode 100644 test/red/api/auth/tokens/localfilesystem_spec.js delete mode 100644 test/red/api/auth/tokens_spec.js diff --git a/public/red/user.js b/public/red/user.js index b60d0c69a..978627551 100644 --- a/public/red/user.js +++ b/public/red/user.js @@ -61,7 +61,7 @@ RED.user = (function() { $(".login-spinner").show(); var body = { - client_id: "node-red-admin", + client_id: "node-red-editor", grant_type: "password", scope:"*" } diff --git a/red/api/auth/clients.js b/red/api/auth/clients.js index 6d8c7380c..ac20b6be0 100644 --- a/red/api/auth/clients.js +++ b/red/api/auth/clients.js @@ -17,6 +17,7 @@ var when = require("when"); var clients = [ + {id:"node-red-editor",secret:"not_available"}, {id:"node-red-admin",secret:"not_available"} ]; diff --git a/red/api/auth/index.js b/red/api/auth/index.js index f05c8b093..0745493ff 100644 --- a/red/api/auth/index.js +++ b/red/api/auth/index.js @@ -22,6 +22,8 @@ var Tokens = require("./tokens"); var Users = require("./users"); var settings = require("../../settings"); +var log = require("../../log"); + passport.use(strategies.bearerStrategy.BearerStrategy); passport.use(strategies.clientPasswordStrategy.ClientPasswordStrategy); @@ -32,7 +34,10 @@ var server = oauth2orize.createServer(); server.exchange(oauth2orize.exchange.password(strategies.passwordTokenExchange)); function init() { - Users.init(); + if (settings.adminAuth) { + Users.init(settings.adminAuth); + Tokens.init(settings) + } } function authenticate(req,res,next) { @@ -70,6 +75,7 @@ function login(req,res) { function revoke(req,res) { var token = req.body.token; + // TODO: audit log Tokens.revoke(token).then(function() { res.send(200); }); @@ -81,7 +87,13 @@ module.exports = { ensureClientSecret: ensureClientSecret, authenticateClient: authenticateClient, getToken: getToken, - errorHandler: server.errorHandler(), + errorHandler: function(err,req,res,next) { + //TODO: standardize json response + //TODO: audit log statment + //console.log(err.stack); + //log.log({level:"audit",type:"auth",msg:err.toString()}); + return server.errorHandler()(err,req,res,next); + }, login: login, revoke: revoke } diff --git a/red/api/auth/permissions.js b/red/api/auth/permissions.js index 8da0f6a1d..4933807d4 100644 --- a/red/api/auth/permissions.js +++ b/red/api/auth/permissions.js @@ -16,8 +16,8 @@ var util = require('util'); -var readRE = /^(.*)\.read$/ -var writeRE = /^(.*)\.write$/ +var readRE = /^((.+)\.)?read$/ +var writeRE = /^((.+)\.)?write$/ function needsPermission(perm) { return function(req,res,next) { diff --git a/red/api/auth/strategies.js b/red/api/auth/strategies.js index 10a6d78a4..7498e706e 100644 --- a/red/api/auth/strategies.js +++ b/red/api/auth/strategies.js @@ -16,10 +16,11 @@ var BearerStrategy = require('passport-http-bearer').Strategy; var ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy; -var passport = require("passport"); +var passport = require("passport"); var crypto = require("crypto"); var util = require("util"); + var Tokens = require("./tokens"); var Users = require("./users"); var Clients = require("./clients"); @@ -53,28 +54,52 @@ var clientPasswordStrategy = function(clientId, clientSecret, done) { } clientPasswordStrategy.ClientPasswordStrategy = new ClientPasswordStrategy(clientPasswordStrategy); -var passwordTokenExchange = function(client, username, password, scope, done) { - Users.authenticate(username,password).then(function(user) { - if (user) { - Tokens.create(username,client.id,scope).then(function(token) { - done(null,token); - }); - } else { - done(null,false); - } - }); -} +var loginAttempts = []; +var loginSignUpWindow = 36000000; // 10 minutes +var passwordTokenExchange = function(client, username, password, scope, done) { + var now = Date.now(); + loginAttempts = loginAttempts.filter(function(logEntry) { + return logEntry.time + loginSignUpWindow > now; + }); + loginAttempts.push({time:now, user:username}); + var attemptCount = 0; + loginAttempts.forEach(function(logEntry) { + if (logEntry.user == username) { + attemptCount++; + } + }); + if (attemptCount > 5) { + // TODO: audit log + done(new Error("Too many login attempts. Wait 10 minutes and try again"),false); + return; + } + + Users.authenticate(username,password).then(function(user) { + if (user) { + loginAttempts = loginAttempts.filter(function(logEntry) { + return logEntry.user !== username; + }); + Tokens.create(username,client.id,scope).then(function(tokens) { + // TODO: audit log + done(null,tokens.accessToken); + }); + } else { + // TODO: audit log + done(null,false); + } + }); +} + function AnonymousStrategy() { passport.Strategy.call(this); this.name = 'anon'; } util.inherits(AnonymousStrategy, passport.Strategy); AnonymousStrategy.prototype.authenticate = function(req) { - var authorization = req.headers['authorization']; var self = this; - Users.anonymous().then(function(anon) { + Users.default().then(function(anon) { if (anon) { self.success(anon); } else { diff --git a/red/api/auth/tokens.js b/red/api/auth/tokens.js deleted file mode 100644 index 9285d0974..000000000 --- a/red/api/auth/tokens.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright 2015 IBM Corp. - * - * 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 when = require("when"); - -function generateToken(length) { - var c = "ABCDEFGHIJKLMNOPQRSTUZWXYZabcdefghijklmnopqrstuvwxyz1234567890"; - var token = []; - for (var i=0;i webSocketKeepAliveTime) { - publish("hb",lastSentTime); - } + var now = Date.now(); + if (now-lastSentTime > webSocketKeepAliveTime) { + publish("hb",lastSentTime); + } }, webSocketKeepAliveTime); }); - wsServer.on('error', function(err) { - log.warn("comms server error : "+err.toString()); - }); - - lastSentTime = Date.now(); - - heartbeatTimer = setInterval(function() { - var now = Date.now(); - if (now-lastSentTime > webSocketKeepAliveTime) { - publish("hb",lastSentTime); - } - }, webSocketKeepAliveTime); } } diff --git a/red/storage/index.js b/red/storage/index.js index ba939627c..d3d27bb51 100644 --- a/red/storage/index.js +++ b/red/storage/index.js @@ -18,6 +18,7 @@ var when = require('when'); var storageModule; var settingsAvailable; +var sessionsAvailable; function moduleSelector(aSettings) { var toReturn; @@ -43,6 +44,7 @@ var storageModuleInterface = { try { storageModule = moduleSelector(settings); settingsAvailable = storageModule.hasOwnProperty("getSettings") && storageModule.hasOwnProperty("saveSettings"); + sessionsAvailable = storageModule.hasOwnProperty("getUserSessions") && storageModule.hasOwnProperty("saveUserSessions"); } catch (e) { return when.reject(e); } @@ -74,6 +76,7 @@ var storageModuleInterface = { return when.resolve(); } }, + /* Library Functions */ getAllFlows: function() { return storageModule.getAllFlows(); diff --git a/red/storage/localfilesystem.js b/red/storage/localfilesystem.js index 62102f0e9..62b9d1c20 100644 --- a/red/storage/localfilesystem.js +++ b/red/storage/localfilesystem.js @@ -262,7 +262,6 @@ var localfilesystem = { return writeFile(globalSettingsFile,JSON.stringify(settings,null,1)); }, - getAllFlows: function() { return listFiles(libFlowsDir); }, diff --git a/test/red/api/auth/clients_spec.js b/test/red/api/auth/clients_spec.js index e69de29bb..0a925aedb 100644 --- a/test/red/api/auth/clients_spec.js +++ b/test/red/api/auth/clients_spec.js @@ -0,0 +1,47 @@ +/** + * Copyright 2015 IBM Corp. + * + * 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 Clients = require("../../../../red/api/auth/clients"); + +describe("Clients", function() { + it('finds the known editor client',function(done) { + Clients.get("node-red-editor").then(function(client) { + client.should.have.property("id","node-red-editor"); + client.should.have.property("secret","not_available"); + done(); + }); + }); + it('finds the known admin client',function(done) { + Clients.get("node-red-admin").then(function(client) { + client.should.have.property("id","node-red-admin"); + client.should.have.property("secret","not_available"); + done(); + }).catch(function(err) { + done(err); + }); + }); + it('returns null for unknown client',function(done) { + Clients.get("unknown-client").then(function(client) { + should.not.exist(client); + done(); + }).catch(function(err) { + done(err); + }); + + }); +}); + \ No newline at end of file diff --git a/test/red/api/auth/index_spec.js b/test/red/api/auth/index_spec.js index 553f9d60d..b1ef3e7b0 100644 --- a/test/red/api/auth/index_spec.js +++ b/test/red/api/auth/index_spec.js @@ -1,5 +1,5 @@ /** - * Copyright 2014 IBM Corp. + * Copyright 2015 IBM Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +15,13 @@ **/ var should = require("should"); +var when = require("when"); var sinon = require("sinon"); -var request = require('supertest'); -var express = require('express'); var passport = require("passport"); var auth = require("../../../../red/api/auth"); +var Tokens = require("../../../../red/api/auth/tokens"); var settings = require("../../../../red/settings"); @@ -103,4 +103,36 @@ describe("api auth middleware",function() { }) }); }); + + describe("revoke", function() { + it("revokes a token", function(done) { + var revokeToken = sinon.stub(Tokens,"revoke",function() { + return when.resolve(); + }); + + var req = { body: { token: "abcdef" } }; + + var res = { send: function(resp) { + revokeToken.restore(); + + resp.should.equal(200); + done(); + }}; + + auth.revoke(req,res); + }); + }); + + describe("login", function() { + it("returns login details", function(done) { + auth.login(null,{json: function(resp) { + resp.should.have.a.property("type","credentials"); + resp.should.have.a.property("prompts"); + resp.prompts.should.have.a.lengthOf(2); + done(); + }}); + }); + + }); + }); diff --git a/test/red/api/auth/permissions_spec.js b/test/red/api/auth/permissions_spec.js index e69de29bb..ebb82b4f2 100644 --- a/test/red/api/auth/permissions_spec.js +++ b/test/red/api/auth/permissions_spec.js @@ -0,0 +1,61 @@ +/** + * Copyright 2015 IBM Corp. + * + * 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 permissions = require("../../../../red/api/auth/permissions"); + + +describe("Auth permissions", function() { + describe("hasPermission", function() { + it('a user with no permissions',function() { + permissions.hasPermission({},"*").should.be.false; + }); + it('a user with global permissions',function() { + permissions.hasPermission({permissions:"*"},"read").should.be.true; + permissions.hasPermission({permissions:"*"},"write").should.be.true; + }); + it('a user with read permissions',function() { + permissions.hasPermission({permissions:"read"},"read").should.be.true; + permissions.hasPermission({permissions:"read"},"node.read").should.be.true; + permissions.hasPermission({permissions:"read"},"write").should.be.false; + permissions.hasPermission({permissions:"read"},"node.write").should.be.false; + }); + }); + + describe("needsPermission middleware", function() { + it('passes if no user on request',function(done) { + var needsPermission = permissions.needsPermission("*"); + needsPermission({},null,function() { + done(); + }); + }); + it('passes if user has required permission',function(done) { + var needsPermission = permissions.needsPermission("read"); + needsPermission({user:{permissions:"read"}},null,function() { + done(); + }); + }); + it('rejects if user does not have required permission',function(done) { + var needsPermission = permissions.needsPermission("write"); + needsPermission({user:{permissions:"read"}},{send: function(code) { + code.should.equal(401); + done(); + }},null); + }); + + }); +}); diff --git a/test/red/api/auth/strategies_spec.js b/test/red/api/auth/strategies_spec.js index 531bc7d4e..566d281d4 100644 --- a/test/red/api/auth/strategies_spec.js +++ b/test/red/api/auth/strategies_spec.js @@ -1,5 +1,5 @@ /** - * Copyright 2014 IBM Corp. + * Copyright 2015 IBM Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,201 @@ * limitations under the License. **/ +var should = require("should"); +var when = require('when'); +var sinon = require('sinon'); + + var strategies = require("../../../../red/api/auth/strategies"); +var Users = require("../../../../red/api/auth/users"); +var Tokens = require("../../../../red/api/auth/tokens"); +var Clients = require("../../../../red/api/auth/clients"); +describe("Auth strategies", function() { + describe("Password Token Exchange", function() { + + var userAuthentication; + afterEach(function() { + if (userAuthentication) { + userAuthentication.restore(); + } + }); + + it('Handles authentication failure',function(done) { + userAuthentication = sinon.stub(Users,"authenticate",function(username,password) { + return when.resolve(null); + }); + + strategies.passwordTokenExchange({},"user","password","scope",function(err,token) { + try { + should.not.exist(err); + token.should.be.false; + done(); + } catch(e) { + done(e); + } + }); + }); + + it('Creates new token on authentication success',function(done) { + userAuthentication = sinon.stub(Users,"authenticate",function(username,password) { + return when.resolve({username:"user"}); + }); + var tokenDetails = {}; + var tokenCreate = sinon.stub(Tokens,"create",function(username,client,scope) { + tokenDetails.username = username; + tokenDetails.client = client; + tokenDetails.scope = scope; + return when.resolve({accessToken: "123456"}); + }); + + strategies.passwordTokenExchange({id:"myclient"},"user","password","scope",function(err,token) { + try { + should.not.exist(err); + token.should.equal("123456"); + tokenDetails.should.have.property("username","user"); + tokenDetails.should.have.property("client","myclient"); + tokenDetails.should.have.property("scope","scope"); + done(); + } catch(e) { + done(e); + } finally { + tokenCreate.restore(); + } + }); + + }); + }); + + describe("Anonymous Strategy", function() { + it('Succeeds if anon user enabled',function(done) { + var userDefault = sinon.stub(Users,"default",function() { + return when.resolve("anon"); + }); + strategies.anonymousStrategy._success = strategies.anonymousStrategy.success; + strategies.anonymousStrategy.success = function(user) { + user.should.equal("anon"); + strategies.anonymousStrategy.success = strategies.anonymousStrategy._success; + delete strategies.anonymousStrategy._success; + userDefault.restore(); + done(); + }; + strategies.anonymousStrategy.authenticate({}); + }); + it('Fails if anon user not enabled',function(done) { + var userDefault = sinon.stub(Users,"default",function() { + return when.resolve(null); + }); + strategies.anonymousStrategy._fail = strategies.anonymousStrategy.fail; + strategies.anonymousStrategy.fail = function(err) { + err.should.equal(401); + strategies.anonymousStrategy.fail = strategies.anonymousStrategy._fail; + delete strategies.anonymousStrategy._fail; + userDefault.restore(); + done(); + }; + strategies.anonymousStrategy.authenticate({}); + }); + }); + + describe("Bearer Strategy", function() { + it('Rejects invalid token',function(done) { + var getToken = sinon.stub(Tokens,"get",function(token) { + return when.resolve(null); + }); + + strategies.bearerStrategy("1234",function(err,user) { + try { + should.not.exist(err); + user.should.be.false; + done(); + } catch(e) { + done(e); + } finally { + getToken.restore(); + } + }); + }); + it('Accepts valid token',function(done) { + var getToken = sinon.stub(Tokens,"get",function(token) { + return when.resolve({user:"user",scope:"scope"}); + }); + var getUser = sinon.stub(Users,"get",function(username) { + return when.resolve("aUser"); + }); + + strategies.bearerStrategy("1234",function(err,user,opts) { + try { + should.not.exist(err); + user.should.equal("aUser"); + opts.should.have.a.property("scope","scope"); + done(); + } catch(e) { + done(e); + } finally { + getToken.restore(); + getUser.restore(); + } + }); + }); + }); + + describe("Client Password Strategy", function() { + it('Accepts valid client',function(done) { + var testClient = {id:"node-red-editor",secret:"not_available"}; + var getClient = sinon.stub(Clients,"get",function(client) { + return when.resolve(testClient); + }); + + strategies.clientPasswordStrategy(testClient.id,testClient.secret,function(err,client) { + try { + should.not.exist(err); + client.should.eql(testClient); + done(); + } catch(e) { + done(e); + } finally { + getClient.restore(); + } + }); + }); + it('Rejects invalid client secret',function(done) { + var testClient = {id:"node-red-editor",secret:"not_available"}; + var getClient = sinon.stub(Clients,"get",function(client) { + return when.resolve(testClient); + }); + + strategies.clientPasswordStrategy(testClient.id,"invalid_secret",function(err,client) { + try { + should.not.exist(err); + client.should.be.false; + done(); + } catch(e) { + done(e); + } finally { + getClient.restore(); + } + }); + }); + it('Rejects invalid client id',function(done) { + var testClient = {id:"node-red-editor",secret:"not_available"}; + var getClient = sinon.stub(Clients,"get",function(client) { + return when.resolve(null); + }); + + strategies.clientPasswordStrategy("invalid_id","invalid_secret",function(err,client) { + try { + should.not.exist(err); + client.should.be.false; + done(); + } catch(e) { + done(e); + } finally { + getClient.restore(); + } + }); + }); + }); +}); + diff --git a/test/red/api/auth/tokens/index_spec.js b/test/red/api/auth/tokens/index_spec.js new file mode 100644 index 000000000..c52dace6c --- /dev/null +++ b/test/red/api/auth/tokens/index_spec.js @@ -0,0 +1,168 @@ +/** + * Copyright 2015 IBM Corp. + * + * 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 when = require("when"); +var sinon = require("sinon"); + + +var Tokens = require("../../../../../red/api/auth/tokens"); + + +describe("Tokens", function() { + describe("#init",function() { + var module = require("module"); + var originalLoader; + beforeEach(function() { + originalLoader = module._load; + }); + afterEach(function() { + module._load = originalLoader; + }); + + it('loads default storage plugin', function(done) { + module._load = function(name) { + name.should.equal("./localfilesystem"); + return {init: function(settings) {done()}}; + } + try { + Tokens.init({}); + } catch(err) { + done(err); + } + }); + it('loads the specified storage plugin', function(done) { + module._load = function(name) { + name.should.equal("./aTestExample"); + return {init: function(settings) {done()}}; + } + try { + Tokens.init({sessionStorageModule:"aTestExample"}); + } catch(err) { + done(err); + } + }); + + it('uses the provided storage plugin', function(done) { + Tokens.init({sessionStorageModule:{init:function(settings){done()}}}); + }); + }); + + + describe("#get",function() { + it('returns a valid token', function(done) { + Tokens.init({sessionStorageModule:{ + init:function(settings){}, + get: function(token) { + return when.resolve({user:"fred",accessExpires: Date.now()+10000}); + } + }}); + + Tokens.get("1234").then(function(token) { + try { + token.should.have.a.property("user","fred"); + done(); + } catch(err) { + done(err); + } + }); + }); + it('deletes an expired token and returns null', function(done) { + var sessionStorageModule = { + init:function(settings){}, + get: function(token) { + return when.resolve({user:"fred",accessExpires: Date.now()-10000}); + }, + delete: sinon.stub().returns(when.resolve()) + }; + + Tokens.init({sessionStorageModule:sessionStorageModule}); + + Tokens.get("1234").then(function(token) { + try { + should.not.exist(token); + sessionStorageModule.delete.calledWith("1234").should.be.true; + done(); + } catch(err) { + done(err); + } + }); + }); + + it('returns null for an invalid token', function(done) { + Tokens.init({sessionStorageModule:{ + init:function(settings){}, + get: function(token) { + return when.resolve(null); + } + }}); + + Tokens.get("1234").then(function(token) { + try { + should.not.exist(token); + done(); + } catch(err) { + done(err); + } + }); + }); + }); + + describe("#create",function() { + it('creates a token', function(done) { + var sessionStorageModule = { + init:function(settings){}, + create: sinon.stub().returns(when.resolve()) + }; + Tokens.init({sessionStorageModule:sessionStorageModule}); + Tokens.create("user","client","scope").then(function(token) { + try { + sessionStorageModule.create.called.should.be.true; + token.should.have.a.property('accessToken',sessionStorageModule.create.args[0][0]); + sessionStorageModule.create.args[0][1].should.have.a.property('user','user'); + sessionStorageModule.create.args[0][1].should.have.a.property('client','client'); + sessionStorageModule.create.args[0][1].should.have.a.property('scope','scope'); + done(); + } catch(err) { + done(err); + } + }); + }); + }); + + describe("#revoke", function() { + it('revokes a token', function(done) { + var deletedToken; + Tokens.init({sessionStorageModule:{ + init:function(settings){}, + delete: function(token) { + deletedToken = token; + return when.resolve(null); + } + }}); + + Tokens.revoke("1234").then(function() { + try { + deletedToken.should.equal("1234"); + done(); + } catch(err) { + done(err); + } + }); + }); + }); + +}); \ No newline at end of file diff --git a/test/red/api/auth/tokens/localfilesystem_spec.js b/test/red/api/auth/tokens/localfilesystem_spec.js new file mode 100644 index 000000000..dce3b8e07 --- /dev/null +++ b/test/red/api/auth/tokens/localfilesystem_spec.js @@ -0,0 +1,96 @@ +/** + * Copyright 2015 IBM Corp. + * + * 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 when = require("when"); +var sinon = require("sinon"); + +var fs = require('fs-extra'); +var path = require('path'); + +var localfilesystem = require("../../../../../red/api/auth/tokens/localfilesystem.js"); + + +describe("Tokens localfilesystem", function() { + var userDir = path.join(__dirname,".testUserHome"); + beforeEach(function(done) { + fs.remove(userDir,function(err) { + fs.mkdir(userDir,done); + }); + }); + afterEach(function(done) { + fs.remove(userDir,done); + }); + + it("initialise when no session file exists",function(done) { + localfilesystem.init({userDir:userDir}).then(function() { + localfilesystem.get("1234").then(function(token) { + should.not.exist(token); + done(); + }); + }); + }); + + it("initialises when session file exists", function(done) { + var sessions = {"1234":{"user":"nol","client":"node-red-admin","scope":["*"],"accessToken":"1234"}}; + fs.writeFileSync(path.join(userDir,".sessions.json"),JSON.stringify(sessions),"utf8"); + + localfilesystem.init({userDir:userDir}).then(function() { + localfilesystem.get("1234").then(function(token) { + token.should.eql(sessions['1234']); + done(); + }); + }); + }); + + it("writes new tokens to the session file",function(done) { + var sessions = {"1234":{"user":"nol","client":"node-red-admin","scope":["*"],"accessToken":"1234"}}; + fs.writeFileSync(path.join(userDir,".sessions.json"),JSON.stringify(sessions),"utf8"); + + localfilesystem.init({userDir:userDir}).then(function() { + localfilesystem.create("5678",{ + user:"fred", + client:"client", + scope:["read"], + accessToken:"5678" + }).then(function() { + var newSessions = JSON.parse(fs.readFileSync(path.join(userDir,".sessions.json"),"utf8")); + newSessions.should.have.a.property("1234"); + newSessions.should.have.a.property("5678"); + done(); + }); + }); + }); + + it("deletes tokens from the session file",function(done) { + var sessions = { + "1234":{"user":"nol","client":"node-red-admin","scope":["*"],"accessToken":"1234"}, + "5678":{"user":"fred","client":"client","scope":["read"],"accessToken":"5678"} + }; + fs.writeFileSync(path.join(userDir,".sessions.json"),JSON.stringify(sessions),"utf8"); + + localfilesystem.init({userDir:userDir}).then(function() { + localfilesystem.delete("5678").then(function() { + var newSessions = JSON.parse(fs.readFileSync(path.join(userDir,".sessions.json"),"utf8")); + newSessions.should.have.a.property("1234"); + newSessions.should.not.have.a.property("5678"); + done(); + }); + }); + }); + + +}); diff --git a/test/red/api/auth/tokens_spec.js b/test/red/api/auth/tokens_spec.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/red/api/auth/users_spec.js b/test/red/api/auth/users_spec.js index e69de29bb..c2aad2f12 100644 --- a/test/red/api/auth/users_spec.js +++ b/test/red/api/auth/users_spec.js @@ -0,0 +1,185 @@ +/** + * Copyright 2015 IBM Corp. + * + * 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 when = require('when'); +var sinon = require('sinon'); + +var Users = require("../../../../red/api/auth/users"); + +describe("Users", function() { + describe('Initalised with a credentials object, no anon',function() { + before(function() { + Users.init({ + type:"credentials", + users:[{ + username:"fred", + password:"5f4dcc3b5aa765d61d8327deb882cf99", // 'password' + permissions:"*" + }] + }); + }); + describe('#get',function() { + it('returns known user',function(done) { + Users.get("fred").then(function(user) { + try { + user.should.have.a.property("username","fred"); + user.should.have.a.property("permissions","*"); + user.should.not.have.a.property("password"); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('returns null for unknown user', function(done) { + Users.get("barney").then(function(user) { + try { + should.not.exist(user); + done(); + } catch(err) { + done(err); + } + }); + }); + }); + + describe('#default',function() { + it('returns null for default user', function(done) { + Users.default().then(function(user) { + try { + should.not.exist(user); + done(); + } catch(err) { + done(err); + } + }); + }); + }); + + describe('#authenticate',function() { + + it('authenticates a known user', function(done) { + Users.authenticate('fred','password').then(function(user) { + try { + user.should.have.a.property("username","fred"); + user.should.have.a.property("permissions","*"); + user.should.not.have.a.property("password"); + done(); + } catch(err) { + done(err); + } + }); + }); + it('rejects invalid password for a known user', function(done) { + Users.authenticate('fred','wrong').then(function(user) { + try { + should.not.exist(user); + done(); + } catch(err) { + done(err); + } + }); + }); + + it('rejects invalid user', function(done) { + Users.authenticate('barney','wrong').then(function(user) { + try { + should.not.exist(user); + done(); + } catch(err) { + done(err); + } + }); + }); + }); + }); + + + describe('Initalised with a credentials object including anon',function() { + before(function() { + Users.init({ + type:"credentials", + users:[], + default: { permissions: "*" } + }); + }); + describe('#default',function() { + it('returns default user', function(done) { + Users.default().then(function(user) { + try { + user.should.have.a.property('anonymous',true); + user.should.have.a.property('permissions','*'); + done(); + } catch(err) { + done(err); + } + }); + }); + }); + }); + + describe('Initialised with a credentials object with user functions',function() { + var authUsername = ''; + var authPassword = ''; + before(function() { + Users.init({ + type:"credentials", + users:function(username) { + return when.resolve({'username':'dave','permissions':'read'}); + }, + authenticate: function(username,password) { + authUsername = username; + authPassword = password; + return when.resolve({'username':'pete','permissions':'write'}); + } + }); + }); + + describe('#get',function() { + it('delegates get user',function(done) { + Users.get('dave').then(function(user) { + try { + user.should.have.a.property("username","dave"); + user.should.have.a.property("permissions","read"); + user.should.not.have.a.property("password"); + done(); + } catch(err) { + done(err); + } + }); + }); + it('delegates authenticate user',function(done) { + Users.authenticate('pete','secret').then(function(user) { + try { + user.should.have.a.property("username","pete"); + user.should.have.a.property("permissions","write"); + user.should.not.have.a.property("password"); + authUsername.should.equal('pete'); + authPassword.should.equal('secret'); + done(); + } catch(err) { + done(err); + } + }); + }); + }); + + + + }); +}); \ No newline at end of file diff --git a/test/red/comms_spec.js b/test/red/comms_spec.js index 19cd02fa6..b4f9a1d1e 100644 --- a/test/red/comms_spec.js +++ b/test/red/comms_spec.js @@ -1,5 +1,5 @@ /** - * Copyright 2014 IBM Corp. + * Copyright 2014, 2015 IBM Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,18 @@ **/ var should = require("should"); +var sinon = require("sinon"); + +var when = require("when"); var http = require('http'); var express = require('express'); var app = express(); var WebSocket = require('ws'); var comms = require("../../red/comms.js"); +var Users = require("../../red/api/auth/users"); +var Tokens = require("../../red/api/auth/tokens"); + var address = '127.0.0.1'; var listenPort = 0; // use ephemeral port @@ -188,5 +194,152 @@ describe("comms", function() { }); }); }); + + describe('authentication required, no anonymous',function() { + var server; + var url; + var port; + var getDefaultUser; + var getUser; + var getToken; + before(function(done) { + getDefaultUser = sinon.stub(Users,"default",function() { return when.resolve(null);}); + getUser = sinon.stub(Users,"get", function(username) { + if (username == "fred") { + return when.resolve({permissions:"read"}); + } else { + return when.resolve(null); + } + }); + getToken = sinon.stub(Tokens,"get",function(token) { + if (token == "1234") { + return when.resolve({user:"fred"}); + } else if (token == "5678") { + return when.resolve({user:"barney"}); + } else { + return when.resolve(null); + } + }); + + + server = http.createServer(function(req,res){app(req,res)}); + comms.init(server, {adminAuth:{}}); + server.listen(listenPort, address); + server.on('listening', function() { + port = server.address().port; + url = 'http://' + address + ':' + port + '/comms'; + comms.start(); + done(); + }); + }); + after(function() { + getDefaultUser.restore(); + getUser.restore(); + getToken.restore(); + comms.stop(); + }); + + it('prevents connections that do not authenticate',function(done) { + var ws = new WebSocket(url); + var count = 0; + var interval; + ws.on('open', function() { + ws.send('{"subscribe":"foo"}'); + }); + ws.on('close', function() { + done(); + }); + }); + + it('allows connections that do authenticate',function(done) { + var ws = new WebSocket(url); + var received = 0; + ws.on('open', function() { + ws.send('{"auth":"1234"}'); + }); + ws.on('message', function(msg) { + received++; + if (received == 1) { + msg.should.equal('{"auth":"ok"}'); + ws.send('{"subscribe":"foo"}'); + comms.publish('foo', 'correct'); + } else { + msg.should.equal('{"topic":"foo","data":"correct"}'); + ws.close(); + } + }); + + ws.on('close', function() { + received.should.equal(2); + done(); + }); + }); + + it('rejects connections for non-existant token',function(done) { + var ws = new WebSocket(url); + var received = 0; + ws.on('open', function() { + ws.send('{"auth":"2345"}'); + }); + ws.on('close', function() { + done(); + }); + }); + it('rejects connections for invalid token',function(done) { + var ws = new WebSocket(url); + var received = 0; + ws.on('open', function() { + ws.send('{"auth":"5678"}'); + }); + ws.on('close', function() { + done(); + }); + }); + }); + describe('authentication required, anonymous enabled',function() { + var server; + var url; + var port; + var getDefaultUser; + before(function(done) { + getDefaultUser = sinon.stub(Users,"default",function() { return when.resolve({permissions:"read"});}); + server = http.createServer(function(req,res){app(req,res)}); + comms.init(server, {adminAuth:{}}); + server.listen(listenPort, address); + server.on('listening', function() { + port = server.address().port; + url = 'http://' + address + ':' + port + '/comms'; + comms.start(); + done(); + }); + }); + after(function() { + getDefaultUser.restore(); + comms.stop(); + }); + + it('allows anonymous connections that do not authenticate',function(done) { + var ws = new WebSocket(url); + var count = 0; + var interval; + ws.on('open', function() { + ws.send('{"subscribe":"foo"}'); + setTimeout(function() { + comms.publish('foo', 'correct'); + },200); + }); + ws.on('message', function(msg) { + msg.should.equal('{"topic":"foo","data":"correct"}'); + count++; + ws.close(); + }); + ws.on('close', function() { + count.should.equal(1); + done(); + }); + }); + }); + + });