diff --git a/package-lock.json b/package-lock.json index 5c5da8c28..9472c1d95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1140,7 +1140,8 @@ "delay": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", - "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==" + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "dev": true }, "delayed-stream": { "version": "1.0.0", diff --git a/package.json b/package.json index 172962e54..d95de3f55 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "db-migrate": "^0.11.12", "db-migrate-mysql": "^2.1.2", "debug": "^4.3.1", - "delay": "^5.0.0", "dockerode": "^3.3.0", "ejs": "^3.1.6", "ejs-cli": "^2.2.1", @@ -75,6 +74,7 @@ "xml2js": "^0.4.23" }, "devDependencies": { + "delay": "^5.0.0", "expect.js": "*", "hock": "^1.4.1", "js2xmlparser": "^4.0.1", diff --git a/src/eventlog.js b/src/eventlog.js index e238e23d2..f632f96ca 100644 --- a/src/eventlog.js +++ b/src/eventlog.js @@ -2,11 +2,11 @@ exports = module.exports = { add, - upsert, + upsertLoginEvent, get, getAllPaged, - getByCreationTime, cleanup, + _clear: clear, // keep in sync with webadmin index.js filter ACTION_ACTIVATE: 'cloudron.activate', @@ -77,94 +77,118 @@ exports = module.exports = { }; const assert = require('assert'), + database = require('./database.js'), debug = require('debug')('box:eventlog'), - eventlogdb = require('./eventlogdb.js'), + mysql = require('mysql'), notifications = require('./notifications.js'), safe = require('safetydance'), - util = require('util'), uuid = require('uuid'); -const NOOP_CALLBACK = function (error) { if (error) debug(error); }; +const EVENTLOG_FIELDS = [ 'id', 'action', 'source', 'data', 'creationTime' ].join(','); -function add(action, source, data, callback) { +function postProcess(record) { + // usually we have sourceJson and dataJson, however since this used to be the JSON data type, we don't + record.source = safe.JSON.parse(record.source); + record.data = safe.JSON.parse(record.data); + + return record; +} + +// never throws, only logs because previously code did not take a callback +async function add(action, source, data) { assert.strictEqual(typeof action, 'string'); assert.strictEqual(typeof source, 'object'); assert.strictEqual(typeof data, 'object'); - assert(!callback || typeof callback === 'function'); - callback = callback || NOOP_CALLBACK; - - eventlogdb.add(uuid.v4(), action, source, data, async function (error, id) { - if (error) return callback(error); - - callback(null, { id: id }); - - await safe(notifications.onEvent(id, action, source, data)); - }); + const id = uuid.v4(); + try { + await database.query('INSERT INTO eventlog (id, action, source, data) VALUES (?, ?, ?, ?)', [ id, action, JSON.stringify(source), JSON.stringify(data) ]); + await notifications.onEvent(id, action, source, data); + return id; + } catch (error) { + debug('add: error adding event', error); + return null; + } } -function upsert(action, source, data, callback) { +// never throws, only logs because previously code did not take a callback +async function upsertLoginEvent(action, source, data) { assert.strictEqual(typeof action, 'string'); assert.strictEqual(typeof source, 'object'); assert.strictEqual(typeof data, 'object'); - assert(!callback || typeof callback === 'function'); - callback = callback || NOOP_CALLBACK; + // can't do a real sql upsert, for frequent eventlog entries we only have to do 2 queries once a day + const queries = [{ + query: 'UPDATE eventlog SET creationTime=NOW(), data=? WHERE action = ? AND source LIKE ? AND DATE(creationTime)=CURDATE()', + args: [ JSON.stringify(data), action, JSON.stringify(source) ] + }, { + query: 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE action = ? AND source LIKE ? AND DATE(creationTime)=CURDATE()', + args: [ action, JSON.stringify(source) ] + }]; - eventlogdb.upsert(uuid.v4(), action, source, data, async function (error, id) { - if (error) return callback(error); + try { + const result = await database.transaction(queries); + if (result[0].affectedRows >= 1) return result[1][0].id; - callback(null, { id: id }); - - await safe(notifications.onEvent(id, action, source, data)); - }); + // no existing eventlog found, create one + return await add(action, source, data); + } catch (error) { + debug('add: error adding event', error); + return null; + } } -function get(id, callback) { +async function get(id) { assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - eventlogdb.get(id, function (error, result) { - if (error) return callback(error); + const result = await database.query('SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE id = ?', [ id ]); + if (result.length === 0) return null; - callback(null, result); - }); + return postProcess(result[0]); } -function getAllPaged(actions, search, page, perPage, callback) { +async function getAllPaged(actions, search, page, perPage) { assert(Array.isArray(actions)); assert(typeof search === 'string' || search === null); assert.strictEqual(typeof page, 'number'); assert.strictEqual(typeof perPage, 'number'); - assert.strictEqual(typeof callback, 'function'); - eventlogdb.getAllPaged(actions, search, page, perPage, function (error, events) { - if (error) return callback(error); + let data = []; + let query = `SELECT ${EVENTLOG_FIELDS} FROM eventlog`; - callback(null, events); + if (actions.length || search) query += ' WHERE'; + if (search) query += ' (source LIKE ' + mysql.escape('%' + search + '%') + ' OR data LIKE ' + mysql.escape('%' + search + '%') + ')'; + + if (actions.length && search) query += ' AND ( '; + actions.forEach(function (action, i) { + query += ' (action LIKE ' + mysql.escape(`%${action}%`) + ') '; + if (i < actions.length-1) query += ' OR '; }); + if (actions.length && search) query += ' ) '; + + query += ' ORDER BY creationTime DESC LIMIT ?,?'; + + data.push((page-1)*perPage); + data.push(perPage); + + const results = await database.query(query, data); + results.forEach(postProcess); + return results; } -function getByCreationTime(creationTime, callback) { - assert(util.types.isDate(creationTime)); - assert.strictEqual(typeof callback, 'function'); +async function cleanup(options) { + assert.strictEqual(typeof options, 'object'); - eventlogdb.getByCreationTime(creationTime, function (error, events) { - if (error) return callback(error); + const creationTime = options.creationTime || new Date(Date.now() - 60 * 60 * 24 * 10 * 1000); // 10 days ago - callback(null, events); - }); + const results = await database.query('SELECT * FROM eventlog WHERE creationTime <= ?', [ creationTime ]); + + for (const result of results) { + await database.query('DELETE FROM notifications WHERE eventId=?', [ result.id ]); // remove notifications that reference the events as well + await database.query('DELETE FROM eventlog WHERE id=?', [ result.id ]); + } } -function cleanup(callback) { - callback = callback || NOOP_CALLBACK; - - var d = new Date(); - d.setDate(d.getDate() - 10); // 10 days ago - - eventlogdb.delByCreationTime(d, function (error) { - if (error) return callback(error); - - callback(null); - }); +async function clear() { + await database.query('DELETE FROM eventlog'); } diff --git a/src/eventlogdb.js b/src/eventlogdb.js deleted file mode 100644 index 749607411..000000000 --- a/src/eventlogdb.js +++ /dev/null @@ -1,171 +0,0 @@ -'use strict'; - -exports = module.exports = { - get, - getAllPaged, - getByCreationTime, - add, - upsert, - count, - delByCreationTime, - - _clear: clear -}; - -const assert = require('assert'), - async = require('async'), - BoxError = require('./boxerror.js'), - database = require('./database.js'), - mysql = require('mysql'), - safe = require('safetydance'), - util = require('util'); - -const EVENTLOG_FIELDS = [ 'id', 'action', 'source', 'data', 'creationTime' ].join(','); - -function postProcess(eventLog) { - // usually we have sourceJson and dataJson, however since this used to be the JSON data type, we don't - eventLog.source = safe.JSON.parse(eventLog.source); - eventLog.data = safe.JSON.parse(eventLog.data); - - return eventLog; -} - -function get(eventId, callback) { - assert.strictEqual(typeof eventId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE id = ?', [ eventId ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Eventlog not found')); - - callback(null, postProcess(result[0])); - }); -} - -function getAllPaged(actions, search, page, perPage, callback) { - assert(Array.isArray(actions)); - assert(typeof search === 'string' || search === null); - assert.strictEqual(typeof page, 'number'); - assert.strictEqual(typeof perPage, 'number'); - assert.strictEqual(typeof callback, 'function'); - - var data = []; - var query = 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog'; - - if (actions.length || search) query += ' WHERE'; - if (search) query += ' (source LIKE ' + mysql.escape('%' + search + '%') + ' OR data LIKE ' + mysql.escape('%' + search + '%') + ')'; - - if (actions.length && search) query += ' AND ( '; - actions.forEach(function (action, i) { - query += ' (action LIKE ' + mysql.escape(`%${action}%`) + ') '; - if (i < actions.length-1) query += ' OR '; - }); - if (actions.length && search) query += ' ) '; - - query += ' ORDER BY creationTime DESC LIMIT ?,?'; - - data.push((page-1)*perPage); - data.push(perPage); - - database.query(query, data, function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - results.forEach(postProcess); - - callback(null, results); - }); -} - -function getByCreationTime(creationTime, callback) { - assert(util.types.isDate(creationTime)); - assert.strictEqual(typeof callback, 'function'); - - var query = 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE creationTime >= ? ORDER BY creationTime DESC'; - database.query(query, [ creationTime ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - results.forEach(postProcess); - - callback(null, results); - }); -} - -function add(id, action, source, data, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof action, 'string'); - assert.strictEqual(typeof source, 'object'); - assert.strictEqual(typeof data, 'object'); - assert.strictEqual(typeof callback, 'function'); - - database.query('INSERT INTO eventlog (id, action, source, data) VALUES (?, ?, ?, ?)', [ id, action, JSON.stringify(source), JSON.stringify(data) ], function (error, result) { - if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error)); - if (error || result.affectedRows !== 1) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null, id); - }); -} - -// id is only used if we didn't do an update but insert instead -function upsert(id, action, source, data, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof action, 'string'); - assert.strictEqual(typeof source, 'object'); - assert.strictEqual(typeof data, 'object'); - assert.strictEqual(typeof callback, 'function'); - - // can't do a real sql upsert, for frequent eventlog entries we only have to do 2 queries once a day - var queries = [{ - query: 'UPDATE eventlog SET creationTime=NOW(), data=? WHERE action = ? AND source LIKE ? AND DATE(creationTime)=CURDATE()', - args: [ JSON.stringify(data), action, JSON.stringify(source) ] - }, { - query: 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE action = ? AND source LIKE ? AND DATE(creationTime)=CURDATE()', - args: [ action, JSON.stringify(source) ] - }]; - - database.transaction(queries, function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result[0].affectedRows >= 1) return callback(null, result[1][0].id); - - // no existing eventlog found, create one - add(id, action, source, data, callback); - }); -} - -function count(callback) { - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT COUNT(*) AS total FROM eventlog', function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - return callback(null, result[0].total); - }); -} - -function clear(callback) { - database.query('DELETE FROM eventlog', function (error) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - -function delByCreationTime(creationTime, callback) { - assert(util.types.isDate(creationTime)); - assert.strictEqual(typeof callback, 'function'); - - // remove notifications that reference the events as well - database.query('SELECT * FROM eventlog WHERE creationTime <= ?', [ creationTime ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - async.eachSeries(result, function (item, iteratorCallback) { - async.series([ - database.query.bind(null, 'DELETE FROM notifications WHERE eventId=?', [ item.id ]), - database.query.bind(null, 'DELETE FROM eventlog WHERE id=?', [ item.id ]) - ], iteratorCallback); - }, function (error) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); - }); -} diff --git a/src/ldap.js b/src/ldap.js index 64725542e..f03cf02a1 100644 --- a/src/ldap.js +++ b/src/ldap.js @@ -491,13 +491,13 @@ function authorizeUserForApp(req, res, next) { assert.strictEqual(typeof req.user, 'object'); assert.strictEqual(typeof req.app, 'object'); - apps.hasAccessTo(req.app, req.user, function (error, hasAccess) { + apps.hasAccessTo(req.app, req.user, async function (error, hasAccess) { if (error) return next(new ldap.OperationsError(error.toString())); // we return no such object, to avoid leakage of a users existence if (!hasAccess) return next(new ldap.NoSuchObjectError(req.dn.toString())); - eventlog.upsert(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) }); + eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) }); res.end(); }); @@ -548,12 +548,13 @@ function authenticateUserMailbox(req, res, next) { if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString())); - verifyMailboxPassword(mailbox, req.credentials || '', function (error, result) { + verifyMailboxPassword(mailbox, req.credentials || '', async function (error, result) { if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.message)); - eventlog.upsert(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) }); + eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) }); + res.end(); }); }); @@ -690,12 +691,13 @@ function authenticateMailAddon(req, res, next) { if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString())); - verifyMailboxPassword(mailbox, req.credentials || '', function (error, result) { + verifyMailboxPassword(mailbox, req.credentials || '', async function (error, result) { if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.message)); - eventlog.upsert(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) }); + eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) }); + res.end(); }); }); diff --git a/src/routes/eventlog.js b/src/routes/eventlog.js index e028e24cb..396173c22 100644 --- a/src/routes/eventlog.js +++ b/src/routes/eventlog.js @@ -5,36 +5,36 @@ exports = module.exports = { list }; -var BoxError = require('../boxerror.js'), +const BoxError = require('../boxerror.js'), eventlog = require('../eventlog.js'), HttpError = require('connect-lastmile').HttpError, - HttpSuccess = require('connect-lastmile').HttpSuccess; + HttpSuccess = require('connect-lastmile').HttpSuccess, + safe = require('safetydance'); -function get(req, res, next) { - eventlog.get(req.params.eventId, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); +async function get(req, res, next) { + const [error, event] = await safe(eventlog.get(req.params.eventId)); + if (error) return next(BoxError.toHttpError(error)); + if (!event) return next(new HttpError(404, 'Eventlog not found')); - next(new HttpSuccess(200, { event: result })); - }); + next(new HttpSuccess(200, { event })); } -function list(req, res, next) { - var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1; +async function list(req, res, next) { + const page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1; if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number')); - var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25; + const perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25; if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number')); if (req.query.actions && typeof req.query.actions !== 'string') return next(new HttpError(400, 'actions must be a comma separated string')); if (req.query.action && typeof req.query.action !== 'string') return next(new HttpError(400, 'action must be a string')); if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string')); - var actions = req.query.actions ? req.query.actions.split(',').map(function (s) { return s.trim(); }) : []; + const actions = req.query.actions ? req.query.actions.split(',').map(function (s) { return s.trim(); }) : []; if (req.query.action) actions.push(req.query.action); - eventlog.getAllPaged(actions, req.query.search || null, page, perPage, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, eventlogs] = await safe(eventlog.getAllPaged(actions, req.query.search || null, page, perPage)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { eventlogs: result })); - }); + next(new HttpSuccess(200, { eventlogs })); } diff --git a/src/routes/notifications.js b/src/routes/notifications.js index 7ed09d677..a4d22857a 100644 --- a/src/routes/notifications.js +++ b/src/routes/notifications.js @@ -7,7 +7,7 @@ exports = module.exports = { update }; -let assert = require('assert'), +const assert = require('assert'), BoxError = require('../boxerror.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, diff --git a/src/routes/test/common.js b/src/routes/test/common.js index e09acc90f..dd8014ffc 100644 --- a/src/routes/test/common.js +++ b/src/routes/test/common.js @@ -5,16 +5,26 @@ const async = require('async'), database = require('../../database.js'), expect = require('expect.js'), server = require('../../server.js'), - superagent = require('superagent'); + superagent = require('superagent'), + tokendb = require('../../tokendb.js'); exports = module.exports = { setup, cleanup, owner: { + id: null, username: 'superadmin', password: 'Foobar?1337', - email: 'silly@me.com', + email: 'superadmin@cloudron.local', + token: null + }, + + user: { + id: null, + username: 'user', + password: 'Foobar?1338', + email: 'user@cloudron.local', token: null }, @@ -22,7 +32,7 @@ exports = module.exports = { }; function setup(done) { - const owner = exports.owner, serverUrl = exports.serverUrl; + const owner = exports.owner, serverUrl = exports.serverUrl, user = exports.user; async.series([ server.start.bind(null), @@ -38,10 +48,28 @@ function setup(done) { // stash token for further use owner.token = result.body.token; + owner.id = result.body.id; callback(); }); + }, + + function createUser(callback) { + superagent.post(`${serverUrl}/api/v1/users`) + .query({ access_token: owner.token }) + .send({ username: user.username, email: user.email }) + .end(function (error, result) { + expect(error).to.not.be.ok(); + expect(result.statusCode).to.equal(201); + + user.id = result.body.id; + user.token = 'usertoken'; + + // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) + tokendb.add({ id: 'tid-3', accessToken: user.token, identifier: user.id, clientId: 'test-client-id', expires: Date.now() + 10000, scope: 'unused', name: 'fromtest' }, callback); + }); } + ], done); } diff --git a/src/routes/test/eventlog-test.js b/src/routes/test/eventlog-test.js index 5fd25ca70..8992f6644 100644 --- a/src/routes/test/eventlog-test.js +++ b/src/routes/test/eventlog-test.js @@ -6,214 +6,118 @@ 'use strict'; -var async = require('async'), - constants = require('../../constants.js'), - database = require('../../database.js'), - eventlogdb = require('../../eventlogdb.js'), +const async = require('async'), + common = require('./common.js'), + eventlog = require('../../eventlog.js'), expect = require('expect.js'), - hat = require('../../hat.js'), - superagent = require('superagent'), - server = require('../../server.js'), - tokendb = require('../../tokendb.js'); - -var SERVER_URL = 'http://localhost:' + constants.PORT; - -var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com'; -var token = null; - -var USER_1_ID = null, token_1; - -var EVENT_0 = { - id: 'event_0', - action: 'foobaraction', - source: { - ip: '127.0.0.1' - }, - data: { - something: 'is there' - } -}; - -function setup(done) { - async.series([ - server.start.bind(server), - - database._clear, - - function createAdmin(callback) { - superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .query({ setupToken: 'somesetuptoken' }) - .send({ username: USERNAME, password: PASSWORD, email: EMAIL }) - .end(function (error, result) { - expect(result).to.be.ok(); - expect(result.statusCode).to.eql(201); - - // stash token for further use - token = result.body.token; - - callback(); - }); - }, - - function (callback) { - superagent.post(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .send({ username: 'nonadmin', email: 'notadmin@server.test' }) - .end(function (err, res) { - expect(res.statusCode).to.equal(201); - - USER_1_ID = res.body.id; - - callback(null); - }); - }, - - function (callback) { - token_1 = hat(8 * 32); - - // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) - tokendb.add({ id: 'tid-0', accessToken: token_1, identifier: USER_1_ID, clientId: 'test-client-id', expires: Date.now() + 100000, scope: 'unused', name: '' }, callback); - }, - - function (callback) { - eventlogdb.add(EVENT_0.id, EVENT_0.action, EVENT_0.source, EVENT_0.data, callback); - } - - ], done); -} - -function cleanup(done) { - database._clear(function (error) { - expect(!error).to.be.ok(); - - server.stop(done); - }); -} + superagent = require('superagent'); describe('Eventlog API', function () { - before(setup); + const { setup, cleanup, serverUrl, owner, user } = common; + + const EVENT_0 = { + id: null, + action: 'foobaraction', + source: {ip: '127.0.0.1' }, + data: { something: 'is there' } + }; + + before(function (done) { + async.series([ + setup, + async () => { EVENT_0.id = await eventlog.add(EVENT_0.action, EVENT_0.source, EVENT_0.data); }, + ], done); + }); after(cleanup); describe('get', function () { - it('fails due to wrong token', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/' + EVENT_0.id) - .query({ access_token: token.toUpperCase() }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - - done(); - }); + it('fails due to wrong token', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/cloudron/eventlog/someid`) + .query({ access_token: 'badtoken' }) + .ok(() => true); + expect(response.statusCode).to.be(401); }); - it('fails for non-admin', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/' + EVENT_0.id) - .query({ access_token: token_1 }) - .end(function (error, result) { - expect(result.statusCode).to.equal(403); - - done(); - }); + it('fails for non-admin', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/cloudron/eventlog/someid`) + .query({ access_token: user.token }) + .ok(() => true); + expect(response.statusCode).to.equal(403); }); - it('fails if not exists', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/doesnotexist') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(404); + it('fails if not exists', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/cloudron/eventlog/someid`) + .query({ access_token: owner.token }) + .ok(() => true); - done(); - }); + expect(response.statusCode).to.equal(404); }); - it('succeeds for admin', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/' + EVENT_0.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - expect(result.body.event).to.be.an('object'); - expect(result.body.event.creationTime).to.be.a('string'); + it('succeeds for admin', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/cloudron/eventlog/${EVENT_0.id}`) + .query({ access_token: owner.token }) + .ok(() => true); - delete result.body.event.creationTime; - expect(result.body.event).to.eql(EVENT_0); - - done(); - }); + expect(response.statusCode).to.equal(200); + delete response.body.event.creationTime; + expect(response.body.event).to.eql(EVENT_0); }); }); describe('list', function () { - it('fails due to wrong token', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog') - .query({ access_token: token.toUpperCase() }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); + it('fails due to wrong token', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/cloudron/eventlog`) + .query({ access_token: 'badtoken' }) + .ok(() => true); + expect(response.statusCode).to.equal(401); }); - it('fails for non-admin', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog') - .query({ access_token: token_1, page: 1, per_page: 10 }) - .end(function (error, result) { - expect(result.statusCode).to.equal(403); + it('fails for non-admin', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/cloudron/eventlog`) + .query({ access_token: user.token, page: 1, per_page: 10 }) + .ok(() => true); - done(); - }); + expect(response.statusCode).to.equal(403); }); - it('succeeds for admin', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog') - .query({ access_token: token, page: 1, per_page: 10 }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - expect(result.body.eventlogs.length >= 2).to.be.ok(); // activate, user.add + it('succeeds for admin', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/cloudron/eventlog`) + .query({ access_token: owner.token, page: 1, per_page: 10 }); - done(); - }); + expect(response.statusCode).to.equal(200); + expect(response.body.eventlogs.length >= 2).to.be.ok(); // activate, user.add }); - it('succeeds with deprecated action', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog') - .query({ access_token: token, page: 1, per_page: 10, action: 'cloudron.activate' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - expect(result.body.eventlogs.length).to.equal(1); + it('succeeds with action', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/cloudron/eventlog`) + .query({ access_token: owner.token, page: 1, per_page: 10, action: 'cloudron.activate' }); - done(); - }); + expect(response.statusCode).to.equal(200); + expect(response.body.eventlogs.length).to.equal(1); }); - it('succeeds with actions', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog') - .query({ access_token: token, page: 1, per_page: 10, actions: 'cloudron.activate, user.add' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - expect(result.body.eventlogs.length).to.equal(3); + it('succeeds with actions', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/cloudron/eventlog`) + .query({ access_token: owner.token, page: 1, per_page: 10, actions: 'cloudron.activate, user.add' }); - done(); - }); + expect(response.statusCode).to.equal(200); + expect(response.body.eventlogs.length).to.equal(3); }); - it('succeeds with search', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog') - .query({ access_token: token, page: 1, per_page: 10, search: EMAIL }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - expect(result.body.eventlogs.length).to.equal(1); + it('succeeds with search', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/cloudron/eventlog`) + .query({ access_token: owner.token, page: 1, per_page: 10, search: owner.email }); - done(); - }); + expect(response.statusCode).to.equal(200); + expect(response.body.eventlogs.length).to.equal(1); }); - it('succeeds with search', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog') - .query({ access_token: token, page: 1, per_page: 10, search: EMAIL, actions: 'cloudron.activate' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - expect(result.body.eventlogs.length).to.equal(0); + it('succeeds with search and actions', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/cloudron/eventlog`) + .query({ access_token: owner.token, page: 1, per_page: 10, search: owner.email, actions: 'cloudron.activate' }); - done(); - }); + expect(response.statusCode).to.equal(200); + expect(response.body.eventlogs.length).to.equal(0); }); }); }); diff --git a/src/test/database-test.js b/src/test/database-test.js index 27db578f7..b20bad55e 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -14,7 +14,6 @@ const appdb = require('../appdb.js'), BoxError = require('../boxerror.js'), database = require('../database'), domaindb = require('../domaindb'), - eventlogdb = require('../eventlogdb.js'), expect = require('expect.js'), groupdb = require('../groupdb.js'), hat = require('../hat.js'), @@ -1306,185 +1305,6 @@ describe('database', function () { }); - describe('eventlog', function () { - - it('add succeeds', function (done) { - eventlogdb.add('someid', 'some.event', { ip: '1.2.3.4' }, { appId: 'thatapp' }, function (error, result) { - expect(error).to.be(null); - expect(result).to.equal('someid'); - done(); - }); - }); - - it('get succeeds', function (done) { - eventlogdb.get('someid', function (error, result) { - expect(error).to.be(null); - expect(result.id).to.be('someid'); - expect(result.action).to.be('some.event'); - expect(result.creationTime).to.be.a(Date); - - expect(result.source).to.be.eql({ ip: '1.2.3.4' }); - expect(result.data).to.be.eql({ appId: 'thatapp' }); - - done(); - }); - }); - - it('get of unknown id fails', function (done) { - eventlogdb.get('notfoundid', function (error, result) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.be(BoxError.NOT_FOUND); - expect(result).to.not.be.ok(); - done(); - }); - }); - - it('getAllPaged succeeds', function (done) { - eventlogdb.getAllPaged([], null, 1, 1, function (error, results) { - expect(error).to.be(null); - expect(results).to.be.an(Array); - expect(results.length).to.be(1); - - expect(results[0].id).to.be('someid'); - expect(results[0].action).to.be('some.event'); - expect(results[0].source).to.be.eql({ ip: '1.2.3.4' }); - expect(results[0].data).to.be.eql({ appId: 'thatapp' }); - - done(); - }); - }); - - it('getAllPaged succeeds with source search', function (done) { - eventlogdb.getAllPaged([], '1.2.3.4', 1, 1, function (error, results) { - expect(error).to.be(null); - expect(results).to.be.an(Array); - expect(results.length).to.be(1); - - expect(results[0].id).to.be('someid'); - expect(results[0].action).to.be('some.event'); - expect(results[0].source).to.be.eql({ ip: '1.2.3.4' }); - expect(results[0].data).to.be.eql({ appId: 'thatapp' }); - - done(); - }); - }); - - it('getAllPaged succeeds with data search', function (done) { - eventlogdb.getAllPaged([], 'thatapp', 1, 1, function (error, results) { - expect(error).to.be(null); - expect(results).to.be.an(Array); - expect(results.length).to.be(1); - - expect(results[0].id).to.be('someid'); - expect(results[0].action).to.be('some.event'); - expect(results[0].source).to.be.eql({ ip: '1.2.3.4' }); - expect(results[0].data).to.be.eql({ appId: 'thatapp' }); - - done(); - }); - }); - - it('upsert with no existing entry succeeds', function (done) { - eventlogdb.upsert('logineventid', 'user.login', { ip: '1.2.3.4' }, { appId: 'thatapp' }, function (error, result) { - expect(error).to.be(null); - expect(result).to.equal('logineventid'); - - done(); - }); - }); - - it('upsert with existing entry succeeds', function (done) { - eventlogdb.get('logineventid', function (error, result) { - expect(error).to.equal(null); - - var oldCreationTime = result.creationTime; - - // now wait 2sec - setTimeout(function () { - eventlogdb.upsert('logineventid_notused', 'user.login', { ip: '1.2.3.4' }, { appId: 'thatapp' }, function (error, result) { - expect(error).to.be(null); - expect(result).to.equal('logineventid'); - - eventlogdb.get('logineventid', function (error, result) { - expect(error).to.equal(null); - // should have changed - expect(oldCreationTime).to.not.equal(result.creationTime); - - done(); - }); - }); - }, 2000); - }); - }); - - it('upsert with existing old entry succeeds', function (done) { - var yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - - database.query('INSERT INTO eventlog (id, action, source, data, creationTime) VALUES (?, ?, ?, ?, ?)', [ 'anotherid', 'user.login2', JSON.stringify({ ip: '1.2.3.4' }), JSON.stringify({ appId: 'thatapp' }), yesterday ], function (error) { - expect(error).to.equal(null); - - eventlogdb.upsert('anotherid_new', 'user.login2', { ip: '1.2.3.4' }, { appId: 'thatapp' }, function (error, result) { - expect(error).to.be(null); - expect(result).to.equal('anotherid_new'); - - done(); - }); - }); - }); - - it('delByCreationTime succeeds', function (done) { - async.each([ 'persistent.event', 'transient.event', 'anothertransient.event', 'anotherpersistent.event' ], function (e, callback) { - eventlogdb.add('someid' + Math.random(), e, { ip: '1.2.3.4' }, { appId: 'thatapp' }, callback); - }, function (error) { - expect(error).to.be(null); - - eventlogdb.delByCreationTime(new Date(Date.now() + 1000), function (error) { - expect(error).to.be(null); - - eventlogdb.getAllPaged([], null, 1, 100, function (error, results) { - expect(error).to.be(null); - expect(results.length).to.be(0); - - done(); - }); - }); - }); - }); - - it('delByCreationTime succeeds with notifications referencing it', function (done) { - async.each([ 'persistent.event', 'transient.event', 'anothertransient.event', 'anotherpersistent.event' ], function (e, callback) { - var eventId = 'someid' + Math.random(); - - eventlogdb.add(eventId, e, { ip: '1.2.3.4' }, { appId: 'thatapp' }, function (error) { - expect(error).to.be(null); - - var notification = { - userId: USER_0.id, - eventId: eventId, - title: 'first one', - message: 'some message there', - }; - - notificationdb.add(notification, callback); - }); - }, function (error) { - expect(error).to.be(null); - - eventlogdb.delByCreationTime(new Date(), function (error) { - expect(error).to.be(null); - - eventlogdb.getAllPaged([], null, 1, 100, function (error, results) { - expect(error).to.be(null); - expect(results.length).to.be(0); - - done(); - }); - }); - }); - }); - }); - describe('groups', function () { before(function (done) { async.series([ diff --git a/src/test/eventlog-test.js b/src/test/eventlog-test.js index bca6904a9..da3077084 100644 --- a/src/test/eventlog-test.js +++ b/src/test/eventlog-test.js @@ -6,11 +6,12 @@ 'use strict'; -var async = require('async'), - BoxError = require('../boxerror.js'), +const async = require('async'), database = require('../database.js'), + delay = require('delay'), eventlog = require('../eventlog.js'), - expect = require('expect.js'); + expect = require('expect.js'), + notifications = require('../notifications.js'); function setup(done) { // ensure data/config/mount paths @@ -33,68 +34,111 @@ describe('Eventlog', function () { var eventId; - it('add succeeds', function (done) { - eventlog.add('some.event', { ip: '1.2.3.4' }, { appId: 'thatapp' }, function (error, result) { - expect(error).to.be(null); - expect(result.id).to.be.ok(); + it('add succeeds', async function () { + const id = await eventlog.add('some.event', { ip: '1.2.3.4' }, { appId: 'thatapp' }); + expect(id).to.be.a('string'); - eventId = result.id; - - done(); - }); + eventId = id; }); - it('get succeeds', function (done) { - eventlog.get(eventId, function (error, result) { - expect(error).to.be(null); - expect(result.id).to.be(eventId); - expect(result.action).to.be('some.event'); - expect(result.creationTime).to.be.a(Date); + it('get succeeds', async function () { + const result = await eventlog.get(eventId); + expect(result.id).to.be(eventId); + expect(result.action).to.be('some.event'); + expect(result.creationTime).to.be.a(Date); - expect(result.source).to.be.eql({ ip: '1.2.3.4' }); - expect(result.data).to.be.eql({ appId: 'thatapp' }); - - done(); - }); + expect(result.source).to.be.eql({ ip: '1.2.3.4' }); + expect(result.data).to.be.eql({ appId: 'thatapp' }); }); - it('get of unknown id fails', function (done) { - eventlog.get('notfoundid', function (error, result) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.be(BoxError.NOT_FOUND); - expect(result).to.not.be.ok(); - - done(); - }); + it('get of unknown id fails', async function () { + const result = await eventlog.get('notfoundid'); + expect(result).to.be(null); }); - it('getAllPaged succeeds', function (done) { - eventlog.getAllPaged([], null, 1, 1, function (error, results) { - expect(error).to.be(null); - expect(results).to.be.an(Array); - expect(results.length).to.be(1); + it('getAllPaged succeeds', async function () { + const results = await eventlog.getAllPaged([], null, 1, 1); + expect(results).to.be.an(Array); + expect(results.length).to.be(1); - expect(results[0].id).to.be(eventId); - expect(results[0].action).to.be('some.event'); - expect(results[0].source).to.be.eql({ ip: '1.2.3.4' }); - expect(results[0].data).to.be.eql({ appId: 'thatapp' }); - - done(); - }); + expect(results[0].id).to.be(eventId); + expect(results[0].action).to.be('some.event'); + expect(results[0].source).to.be.eql({ ip: '1.2.3.4' }); + expect(results[0].data).to.be.eql({ appId: 'thatapp' }); }); - it('cleans up token', function (done) { - eventlog.cleanup(function (error) { - expect(error).to.be(null); + it('getAllPaged succeeds with source search', async function () { + const results = await eventlog.getAllPaged([], '1.2.3.4', 1, 1); + expect(results).to.be.an(Array); + expect(results.length).to.be(1); + expect(results[0].id).to.be(eventId); + expect(results[0].action).to.be('some.event'); + expect(results[0].source).to.be.eql({ ip: '1.2.3.4' }); + expect(results[0].data).to.be.eql({ appId: 'thatapp' }); + }); - eventlog.get(eventId, function (error, result) { // should not have deleted it - expect(error).to.be(null); - expect(result.id).to.be(eventId); - expect(result.action).to.be('some.event'); - expect(result.creationTime).to.be.a(Date); + it('getAllPaged succeeds with data search', async function () { + const results = await eventlog.getAllPaged([], 'thatapp', 1, 1); + expect(results).to.be.an(Array); + expect(results.length).to.be(1); + expect(results[0].id).to.be(eventId); + expect(results[0].action).to.be('some.event'); + expect(results[0].source).to.be.eql({ ip: '1.2.3.4' }); + expect(results[0].data).to.be.eql({ appId: 'thatapp' }); + }); - done(); - }); - }); + let loginEventId; + it('upsert with no existing entry succeeds', async function () { + const result = await eventlog.upsertLoginEvent('user.login', { ip: '1.2.3.4' }, { appId: 'thatapp' }); + expect(result).to.be.a('string'); + loginEventId = result; + }); + + it('upsert with existing entry succeeds', async function () { + let result = await eventlog.get(loginEventId); + const oldCreationTime = result.creationTime; + + await delay(2000); + result = await eventlog.upsertLoginEvent('user.login', { ip: '1.2.3.4' }, { appId: 'thatapp' }); + expect(result).to.equal(loginEventId); + + result = await eventlog.get(loginEventId); + // should have changed + expect(oldCreationTime).to.not.equal(result.creationTime); + }); + + it('upsert with existing old entry succeeds', async function () { + let yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + + await database.query('INSERT INTO eventlog (id, action, source, data, creationTime) VALUES (?, ?, ?, ?, ?)', [ 'anotherid', 'user.login2', JSON.stringify({ ip: '1.2.3.4' }), JSON.stringify({ appId: 'thatapp' }), yesterday ]); + + const result = await eventlog.upsertLoginEvent('user.login2', { ip: '1.2.3.4' }, { appId: 'thatapp' }); + expect(result).to.not.equal('anotherid'); + }); + + it('cleans up', async function () { + await eventlog._clear(); + + for (const e of [ 'persistent.event', 'transient.event', 'anothertransient.event', 'anotherpersistent.event' ]) { + const eventId = await eventlog.add(e, { ip: '1.2.3.4' }, { appId: 'thatapp' }); + + await notifications._add(eventId, 'title', 'some message'); + } + + await delay(2000); + + const id = await eventlog.add('some.event', { ip: '1.2.3.4' }, { appId: 'thatapp' }); + + await eventlog.cleanup({ creationTime: new Date(Date.now() - 1000) }); // 1 second ago + let results = await eventlog.getAllPaged([], null, 1, 100); + expect(results.length).to.be(1); + + expect(results[0].id).to.be(id); + expect(results[0].action).to.be('some.event'); + expect(results[0].creationTime).to.be.a(Date); + + results = await notifications.list({}, 1, 10); + expect(results.length).to.be(0); }); });