diff --git a/src/apppasswords.js b/src/apppasswords.js new file mode 100644 index 000000000..909f7cacd --- /dev/null +++ b/src/apppasswords.js @@ -0,0 +1,88 @@ +'use strict'; + +exports = module.exports = { + get, + add, + list, + del, + + removePrivateFields +}; + +const assert = require('assert'), + BoxError = require('./boxerror.js'), + crypto = require('crypto'), + database = require('./database.js'), + hat = require('./hat.js'), + safe = require('safetydance'), + uuid = require('uuid'), + _ = require('underscore'); + +const APP_PASSWORD_FIELDS = [ 'id', 'name', 'userId', 'identifier', 'hashedPassword', 'creationTime' ].join(','); + +function validateAppPasswordName(name) { + assert.strictEqual(typeof name, 'string'); + + if (name.length < 1) return new BoxError(BoxError.BAD_FIELD, 'name must be atleast 1 char'); + if (name.length >= 200) return new BoxError(BoxError.BAD_FIELD, 'name too long'); + + return null; +} + +function removePrivateFields(appPassword) { + return _.pick(appPassword, 'id', 'name', 'userId', 'identifier', 'creationTime'); +} + +async function get(id) { + assert.strictEqual(typeof id, 'string'); + + const result = await database.query('SELECT ' + APP_PASSWORD_FIELDS + ' FROM appPasswords WHERE id = ?', [ id ]); + if (result.length === 0) return null; + return result[0]; +} + +async function add(userId, identifier, name) { + assert.strictEqual(typeof userId, 'string'); + assert.strictEqual(typeof identifier, 'string'); + assert.strictEqual(typeof name, 'string'); + + let error = validateAppPasswordName(name); + if (error) throw error; + + if (identifier.length < 1) throw new BoxError(BoxError.BAD_FIELD, 'identifier must be atleast 1 char'); + + const password = hat(16 * 4); + const hashedPassword = crypto.createHash('sha256').update(password).digest('base64'); + + const appPassword = { + id: 'uid-' + uuid.v4(), + name, + userId, + identifier, + password, + hashedPassword + }; + + const query = 'INSERT INTO appPasswords (id, userId, identifier, name, hashedPassword) VALUES (?, ?, ?, ?, ?)'; + const args = [ appPassword.id, appPassword.userId, appPassword.identifier, appPassword.name, appPassword.hashedPassword ]; + + [error] = await safe(database.query(query, args)); + if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('appPasswords_name_userId_identifier') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'name/app combination already exists'); + if (error && error.code === 'ER_NO_REFERENCED_ROW_2' && error.sqlMessage.indexOf('userId')) throw new BoxError(BoxError.NOT_FOUND, 'user not found'); + if (error) throw error; + + return { id: appPassword.id, password: appPassword.password }; +} + +async function list(userId) { + assert.strictEqual(typeof userId, 'string'); + + return await database.query('SELECT ' + APP_PASSWORD_FIELDS + ' FROM appPasswords WHERE userId = ?', [ userId ]); +} + +async function del(id) { + assert.strictEqual(typeof id, 'string'); + + const result = await database.query('DELETE FROM appPasswords WHERE id = ?', [ id ]); + if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'password not found'); +} diff --git a/src/routes/apppasswords.js b/src/routes/apppasswords.js index cb501f997..fb9c9afbe 100644 --- a/src/routes/apppasswords.js +++ b/src/routes/apppasswords.js @@ -7,55 +7,54 @@ exports = module.exports = { add }; -var assert = require('assert'), +const appPasswords = require('../apppasswords.js'), + assert = require('assert'), BoxError = require('../boxerror.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, - users = require('../users.js'); + safe = require('safetydance'); -function get(req, res, next) { +async function get(req, res, next) { assert.strictEqual(typeof req.user, 'object'); assert.strictEqual(typeof req.params.id, 'string'); - users.getAppPassword(req.params.id, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(appPasswords.get(req.params.id)); + if (error) return next(BoxError.toHttpError(error)); + if (!result) return next(new HttpError(404, 'appPassword not found')); - next(new HttpSuccess(200, result)); - }); + next(new HttpSuccess(200, appPasswords.removePrivateFields(result))); } -function add(req, res, next) { +async function add(req, res, next) { assert.strictEqual(typeof req.user, 'object'); assert.strictEqual(typeof req.body, 'object'); if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be string')); if (typeof req.body.identifier !== 'string') return next(new HttpError(400, 'identifier must be string')); - users.addAppPassword(req.user.id, req.body.identifier, req.body.name, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(appPasswords.add(req.user.id, req.body.identifier, req.body.name)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(201, result)); - }); + next(new HttpSuccess(201, { id: result.id, password: result.password })); } -function list(req, res, next) { +async function list(req, res, next) { assert.strictEqual(typeof req.user, 'object'); - users.getAppPasswords(req.user.id, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + let [error, result] = await safe(appPasswords.list(req.user.id)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { appPasswords: result })); - }); + result = result.map(appPasswords.removePrivateFields); + next(new HttpSuccess(200, { appPasswords: result })); } -function del(req, res, next) { +async function del(req, res, next) { assert.strictEqual(typeof req.user, 'object'); assert.strictEqual(typeof req.params.id, 'string'); // TODO: verify userId owns the id ? - users.delAppPassword(req.params.id, function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(appPasswords.del(req.params.id)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(204, {})); - }); + next(new HttpSuccess(204, {})); } diff --git a/src/routes/profile.js b/src/routes/profile.js index 74bbc3321..497b8bfac 100644 --- a/src/routes/profile.js +++ b/src/routes/profile.js @@ -13,7 +13,7 @@ exports = module.exports = { disableTwoFactorAuthentication, }; -var assert = require('assert'), +const assert = require('assert'), auditSource = require('../auditsource.js'), BoxError = require('../boxerror.js'), HttpError = require('connect-lastmile').HttpError, @@ -35,24 +35,24 @@ function authorize(req, res, next) { }); } -function get(req, res, next) { +async function get(req, res, next) { assert.strictEqual(typeof req.user, 'object'); - users.getAvatarUrl(req.user, function (error, avatarUrl) { - if (error) return next(BoxError.toHttpError(error)); + const [error, avatarUrl] = await safe(users.getAvatarUrl(req.user)); + if (error) return next(BoxError.toHttpError(error)); + if (!avatarUrl) return next(new HttpError(404, 'User not found')); - next(new HttpSuccess(200, { - id: req.user.id, - username: req.user.username, - email: req.user.email, - fallbackEmail: req.user.fallbackEmail, - displayName: req.user.displayName, - twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled, - role: req.user.role, - source: req.user.source, - avatarUrl - })); - }); + next(new HttpSuccess(200, { + id: req.user.id, + username: req.user.username, + email: req.user.email, + fallbackEmail: req.user.fallbackEmail, + displayName: req.user.displayName, + twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled, + role: req.user.role, + source: req.user.source, + avatarUrl + })); } function update(req, res, next) { @@ -72,7 +72,7 @@ function update(req, res, next) { }); } -function setAvatar(req, res, next) { +async function setAvatar(req, res, next) { assert.strictEqual(typeof req.user, 'object'); if (!req.files.avatar) return next(new HttpError(400, 'avatar is missing')); @@ -80,31 +80,29 @@ function setAvatar(req, res, next) { const avatar = safe.fs.readFileSync(req.files.avatar.path); if (!avatar) return next(BoxError.toHttpError(new BoxError(BoxError.FS_ERROR, safe.error.message))); - users.setAvatar(req.user.id, avatar, function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(users.setAvatar(req.user.id, avatar)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, {})); - }); + next(new HttpSuccess(202, {})); } -function clearAvatar(req, res, next) { +async function clearAvatar(req, res, next) { assert.strictEqual(typeof req.user, 'object'); - users.setAvatar(req.user.id, null, function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(users.setAvatar(req.user.id, null)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, {})); - }); + next(new HttpSuccess(202, {})); } -function getAvatar(req, res, next) { +async function getAvatar(req, res, next) { assert.strictEqual(typeof req.params.identifier, 'string'); - users.getAvatar(req.params.identifier, function (error, avatar) { - if (error) return next(BoxError.toHttpError(error)); + const [error, avatar] = await safe(users.getAvatar(req.params.identifier)); + if (error) return next(BoxError.toHttpError(error)); + if (!avatar) return next(new HttpError(404, 'User not found')); - res.send(avatar); - }); + res.send(avatar); } function changePassword(req, res, next) { diff --git a/src/routes/test/apppasswords-test.js b/src/routes/test/apppasswords-test.js new file mode 100644 index 000000000..4b5bfa3a0 --- /dev/null +++ b/src/routes/test/apppasswords-test.js @@ -0,0 +1,80 @@ +/* jslint node:true */ +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +'use strict'; + +const common = require('./common.js'), + expect = require('expect.js'), + superagent = require('superagent'); + +describe('App Passwords', function () { + const { setup, cleanup, serverUrl, user } = common; + + before(setup); + after(cleanup); + + describe('app password', function () { + it('cannot add app password with invalid token', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/app_passwords`) + .query({ access_token: user.token + 'xx' }) + .send({ name: 'my-device', identifier: 'someapp' }) + .ok(() => true); + + expect(response.statusCode).to.equal(401); + }); + + it('cannot add app password without name', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/app_passwords`) + .query({ access_token: user.token }) + .send({ identifier: 'someapp' }) + .ok(() => true); + + expect(response.statusCode).to.equal(400); + }); + + let pwd; + it('can add app password', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/app_passwords`) + .query({ access_token: user.token }) + .send({ name: 'my-device', identifier: 'someapp' }); + + expect(response.statusCode).to.equal(201); + expect(response.body.password).to.be.a('string'); + pwd = response.body; + }); + + it('can get app passwords', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/app_passwords`) + .query({ access_token: user.token }); + + expect(response.statusCode).to.equal(200); + expect(response.body.appPasswords).to.be.an(Array); + expect(response.body.appPasswords.length).to.be(1); + expect(response.body.appPasswords[0].name).to.be('my-device'); + expect(response.body.appPasswords[0].identifier).to.be('someapp'); + expect(response.body.appPasswords[0].hashedPassword).to.be(undefined); + expect(response.body.appPasswords[0].password).to.be(undefined); + }); + + it('can get app password', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/app_passwords/${pwd.id}`) + .query({ access_token: user.token }); + + expect(response.statusCode).to.equal(200); + expect(response.body.name).to.be('my-device'); + expect(response.body.identifier).to.be('someapp'); + expect(response.body.hashedPassword).to.be(undefined); + expect(response.body.password).to.be(undefined); + }); + + it('can del app password', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/app_passwords/${pwd.id}`) + .query({ access_token: user.token }); + + expect(response.statusCode).to.equal(204); + }); + }); +}); diff --git a/src/routes/test/profile-test.js b/src/routes/test/profile-test.js index 8110d3b74..c7ab34a55 100644 --- a/src/routes/test/profile-test.js +++ b/src/routes/test/profile-test.js @@ -258,65 +258,53 @@ describe('Profile API', function () { }); }); - describe('app password', function () { - it('cannot add app password with invalid token', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/app_passwords`) - .query({ access_token: user.token + 'xx' }) - .send({ name: 'my-device', identifier: 'someapp' }) + describe('avatar', function () { + it('has no avatar by default (public route)', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/profile`) + .query({ access_token: user.token }); + expect(response.body.avatarUrl).to.contain('www.gravatar.com'); + + const response2 = await superagent.get(`${serverUrl}/api/v1/profile/avatar/${user.id}`) .ok(() => true); - expect(response.statusCode).to.equal(401); + expect(response2.statusCode).to.equal(404); }); - it('cannot add app password without name', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/app_passwords`) + it('can set avatar', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/profile/avatar`) .query({ access_token: user.token }) - .send({ identifier: 'someapp' }) + .attach('avatar', './logo.png'); + + expect(response.statusCode).to.be(202); + }); + + it('did set avatar', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/profile`) + .query({ access_token: user.token }); + expect(response.body.avatarUrl).to.contain('/api/v1/profile/avatar/'); + + const response2 = await superagent.get(`${serverUrl}/api/v1/profile/avatar/${user.id}`) .ok(() => true); - expect(response.statusCode).to.equal(400); + expect(response2.statusCode).to.equal(200); }); - let pwd; - it('can add app password', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/app_passwords`) - .query({ access_token: user.token }) - .send({ name: 'my-device', identifier: 'someapp' }); - - expect(response.statusCode).to.equal(201); - expect(response.body.password).to.be.a('string'); - pwd = response.body; - }); - - it('can get app passwords', async function () { - const response = await superagent.get(`${serverUrl}/api/v1/app_passwords`) + it('can clear avatar', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/profile/avatar`) .query({ access_token: user.token }); - expect(response.statusCode).to.equal(200); - expect(response.body.appPasswords).to.be.an(Array); - expect(response.body.appPasswords.length).to.be(1); - expect(response.body.appPasswords[0].name).to.be('my-device'); - expect(response.body.appPasswords[0].identifier).to.be('someapp'); - expect(response.body.appPasswords[0].hashedPassword).to.be(undefined); - expect(response.body.appPasswords[0].password).to.be(undefined); + expect(response.statusCode).to.be(202); }); - it('can get app password', async function () { - const response = await superagent.get(`${serverUrl}/api/v1/app_passwords/${pwd.id}`) + it('did clear avatar', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/profile`) .query({ access_token: user.token }); + expect(response.body.avatarUrl).to.contain('www.gravatar.com'); - expect(response.statusCode).to.equal(200); - expect(response.body.name).to.be('my-device'); - expect(response.body.identifier).to.be('someapp'); - expect(response.body.hashedPassword).to.be(undefined); - expect(response.body.password).to.be(undefined); - }); + const response2 = await superagent.get(`${serverUrl}/api/v1/profile/avatar/${user.id}`) + .ok(() => true); - it('can del app password', async function () { - const response = await superagent.del(`${serverUrl}/api/v1/app_passwords/${pwd.id}`) - .query({ access_token: user.token }); - - expect(response.statusCode).to.equal(204); + expect(response2.statusCode).to.equal(404); }); }); }); diff --git a/src/test/apppasswords-test.js b/src/test/apppasswords-test.js new file mode 100644 index 000000000..a889261a1 --- /dev/null +++ b/src/test/apppasswords-test.js @@ -0,0 +1,62 @@ +/* jslint node:true */ +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +'use strict'; + +const appPasswords = require('../apppasswords.js'), + BoxError = require('../boxerror.js'), + common = require('./common.js'), + expect = require('expect.js'), + safe = require('safetydance'); + +describe('App passwords', function () { + const { setup, cleanup, ADMIN } = common; + + before(setup); + after(cleanup); + + let id; + it('cannot add bad app password', async function () { + const [error] = await safe(appPasswords.add(ADMIN.id, 'appid', 'x'.repeat(201))); + expect(error.reason).to.be(BoxError.BAD_FIELD); + }); + + it('can add app password', async function () { + const result = await appPasswords.add(ADMIN.id, 'appid', 'spark'); + expect(result.id).to.be.a('string'); + expect(result.password).to.be.a('string'); + id = result.id; + }); + + it('can get app password', async function () { + const result = await appPasswords.get(id); + expect(result.hashedPassword).to.be.a('string'); + expect(result.name).to.be('spark'); + expect(result.identifier).to.be('appid'); + }); + + it('cannot get random app password', async function () { + const result = await appPasswords.get('random'); + expect(result).to.be(null); + }); + + it('can get app passwords', async function () { + const results = await appPasswords.list(ADMIN.id); + expect(results.length).to.be(1); + expect(results[0].hashedPassword).to.be.a('string'); + expect(results[0].name).to.be('spark'); + expect(results[0].identifier).to.be('appid'); + }); + + it('can del app password', async function () { + await appPasswords.del(id); + }); + + it('cannot del random app password', async function () { + const [error] = await safe(appPasswords.del('random')); + expect(error.reason).to.be(BoxError.NOT_FOUND); + }); +}); diff --git a/src/test/blobs-test.js b/src/test/blobs-test.js new file mode 100644 index 000000000..682672f3b --- /dev/null +++ b/src/test/blobs-test.js @@ -0,0 +1,36 @@ +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +'use strict'; + +const blobs = require('../blobs.js'), + common = require('./common.js'), + expect = require('expect.js'); + +describe('blobs', function () { + const { setup, cleanup } = common; + + before(setup); + after(cleanup); + + it('can set value', async function () { + await blobs.set('someid', Buffer.from('somevalue')); + }); + it('can get the set value', async function () { + const value = await blobs.get('someid'); + expect(value).to.eql(Buffer.from('somevalue')); + }); + it('can update a value', async function () { + await blobs.set('someid', Buffer.from('someothervalue')); + }); + it('can get updated value', async function () { + const value = await blobs.get('someid'); + expect(value).to.eql(Buffer.from('someothervalue')); + }); + it('cannot get randome value', async function () { + const value = await blobs.get('unknownid'); + expect(value).to.be(null); + }); +}); diff --git a/src/test/common.js b/src/test/common.js index 62a08ff82..d9813aaa5 100644 --- a/src/test/common.js +++ b/src/test/common.js @@ -137,7 +137,13 @@ function setup(done) { settings.initCache, blobs.initSecrets, domains.add.bind(null, DOMAIN.domain, DOMAIN, AUDIT_SOURCE), - users.createOwner.bind(null, ADMIN.username, ADMIN.password, ADMIN.email, ADMIN.displayName, AUDIT_SOURCE), + function createOwner(done) { + users.createOwner(ADMIN.username, ADMIN.password, ADMIN.email, ADMIN.displayName, AUDIT_SOURCE, function (error, result) { + if (error) return done(error); + ADMIN.id = result.id; + done(); + }); + }, appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.domain, APP.portBindings, APP), settingsdb.set.bind(null, settings.CLOUDRON_TOKEN_KEY, exports.APPSTORE_TOKEN), // appstore token ], done); diff --git a/src/test/database-test.js b/src/test/database-test.js index 7190bb36a..20db381ee 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -10,7 +10,6 @@ const appdb = require('../appdb.js'), async = require('async'), backupdb = require('../backupdb.js'), backups = require('../backups.js'), - blobs = require('../blobs.js'), BoxError = require('../boxerror.js'), database = require('../database'), domaindb = require('../domaindb'), @@ -512,44 +511,6 @@ describe('database', function () { }); }); - describe('appPasswords', function () { - before(function (done) { - userdb.add(USER_0.id, USER_0, done); - }); - after(function (done) { - userdb.del(USER_0.id, done); - }); - - it('can add app password', function (done) { - userdb.addAppPassword('someid', { userId: USER_0.id, hashedPassword: 'hash', name: 'spark', identifier: 'appid' }, done); - }); - - it('can get app password', function (done) { - userdb.getAppPassword('someid', function (error, result) { - if (error) return done(error); - expect(result.hashedPassword).to.be('hash'); - expect(result.name).to.be('spark'); - expect(result.identifier).to.be('appid'); - done(); - }); - }); - - it('can get app passwords', function (done) { - userdb.getAppPasswords(USER_0.id, function (error, results) { - if (error) return done(error); - expect(results.length).to.be(1); - expect(results[0].hashedPassword).to.be('hash'); - expect(results[0].name).to.be('spark'); - expect(results[0].identifier).to.be('appid'); - done(); - }); - }); - - it('can del app password', function (done) { - userdb.delAppPassword('someid', done); - }); - }); - describe('apps', function () { var APP_0 = { id: 'appid-0', @@ -1008,27 +969,6 @@ describe('database', function () { }); - describe('blobs', function () { - it('can set value', async function () { - await blobs.set('someid', Buffer.from('somevalue')); - }); - it('can get the set value', async function () { - const value = await blobs.get('someid'); - expect(value).to.eql(Buffer.from('somevalue')); - }); - it('can update a value', async function () { - await blobs.set('someid', Buffer.from('someothervalue')); - }); - it('can get updated value', async function () { - const value = await blobs.get('someid'); - expect(value).to.eql(Buffer.from('someothervalue')); - }); - it('cannot get randome value', async function () { - const value = await blobs.get('unknownid'); - expect(value).to.be(null); - }); - }); - describe('backup', function () { it('add succeeds', function (done) { diff --git a/src/userdb.js b/src/userdb.js index 8872198ea..85f76f2a8 100644 --- a/src/userdb.js +++ b/src/userdb.js @@ -13,29 +13,20 @@ exports = module.exports = { del, update, count, - getAvatar, - setAvatar, - - addAppPassword, - getAppPasswords, - getAppPassword, - delAppPassword, _clear: clear }; +// the avatar field is special and not added here to reduce response sizes +const USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'creationTime', 'resetToken', 'displayName', + 'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime', 'loginLocationsJson' ].join(','); + const assert = require('assert'), BoxError = require('./boxerror.js'), database = require('./database.js'), mysql = require('mysql'), safe = require('safetydance'); -// the avatar field is special and not added here to reduce response sizes -const USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'creationTime', 'resetToken', 'displayName', - 'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime', 'loginLocationsJson' ].join(','); - -const APP_PASSWORD_FIELDS = [ 'id', 'name', 'userId', 'identifier', 'hashedPassword', 'creationTime' ].join(','); - function postProcess(result) { assert.strictEqual(typeof result, 'object'); @@ -280,77 +271,3 @@ function count(callback) { }); } -function getAppPasswords(userId, callback) { - assert.strictEqual(typeof userId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + APP_PASSWORD_FIELDS + ' FROM appPasswords WHERE userId = ?', [ userId ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null, results); - }); -} - -function getAppPassword(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + APP_PASSWORD_FIELDS + ' FROM appPasswords WHERE id = ?', [ id ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null, results[0]); - }); -} - -function addAppPassword(id, appPassword, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof appPassword, 'object'); - assert.strictEqual(typeof callback, 'function'); - - const query = 'INSERT INTO appPasswords (id, userId, identifier, name, hashedPassword) VALUES (?, ?, ?, ?, ?)'; - const args = [ id, appPassword.userId, appPassword.identifier, appPassword.name, appPassword.hashedPassword ]; - - database.query(query, args, function (error) { - if (error && error.code === 'ER_DUP_ENTRY' && error.message.indexOf('appPasswords_name_userId_identifier') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'name/app combination already exists')); - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - -function delAppPassword(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM appPasswords WHERE id = ?', [ id ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'password not found')); - - return callback(null); - }); -} - -function getAvatar(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT avatar FROM users WHERE id = ?', [ id ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); - - callback(null, result[0].avatar); - }); -} - -function setAvatar(id, avatar, callback) { - assert.strictEqual(typeof id, 'string'); - assert(avatar === null || typeof Buffer.isBuffer(avatar)); - assert.strictEqual(typeof callback, 'function'); - - database.query('UPDATE users SET avatar=? WHERE id = ?', [ avatar, id ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); - - callback(null); - }); -} diff --git a/src/users.js b/src/users.js index ca0a114bd..4fc39b674 100644 --- a/src/users.js +++ b/src/users.js @@ -47,19 +47,16 @@ exports = module.exports = { ROLE_USER_MANAGER: 'usermanager', ROLE_OWNER: 'owner', compareRoles, - - getAppPasswords, - getAppPassword, - addAppPassword, - delAppPassword }; const ORDERED_ROLES = [ exports.ROLE_USER, exports.ROLE_USER_MANAGER, exports.ROLE_ADMIN, exports.ROLE_OWNER ]; -const assert = require('assert'), +const appPasswords = require('./apppasswords.js'), + assert = require('assert'), BoxError = require('./boxerror.js'), crypto = require('crypto'), constants = require('./constants.js'), + database = require('./database.js'), debug = require('debug')('box:user'), eventlog = require('./eventlog.js'), externalLdap = require('./externalldap.js'), @@ -79,10 +76,10 @@ const assert = require('assert'), validator = require('validator'), _ = require('underscore'); -var CRYPTO_SALT_SIZE = 64; // 512-bit salt -var CRYPTO_ITERATIONS = 10000; // iterations -var CRYPTO_KEY_LENGTH = 512; // bits -var CRYPTO_DIGEST = 'sha1'; // used to be the default in node 4.1.1 cannot change since it will affect existing db records +const CRYPTO_SALT_SIZE = 64; // 512-bit salt +const CRYPTO_ITERATIONS = 10000; // iterations +const CRYPTO_KEY_LENGTH = 512; // bits +const CRYPTO_DIGEST = 'sha1'; // used to be the default in node 4.1.1 cannot change since it will affect existing db records // keep this in sync with validateGroupname and validateAlias function validateUsername(username) { @@ -230,22 +227,21 @@ function verifyGhost(username, password) { return false; } -function verifyAppPassword(userId, password, identifier, callback) { +async function verifyAppPassword(userId, password, identifier, callback) { assert.strictEqual(typeof userId, 'string'); assert.strictEqual(typeof password, 'string'); assert.strictEqual(typeof identifier, 'string'); assert.strictEqual(typeof callback, 'function'); - userdb.getAppPasswords(userId, function (error, results) { - if (error) return callback(error); + const [error, results] = await safe(appPasswords.list(userId)); + if (error) return callback(error); - const hashedPasswords = results.filter(r => r.identifier === identifier).map(r => r.hashedPassword); - let hash = crypto.createHash('sha256').update(password).digest('base64'); + const hashedPasswords = results.filter(r => r.identifier === identifier).map(r => r.hashedPassword); + let hash = crypto.createHash('sha256').update(password).digest('base64'); - if (hashedPasswords.includes(hash)) return callback(null); + if (hashedPasswords.includes(hash)) return callback(null); - return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - }); + return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); } function verify(userId, password, identifier, callback) { @@ -784,108 +780,29 @@ function compareRoles(role1, role2) { return roleInt1 - roleInt2; } -function validateAppPasswordName(name) { - assert.strictEqual(typeof name, 'string'); - - if (name.length < 1) return new BoxError(BoxError.BAD_FIELD, 'name must be atleast 1 char'); - if (name.length >= 200) return new BoxError(BoxError.BAD_FIELD, 'name too long'); - - return null; -} - -function getAppPassword(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - userdb.getAppPassword(id, function (error, result) { - if (error) return callback(error); - - callback(null, _.omit(result, 'hashedPassword')); - }); -} - -function addAppPassword(userId, identifier, name, callback) { - assert.strictEqual(typeof userId, 'string'); - assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof callback, 'function'); - - let error = validateAppPasswordName(name); - if (error) return callback(error); - - if (identifier.length < 1) return callback(new BoxError(BoxError.BAD_FIELD, 'identifier must be atleast 1 char')); - - const password = hat(16 * 4); - const hashedPassword = crypto.createHash('sha256').update(password).digest('base64'); - - var appPassword = { - id: 'uid-' + uuid.v4(), - name, - userId, - identifier, - password, - hashedPassword - }; - - userdb.addAppPassword(appPassword.id, appPassword, function (error) { - if (error) return callback(error); - - callback(null, _.omit(appPassword, 'hashedPassword')); - }); -} - -function getAppPasswords(userId, callback) { - assert.strictEqual(typeof userId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - userdb.getAppPasswords(userId, function (error, results) { - if (error) return callback(error); - - results.map(r => delete r.hashedPassword); - - callback(null, results); - }); -} - -function delAppPassword(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - userdb.delAppPassword(id, function (error) { - if (error) return callback(error); - - callback(null); - }); -} - -function getAvatarUrl(user, callback) { +async function getAvatarUrl(user) { assert.strictEqual(typeof user, 'object'); - assert.strictEqual(typeof callback, 'function'); - userdb.getAvatar(user.id, function (error, avatar) { - if (error) return callback(error); - if (avatar) return callback(null, `${settings.dashboardOrigin()}/api/v1/profile/avatar/${user.id}`); + const result = await database.query('SELECT avatar FROM users WHERE id = ?', [ user.id ]); + if (result.length === 0) return null; + if (result[0].avatar) return `${settings.dashboardOrigin()}/api/v1/profile/avatar/${user.id}`; - const emailHash = require('crypto').createHash('md5').update(user.email).digest('hex'); - return callback(null, `https://www.gravatar.com/avatar/${emailHash}.jpg`); - }); + const emailHash = require('crypto').createHash('md5').update(user.email).digest('hex'); + return `https://www.gravatar.com/avatar/${emailHash}.jpg`; } -function getAvatar(id, callback) { +async function getAvatar(id) { assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - userdb.getAvatar(id, function (error, avatar) { - if (error) return callback(error); - - return callback(null, avatar); - }); + const result = await database.query('SELECT avatar FROM users WHERE id = ?', [ id ]); + if (result.length === 0) return null; + return result[0].avatar; } -function setAvatar(id, avatar, callback) { +async function setAvatar(id, avatar) { assert.strictEqual(typeof id, 'string'); assert(avatar === null || Buffer.isBuffer(avatar)); - assert.strictEqual(typeof callback, 'function'); - userdb.setAvatar(id, avatar, callback); + const result = await database.query('UPDATE users SET avatar=? WHERE id = ?', [ avatar, id ]); + if (result.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); }