diff --git a/packages/instance-model/.travis.yml b/packages/instance-model/.travis.yml index a4cc1113483..9945d5ff5f0 100644 --- a/packages/instance-model/.travis.yml +++ b/packages/instance-model/.travis.yml @@ -8,9 +8,9 @@ before_install: - npm config -g list -l - npm --version script: npm run-script ci -cache: - directories: - - node_modules +# cache: +# directories: +# - node_modules notifications: flowdock: e3dc17bc8a2c1b3412abe3e5747f8291 env: diff --git a/packages/instance-model/lib/fetch.js b/packages/instance-model/lib/fetch.js index b6d4503e389..7ce4ce1d24c 100644 --- a/packages/instance-model/lib/fetch.js +++ b/packages/instance-model/lib/fetch.js @@ -65,6 +65,8 @@ function getBuildInfo(done, results) { debug('checking we can get buildInfo...'); db.admin().buildInfo(function(err, res) { if (err) { + // buildInfo doesn't require any privileges to run, so if it fails, + // something really went wrong and we should return the error. debug('buildInfo failed!', err); err.command = 'buildInfo'; return done(err); @@ -131,24 +133,25 @@ function parseHostInfo(resp) { function getHostInfo(done, results) { var db = results.db; - debug('checking we can get hostInfo...'); var spec = { hostInfo: 1 }; var options = {}; db.admin().command(spec, options, function(err, res) { if (err) { - if (/^not authorized/.test(err.message)) { - debug('hostInfo unavailable for this user and thats ok!'); + if (isNotAuthorized(err)) { + // if the error is that the user is not authorized, silently ignore it + // and return an empty document + debug('user does not have hostInfo privilege, returning empty document {}'); done(null, {}); return; } + // something else went wrong and we should return the error. debug('driver error', err); err.command = 'hostInfo'; done(err); return; } - debug('got hostInfo successully!'); done(null, parseHostInfo(res)); }); } @@ -179,11 +182,12 @@ function listDatabases(done, results) { db.admin().command(spec, options, function(err, res) { if (err) { if (isNotAuthorized(err)) { + // we caught this further up already and really should never get here! debug('listDatabases failed. returning empty list []'); done(null, []); return; } - + // the command failed for another reason, report the error debug('listDatabases failed', err); err.command = 'listDatabases'; done(err); @@ -270,9 +274,7 @@ function getDatabases(done, results) { async.parallel(_.map(dbnames, function(name) { var result = _.partial(getDatabase, db, name); return result; - }), function(err, res) { - done(err, res); - }); + }), done); } @@ -284,6 +286,7 @@ function getUserInfo(done, results) { connectionStatus: 1, showPrivileges: true }, function(err, res) { + // no auth required, if this fails there was a real problem if (err) { done(err); } @@ -297,6 +300,7 @@ function getUserInfo(done, results) { usersInfo: user, showPrivileges: true }, function(_err, _res) { + // should always succeed for the logged-in user if (_err) { done(_err); } @@ -355,6 +359,15 @@ function getDatabaseCollections(db, done) { db.listCollections(spec, options).toArray(function(err, res) { if (err) { + if (isNotAuthorized(err)) { + // if the error is that the user is not authorized, silently ignore it + // and return an empty list + debug('not allowed to run `listCollections` command on %s, returning' + + ' empty result [].', db.databaseName); + return done(null, []); + } + // the command failed for another reason, report the error + debug('listCollections failed', err); err.command = 'listCollections'; return done(err); } @@ -463,10 +476,10 @@ function getInstanceDetail(db, done) { module.exports = getInstanceDetail; - -// module.exports.getCollections = getCollections; -// module.exports.getDatabaseCollections = getDatabaseCollections; -// module.exports.getDatabases = getDatabases; -// module.exports.getDatabase = getDatabase; -// module.exports.getBuildInfo = getBuildInfo; -// module.exports.getHostInfo = getHostInfo; +module.exports.getBuildInfo = getBuildInfo; +module.exports.getHostInfo = getHostInfo; +module.exports.listDatabases = listDatabases; +module.exports.getAllowedDatabases = getAllowedDatabases; +module.exports.getAllowedCollections = getAllowedCollections; +module.exports.getDatabaseCollections = getDatabaseCollections; +module.exports.listCollections = listCollections; diff --git a/packages/instance-model/test/fetch-mocked.test.js b/packages/instance-model/test/fetch-mocked.test.js new file mode 100644 index 00000000000..1f9e1769882 --- /dev/null +++ b/packages/instance-model/test/fetch-mocked.test.js @@ -0,0 +1,135 @@ +var assert = require('assert'); +var fetch = require('../').fetch; +var fixtures = require('./fixtures'); + +// var debug = require('debug')('mongodb-instance-model:test:fetch-mocked'); + + +describe('unit tests on fetch functions', function() { + var makeMockDB; + + before(function() { + /** + * Create a mock db object that will return an error or a result on + * any of its methods. Pass in either and error or a result, but not + * both (just like regular Errbacks). + * + * @param {Error|null} err if the call should return an error, specify + * the error object here. + * @param {Any} res if the call should return a value, specify + * the value here. + * @return {Object} a db object that behaves like the mongodb + * driver + */ + makeMockDB = function(err, res) { + var db = {}; + db.admin = function() { + return { + // add more db methods here as needed. + + // buildInfo is a separate function on the admin object + buildInfo: function(callback) { + return callback(err, res); + }, + // all other commands return the global err/res results + command: function(command, options, callback) { + return callback(err, res); + } + }; + }; + return db; + }; + }); + + describe('getBuildInfo', function() { + it('should pass on any error that buildInfo returns', function(done) { + // instead of the real db handle, pass in the mocked one + var results = { + // make a db that always returns error for db.admin().buildInfo() + db: makeMockDB(new Error('some strange error'), null) + }; + fetch.getBuildInfo(function(err, res) { + assert.equal(res, null); + assert.equal(err.command, 'buildInfo'); + assert.equal(err.message, 'some strange error'); + done(); + }, results); + }); + }); + + describe('getHostInfo', function() { + it('should ignore auth errors gracefully', function(done) { + // instead of the real db handle, pass in the mocked one + var results = { + db: makeMockDB(new Error('not authorized on fooBarDatabase to execute command ' + + '{listCollections: true, filter: {}, cursor: {}'), null) + }; + fetch.getHostInfo(function(err, res) { + assert.equal(err, null); + assert.deepEqual(res, []); + done(); + }, results); + }); + it('should pass on other errors from the hostInfo command', function(done) { + // instead of the real db handle, pass in the mocked one + var results = { + db: makeMockDB(new Error('some other error from hostInfo'), null) + }; + fetch.getHostInfo(function(err, res) { + assert.ok(err); + assert.equal(err.command, 'hostInfo'); + assert.deepEqual(res, null); + done(); + }, results); + }); + }); + + describe('listDatabases', function() { + var results = {}; + + beforeEach(function() { + results.userInfo = fixtures.USER_INFO; + }); + + it('should ignore auth errors gracefully', function(done) { + // instead of the real db handle, pass in the mocked one + results.db = makeMockDB(new Error('not authorized on admin to execute command ' + + '{ listDatabases: 1.0 }'), null); + + fetch.listDatabases(function(err, res) { + assert.equal(err, null); + assert.deepEqual(res, []); + done(); + }, results); + }); + it('should pass on other errors from the listDatabases command', function(done) { + // instead of the real db handle, pass in the mocked one + results.db = makeMockDB(new Error('some other error from hostInfo'), null); + + fetch.listDatabases(function(err, res) { + assert.ok(err); + assert.equal(err.command, 'listDatabases'); + assert.deepEqual(res, null); + done(); + }, results); + }); + }); + + describe('getAllowedDatabases', function() { + it('should return the correct results'); + // ... + }); + describe('getAllowedCollections', function() { + it('should return the correct results'); + // ... + }); + describe('getDatabaseCollections', function() { + it('should ignore auth errors gracefully'); + it('should pass on other errors from the listCollections command'); + // ... + }); + describe('listCollections', function() { + it('should merge the two collection lists correctly'); + // ... + }); +}); diff --git a/packages/instance-model/test/fetch.test.js b/packages/instance-model/test/fetch.test.js index bfd2b6b9330..3fa38ac2add 100644 --- a/packages/instance-model/test/fetch.test.js +++ b/packages/instance-model/test/fetch.test.js @@ -28,50 +28,6 @@ describe('mongodb-instance-model#fetch', function() { done(); }); }); - // it('should list collections', function(done) { - // assert(db); - // fetch.getAllCollections(db, function(err, res) { - // if (err) { - // return done(err); - // } - // debug('list collections', JSON.stringify(res, null, 2)); - // done(); - // }); - // }); - // - // it('should list databases', function(done) { - // assert(db); - // fetch.getDatabases(db, function(err, res) { - // if (err) { - // return done(err); - // } - // debug('list databases', JSON.stringify(res, null, 2)); - // done(); - // }); - // }); - // - // it('should get build info', function(done) { - // assert(db); - // fetch.getBuildInfo(db, function(err, res) { - // if (err) { - // return done(err); - // } - // debug('build info', JSON.stringify(res, null, 2)); - // done(); - // }); - // }); - // - // it('should get host info', function(done) { - // assert(db); - // fetch.getHostInfo(db, function(err, res) { - // if (err) { - // return done(err); - // } - // debug('host info', JSON.stringify(res, null, 2)); - // done(); - // }); - // }); - it('should get instance details', function(done) { assert(db); fetch(db, function(err, res) { @@ -141,44 +97,6 @@ describe('mongodb-instance-model#fetch', function() { done(); }); }); - - // it('should list databases', function(done) { - // if (process.env.dry ) { - // this.skip(); - // return; - // } - // this.slow(5000); - // this.timeout(10000); - // assert(db, 'requires successful connection'); - // - // fetch.getDatabases(db, function(err, res) { - // if (err) return done(err); - // - // assert(Array.isArray(res)); - // assert(res.length > 0, 'Database list is empty'); - // done(); - // }); - // }); - // - // it('should list collections', function(done) { - // if (process.env.dry ) { - // this.skip(); - // return; - // } - // this.slow(5000); - // this.timeout(10000); - // assert(db, 'requires successful connection'); - // - // fetch.getAllCollections(db, function(err, res) { - // if (err) return done(err); - // - // assert(Array.isArray(res)); - // assert(res.length > 0, 'Collection list is empty'); - // done(); - // }); - // }); - - it('should get instance details', function(done) { if (process.env.dry) { this.skip(); diff --git a/packages/instance-model/test/fixtures.js b/packages/instance-model/test/fixtures.js new file mode 100644 index 00000000000..b644001894e --- /dev/null +++ b/packages/instance-model/test/fixtures.js @@ -0,0 +1,383 @@ +/* eslint indent: 0, quotes: 0, key-spacing: 0 */ + +var HOST_INFO = { + "system" : { + "currentTime" : new Date("2015-12-03T00:51:06.763Z"), + "hostname" : "Groot.local", + "cpuAddrSize" : 64, + "memSizeMB" : 16384, + "numCores" : 4, + "cpuArch" : "x86_64", + "numaEnabled" : false + }, + "os" : { + "type" : "Darwin", + "name" : "Mac OS X", + "version" : "15.0.0" + }, + "extra" : { + "versionString" : "Darwin Kernel Version 15.0.0: Sat Sep 19 15:53:46 PDT 2015; root:xnu-3247.10.11~1/RELEASE_X86_64", + "alwaysFullSync" : 0, + "nfsAsync" : 0, + "model" : "MacBookPro12,1", + "physicalCores" : 2, + "cpuFrequencyMHz" : 3100, + "cpuString" : "Intel(R) Core(TM) i7-5557U CPU @ 3.10GHz", + "cpuFeatures" : "FPU VME DE PSE TSC MSR PAE MCE CX8 APIC SEP MTRR PGE MCA CMOV PAT PSE36 CLFSH DS ACPI MMX FXSR SSE SSE2 SS HTT TM PBE SSE3 PCLMULQDQ DTES64 MON DSCPL VMX EST TM2 SSSE3 FMA CX16 TPR PDCM SSE4.1 SSE4.2 x2APIC MOVBE POPCNT AES PCID XSAVE OSXSAVE SEGLIM64 TSCTMR AVX1.0 RDRAND F16C SYSCALL XD 1GBPAGE EM64T LAHF LZCNT PREFETCHW RDTSCP TSCI", + "pageSize" : 4096, + "scheduler" : "multiq" + }, + "ok" : 1 +}; + + +var USER_INFO = { + "_id" : "admin.john", + "user" : "john", + "db" : "admin", + "roles" : [ + { + "role" : "mongodb-user", + "db" : "tenants" + }, + { + "role" : "read", + "db" : "reporting" + }, + { + "role" : "read", + "db" : "products" + }, + { + "role" : "read", + "db" : "sales" + }, + { + "role" : "readWrite", + "db" : "accounts" + } + ], + "inheritedRoles" : [ + { + "role" : "readWrite", + "db" : "accounts" + }, + { + "role" : "read", + "db" : "sales" + }, + { + "role" : "read", + "db" : "products" + }, + { + "role" : "read", + "db" : "reporting" + }, + { + "role" : "mongodb-user", + "db" : "tenants" + } + ], + "inheritedPrivileges" : [ + { + "resource" : { + "db" : "tenants", + "collection" : "mongodb" + }, + "actions" : [ + "collStats", + "find" + ] + }, + { + "resource" : { + "db" : "reporting", + "collection" : "" + }, + "actions" : [ + "collStats", + "dbHash", + "dbStats", + "find", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead" + ] + }, + { + "resource" : { + "anyResource" : true + }, + "actions" : [ + "listCollections" + ] + }, + { + "resource" : { + "db" : "reporting", + "collection" : "system.indexes" + }, + "actions" : [ + "collStats", + "dbHash", + "dbStats", + "find", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead" + ] + }, + { + "resource" : { + "db" : "reporting", + "collection" : "system.js" + }, + "actions" : [ + "collStats", + "dbHash", + "dbStats", + "find", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead" + ] + }, + { + "resource" : { + "db" : "reporting", + "collection" : "system.namespaces" + }, + "actions" : [ + "collStats", + "dbHash", + "dbStats", + "find", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead" + ] + }, + { + "resource" : { + "db" : "products", + "collection" : "" + }, + "actions" : [ + "collStats", + "dbHash", + "dbStats", + "find", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead" + ] + }, + { + "resource" : { + "db" : "products", + "collection" : "system.indexes" + }, + "actions" : [ + "collStats", + "dbHash", + "dbStats", + "find", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead" + ] + }, + { + "resource" : { + "db" : "products", + "collection" : "system.js" + }, + "actions" : [ + "collStats", + "dbHash", + "dbStats", + "find", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead" + ] + }, + { + "resource" : { + "db" : "products", + "collection" : "system.namespaces" + }, + "actions" : [ + "collStats", + "dbHash", + "dbStats", + "find", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead" + ] + }, + { + "resource" : { + "db" : "sales", + "collection" : "" + }, + "actions" : [ + "collStats", + "dbHash", + "dbStats", + "find", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead" + ] + }, + { + "resource" : { + "db" : "sales", + "collection" : "system.indexes" + }, + "actions" : [ + "collStats", + "dbHash", + "dbStats", + "find", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead" + ] + }, + { + "resource" : { + "db" : "sales", + "collection" : "system.js" + }, + "actions" : [ + "collStats", + "dbHash", + "dbStats", + "find", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead" + ] + }, + { + "resource" : { + "db" : "sales", + "collection" : "system.namespaces" + }, + "actions" : [ + "collStats", + "dbHash", + "dbStats", + "find", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead" + ] + }, + { + "resource" : { + "db" : "accounts", + "collection" : "" + }, + "actions" : [ + "collStats", + "convertToCapped", + "createCollection", + "createIndex", + "dbHash", + "dbStats", + "dropCollection", + "dropIndex", + "emptycapped", + "find", + "insert", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead", + "remove", + "renameCollectionSameDB", + "update" + ] + }, + { + "resource" : { + "db" : "accounts", + "collection" : "system.indexes" + }, + "actions" : [ + "collStats", + "dbHash", + "dbStats", + "find", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead" + ] + }, + { + "resource" : { + "db" : "accounts", + "collection" : "system.js" + }, + "actions" : [ + "collStats", + "convertToCapped", + "createCollection", + "createIndex", + "dbHash", + "dbStats", + "dropCollection", + "dropIndex", + "emptycapped", + "find", + "insert", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead", + "remove", + "renameCollectionSameDB", + "update" + ] + }, + { + "resource" : { + "db" : "accounts", + "collection" : "system.namespaces" + }, + "actions" : [ + "collStats", + "dbHash", + "dbStats", + "find", + "killCursors", + "listCollections", + "listIndexes", + "planCacheRead" + ] + } + ] +}; + +module.exports = { + HOST_INFO: HOST_INFO, + USER_INFO: USER_INFO +};